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.
Files changed (183) hide show
  1. {gemcode-0.4.16/src/gemcode.egg-info → gemcode-0.4.18}/PKG-INFO +7 -1
  2. {gemcode-0.4.16 → gemcode-0.4.18}/README.md +6 -0
  3. {gemcode-0.4.16 → gemcode-0.4.18}/pyproject.toml +1 -1
  4. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/agent_habits.py +26 -6
  5. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/agent_mesh.py +71 -0
  6. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/repl_commands.py +1 -0
  7. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/repl_slash.py +60 -0
  8. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/__init__.py +28 -0
  9. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/bash.py +25 -1
  10. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/shell.py +41 -1
  11. {gemcode-0.4.16 → gemcode-0.4.18/src/gemcode.egg-info}/PKG-INFO +7 -1
  12. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_agent_mesh.py +26 -0
  13. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_tools.py +19 -12
  14. {gemcode-0.4.16 → gemcode-0.4.18}/LICENSE +0 -0
  15. {gemcode-0.4.16 → gemcode-0.4.18}/MANIFEST.in +0 -0
  16. {gemcode-0.4.16 → gemcode-0.4.18}/setup.cfg +0 -0
  17. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/__init__.py +0 -0
  18. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/__main__.py +0 -0
  19. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/a2a_bridge.py +0 -0
  20. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/agent.py +0 -0
  21. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/agent_intelligence.py +0 -0
  22. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/agent_triggers.py +0 -0
  23. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/audit.py +0 -0
  24. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/autocompact.py +0 -0
  25. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/automations.py +0 -0
  26. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/autotune.py +0 -0
  27. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/callbacks.py +0 -0
  28. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/capability_routing.py +0 -0
  29. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/checkpoints.py +0 -0
  30. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/cli.py +0 -0
  31. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/codebase_awareness.py +0 -0
  32. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/compaction.py +0 -0
  33. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/computer_use/__init__.py +0 -0
  34. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/computer_use/browser_computer.py +0 -0
  35. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/config.py +0 -0
  36. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/context_budget.py +0 -0
  37. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/context_warning.py +0 -0
  38. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/credentials.py +0 -0
  39. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/curated_memory.py +0 -0
  40. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/delegation_learning.py +0 -0
  41. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/dynamic_policy.py +0 -0
  42. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/evals/harness.py +0 -0
  43. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/event_bus.py +0 -0
  44. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/fleet_reports.py +0 -0
  45. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/hitl_session.py +0 -0
  46. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/hooks.py +0 -0
  47. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/ide_protocol.py +0 -0
  48. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/ide_stdio.py +0 -0
  49. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/intent_classifier.py +0 -0
  50. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/interactions.py +0 -0
  51. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/invoke.py +0 -0
  52. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/kaira_client.py +0 -0
  53. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/kaira_daemon.py +0 -0
  54. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/kaira_ipc.py +0 -0
  55. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/kaira_job_store.py +0 -0
  56. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/learning.py +0 -0
  57. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/limits.py +0 -0
  58. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/live_audio_engine.py +0 -0
  59. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/logging_config.py +0 -0
  60. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/mcp_loader.py +0 -0
  61. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/memory/__init__.py +0 -0
  62. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/memory/embedding_memory_service.py +0 -0
  63. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/memory/file_memory_service.py +0 -0
  64. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/modality_tools.py +0 -0
  65. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/model_errors.py +0 -0
  66. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/model_routing.py +0 -0
  67. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/multimodal_input.py +0 -0
  68. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/openapi_loader.py +0 -0
  69. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/org.py +0 -0
  70. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/output_styles.py +0 -0
  71. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/paths.py +0 -0
  72. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/permissions.py +0 -0
  73. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/plugins/__init__.py +0 -0
  74. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  75. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  76. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/policy_profile.py +0 -0
  77. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/pricing.py +0 -0
  78. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/prompt_suggestions.py +0 -0
  79. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/query/__init__.py +0 -0
  80. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/query/config.py +0 -0
  81. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/query/deps.py +0 -0
  82. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/query/engine.py +0 -0
  83. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/query/stop_hooks.py +0 -0
  84. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/query/token_budget.py +0 -0
  85. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/query/transitions.py +0 -0
  86. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/query_sanitizer.py +0 -0
  87. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/refine.py +0 -0
  88. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/review_agent.py +0 -0
  89. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/rules.py +0 -0
  90. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/self_healing.py +0 -0
  91. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/session_runtime.py +0 -0
  92. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/session_store.py +0 -0
  93. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/session_summariser.py +0 -0
  94. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/skills.py +0 -0
  95. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/slash_commands.py +0 -0
  96. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/thinking.py +0 -0
  97. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tool_prompt_manifest.py +0 -0
  98. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tool_registry.py +0 -0
  99. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tool_result_store.py +0 -0
  100. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tool_synthesis.py +0 -0
  101. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/automations_tools.py +0 -0
  102. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/browser.py +0 -0
  103. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/compress_memory.py +0 -0
  104. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/curated_memory.py +0 -0
  105. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/edit.py +0 -0
  106. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/filesystem.py +0 -0
  107. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/notebook.py +0 -0
  108. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/notes.py +0 -0
  109. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/org_tools.py +0 -0
  110. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/repo_map.py +0 -0
  111. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/search.py +0 -0
  112. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/shell_gate.py +0 -0
  113. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/skills.py +0 -0
  114. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/subtask.py +0 -0
  115. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/tasks.py +0 -0
  116. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/think.py +0 -0
  117. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/todo.py +0 -0
  118. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/user_choice.py +0 -0
  119. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/veomem_tools.py +0 -0
  120. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/web.py +0 -0
  121. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools/web_search.py +0 -0
  122. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tools_inspector.py +0 -0
  123. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/trust.py +0 -0
  124. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tui/input_handler.py +0 -0
  125. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tui/scrollback.py +0 -0
  126. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tui/spinner.py +0 -0
  127. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tui/welcome_banner.py +0 -0
  128. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/tui/welcome_rich.py +0 -0
  129. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/veomem_bridge.py +0 -0
  130. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/version.py +0 -0
  131. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/vertex.py +0 -0
  132. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/wal.py +0 -0
  133. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/web/__init__.py +0 -0
  134. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/web/sse_adapter.py +0 -0
  135. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/web/terminal_repl.py +0 -0
  136. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/web/web_sse_compat.py +0 -0
  137. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode/workspace_hints.py +0 -0
  138. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode.egg-info/SOURCES.txt +0 -0
  139. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode.egg-info/dependency_links.txt +0 -0
  140. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode.egg-info/entry_points.txt +0 -0
  141. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode.egg-info/requires.txt +0 -0
  142. {gemcode-0.4.16 → gemcode-0.4.18}/src/gemcode.egg-info/top_level.txt +0 -0
  143. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_add_dir.py +0 -0
  144. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_agent_habits.py +0 -0
  145. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_agent_instruction.py +0 -0
  146. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_autocompact.py +0 -0
  147. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_automations.py +0 -0
  148. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_capability_routing.py +0 -0
  149. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_checkpoint_diff_command.py +0 -0
  150. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_cli_init.py +0 -0
  151. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_compress_memory_tool.py +0 -0
  152. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_computer_use_permissions.py +0 -0
  153. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_context_budget.py +0 -0
  154. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_context_warning.py +0 -0
  155. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_credentials.py +0 -0
  156. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_eval_harness_layout.py +0 -0
  157. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_event_bus.py +0 -0
  158. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_fleet_reports.py +0 -0
  159. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_ide_stdio_attachments.py +0 -0
  160. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_interactive_permission_ask.py +0 -0
  161. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_kaira_ipc_paths.py +0 -0
  162. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_kaira_scheduler.py +0 -0
  163. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_modality_tools.py +0 -0
  164. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_model_error_retry.py +0 -0
  165. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_model_errors.py +0 -0
  166. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_model_routing.py +0 -0
  167. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_multimodal_input.py +0 -0
  168. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_output_styles_and_rules.py +0 -0
  169. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_paths.py +0 -0
  170. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_permissions.py +0 -0
  171. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_prompt_suggestions.py +0 -0
  172. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_repl_commands.py +0 -0
  173. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_repl_slash.py +0 -0
  174. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_session_runtime_cache.py +0 -0
  175. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_skills.py +0 -0
  176. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_slash_commands.py +0 -0
  177. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_slash_completion_registry.py +0 -0
  178. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_thinking_config.py +0 -0
  179. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_token_budget.py +0 -0
  180. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_tool_context_circulation.py +0 -0
  181. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_tools_inspector.py +0 -0
  182. {gemcode-0.4.16 → gemcode-0.4.18}/tests/test_web_sse_adapter.py +0 -0
  183. {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.16
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.4.16"
7
+ version = "0.4.18"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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` and can be managed via tools
14
- or the REPL. Each habit specifies:
15
- - Which agent runs it (org member)
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 configured agent habits (scheduled recurring tasks)."""
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 bash(
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 run_command(
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.16
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
- def test_run_command_allowlist_bypass_after_shell_gate(tmp_path: Path, monkeypatch) -> None:
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
- def test_run_command_sticky_session_bypasses_allowlist(tmp_path: Path, monkeypatch) -> None:
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
- def test_run_command_cwd_subdir(tmp_path: Path, monkeypatch) -> None:
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
- def test_run_command_background_returns_pid(tmp_path: Path, monkeypatch) -> None:
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
- def test_run_command_extra_env_merges(tmp_path: Path, monkeypatch) -> None:
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