gemcode 0.4.13__tar.gz → 0.4.15__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.13/src/gemcode.egg-info → gemcode-0.4.15}/PKG-INFO +2 -2
  2. {gemcode-0.4.13 → gemcode-0.4.15}/README.md +1 -1
  3. {gemcode-0.4.13 → gemcode-0.4.15}/pyproject.toml +1 -1
  4. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/agent_habits.py +5 -3
  5. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/agent_mesh.py +183 -64
  6. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/fleet_reports.py +57 -0
  7. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/repl_commands.py +1 -0
  8. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/repl_slash.py +55 -0
  9. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tui/scrollback.py +79 -0
  10. {gemcode-0.4.13 → gemcode-0.4.15/src/gemcode.egg-info}/PKG-INFO +2 -2
  11. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_agent_mesh.py +86 -35
  12. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_fleet_reports.py +22 -0
  13. {gemcode-0.4.13 → gemcode-0.4.15}/LICENSE +0 -0
  14. {gemcode-0.4.13 → gemcode-0.4.15}/MANIFEST.in +0 -0
  15. {gemcode-0.4.13 → gemcode-0.4.15}/setup.cfg +0 -0
  16. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/__init__.py +0 -0
  17. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/__main__.py +0 -0
  18. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/a2a_bridge.py +0 -0
  19. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/agent.py +0 -0
  20. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/agent_intelligence.py +0 -0
  21. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/agent_triggers.py +0 -0
  22. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/audit.py +0 -0
  23. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/autocompact.py +0 -0
  24. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/automations.py +0 -0
  25. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/autotune.py +0 -0
  26. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/callbacks.py +0 -0
  27. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/capability_routing.py +0 -0
  28. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/checkpoints.py +0 -0
  29. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/cli.py +0 -0
  30. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/codebase_awareness.py +0 -0
  31. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/compaction.py +0 -0
  32. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/computer_use/__init__.py +0 -0
  33. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/computer_use/browser_computer.py +0 -0
  34. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/config.py +0 -0
  35. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/context_budget.py +0 -0
  36. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/context_warning.py +0 -0
  37. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/credentials.py +0 -0
  38. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/curated_memory.py +0 -0
  39. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/delegation_learning.py +0 -0
  40. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/dynamic_policy.py +0 -0
  41. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/evals/harness.py +0 -0
  42. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/event_bus.py +0 -0
  43. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/hitl_session.py +0 -0
  44. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/hooks.py +0 -0
  45. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/ide_protocol.py +0 -0
  46. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/ide_stdio.py +0 -0
  47. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/intent_classifier.py +0 -0
  48. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/interactions.py +0 -0
  49. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/invoke.py +0 -0
  50. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/kaira_client.py +0 -0
  51. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/kaira_daemon.py +0 -0
  52. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/kaira_ipc.py +0 -0
  53. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/kaira_job_store.py +0 -0
  54. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/learning.py +0 -0
  55. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/limits.py +0 -0
  56. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/live_audio_engine.py +0 -0
  57. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/logging_config.py +0 -0
  58. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/mcp_loader.py +0 -0
  59. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/memory/__init__.py +0 -0
  60. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/memory/embedding_memory_service.py +0 -0
  61. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/memory/file_memory_service.py +0 -0
  62. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/modality_tools.py +0 -0
  63. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/model_errors.py +0 -0
  64. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/model_routing.py +0 -0
  65. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/multimodal_input.py +0 -0
  66. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/openapi_loader.py +0 -0
  67. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/org.py +0 -0
  68. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/output_styles.py +0 -0
  69. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/paths.py +0 -0
  70. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/permissions.py +0 -0
  71. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/plugins/__init__.py +0 -0
  72. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  73. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  74. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/policy_profile.py +0 -0
  75. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/pricing.py +0 -0
  76. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/prompt_suggestions.py +0 -0
  77. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/query/__init__.py +0 -0
  78. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/query/config.py +0 -0
  79. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/query/deps.py +0 -0
  80. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/query/engine.py +0 -0
  81. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/query/stop_hooks.py +0 -0
  82. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/query/token_budget.py +0 -0
  83. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/query/transitions.py +0 -0
  84. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/query_sanitizer.py +0 -0
  85. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/refine.py +0 -0
  86. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/review_agent.py +0 -0
  87. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/rules.py +0 -0
  88. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/self_healing.py +0 -0
  89. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/session_runtime.py +0 -0
  90. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/session_store.py +0 -0
  91. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/session_summariser.py +0 -0
  92. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/skills.py +0 -0
  93. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/slash_commands.py +0 -0
  94. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/thinking.py +0 -0
  95. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tool_prompt_manifest.py +0 -0
  96. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tool_registry.py +0 -0
  97. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tool_result_store.py +0 -0
  98. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tool_synthesis.py +0 -0
  99. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/__init__.py +0 -0
  100. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/automations_tools.py +0 -0
  101. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/bash.py +0 -0
  102. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/browser.py +0 -0
  103. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/compress_memory.py +0 -0
  104. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/curated_memory.py +0 -0
  105. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/edit.py +0 -0
  106. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/filesystem.py +0 -0
  107. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/notebook.py +0 -0
  108. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/notes.py +0 -0
  109. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/org_tools.py +0 -0
  110. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/repo_map.py +0 -0
  111. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/search.py +0 -0
  112. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/shell.py +0 -0
  113. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/shell_gate.py +0 -0
  114. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/skills.py +0 -0
  115. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/subtask.py +0 -0
  116. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/tasks.py +0 -0
  117. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/think.py +0 -0
  118. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/todo.py +0 -0
  119. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/user_choice.py +0 -0
  120. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/veomem_tools.py +0 -0
  121. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/web.py +0 -0
  122. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/web_search.py +0 -0
  123. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools_inspector.py +0 -0
  124. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/trust.py +0 -0
  125. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tui/input_handler.py +0 -0
  126. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tui/spinner.py +0 -0
  127. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tui/welcome_banner.py +0 -0
  128. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tui/welcome_rich.py +0 -0
  129. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/veomem_bridge.py +0 -0
  130. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/version.py +0 -0
  131. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/vertex.py +0 -0
  132. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/wal.py +0 -0
  133. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/web/__init__.py +0 -0
  134. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/web/sse_adapter.py +0 -0
  135. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/web/terminal_repl.py +0 -0
  136. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/web/web_sse_compat.py +0 -0
  137. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/workspace_hints.py +0 -0
  138. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode.egg-info/SOURCES.txt +0 -0
  139. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode.egg-info/dependency_links.txt +0 -0
  140. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode.egg-info/entry_points.txt +0 -0
  141. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode.egg-info/requires.txt +0 -0
  142. {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode.egg-info/top_level.txt +0 -0
  143. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_add_dir.py +0 -0
  144. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_agent_habits.py +0 -0
  145. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_agent_instruction.py +0 -0
  146. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_autocompact.py +0 -0
  147. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_automations.py +0 -0
  148. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_capability_routing.py +0 -0
  149. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_checkpoint_diff_command.py +0 -0
  150. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_cli_init.py +0 -0
  151. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_compress_memory_tool.py +0 -0
  152. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_computer_use_permissions.py +0 -0
  153. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_context_budget.py +0 -0
  154. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_context_warning.py +0 -0
  155. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_credentials.py +0 -0
  156. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_eval_harness_layout.py +0 -0
  157. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_event_bus.py +0 -0
  158. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_ide_stdio_attachments.py +0 -0
  159. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_interactive_permission_ask.py +0 -0
  160. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_kaira_ipc_paths.py +0 -0
  161. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_kaira_scheduler.py +0 -0
  162. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_modality_tools.py +0 -0
  163. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_model_error_retry.py +0 -0
  164. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_model_errors.py +0 -0
  165. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_model_routing.py +0 -0
  166. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_multimodal_input.py +0 -0
  167. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_output_styles_and_rules.py +0 -0
  168. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_paths.py +0 -0
  169. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_permissions.py +0 -0
  170. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_prompt_suggestions.py +0 -0
  171. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_repl_commands.py +0 -0
  172. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_repl_slash.py +0 -0
  173. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_session_runtime_cache.py +0 -0
  174. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_skills.py +0 -0
  175. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_slash_commands.py +0 -0
  176. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_slash_completion_registry.py +0 -0
  177. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_thinking_config.py +0 -0
  178. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_token_budget.py +0 -0
  179. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_tool_context_circulation.py +0 -0
  180. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_tools.py +0 -0
  181. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_tools_inspector.py +0 -0
  182. {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_web_sse_adapter.py +0 -0
  183. {gemcode-0.4.13 → gemcode-0.4.15}/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.13
3
+ Version: 0.4.15
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -398,7 +398,7 @@ The LLM calls `transfer_to_agent(agent_name='verifier')` → ADK routes natively
398
398
 
399
399
  For background work: `org_delegate("kaira", "run tests")` → mesh runs kaira as a full GemCode session → result flows back via fleet reports.
400
400
 
401
- **Mesh / habits reliability:** overlapping jobs for the **same** org member serialize writes to that agent’s durable SQLite session so ADK does not raise “stale session” errors. Mesh workers default to **unattended tool approval** (env **`GEMCODE_MESH_WORKER_UNATTENDED`**, on by default) so background shell / delegation / writes do not block the main TUI on HITL. See [`../docs/configuration.md`](../docs/configuration.md) and [`../docs/orchestration.md`](../docs/orchestration.md).
401
+ **Mesh / habits reliability:** overlapping jobs for the **same** org member serialize writes to that agent’s durable SQLite session so ADK does not raise “stale session” errors. Mesh workers default to **unattended tool approval** (env **`GEMCODE_MESH_WORKER_UNATTENDED`**, on by default) so background shell / delegation / writes do not block the main TUI on HITL. Fleet auto-continue digests the inbox after **assistant** turns; while idle at ❯, use **`/fleet`** / **`/fleet show`** or any message — see **`GEMCODE_FLEET_TUI_NOTIFY`** in [`../docs/configuration.md`](../docs/configuration.md). See [`../docs/orchestration.md`](../docs/orchestration.md).
402
402
 
403
403
  Docs:
404
404
  - [`../docs/orchestration.md`](../docs/orchestration.md)
@@ -205,7 +205,7 @@ The LLM calls `transfer_to_agent(agent_name='verifier')` → ADK routes natively
205
205
 
206
206
  For background work: `org_delegate("kaira", "run tests")` → mesh runs kaira as a full GemCode session → result flows back via fleet reports.
207
207
 
208
- **Mesh / habits reliability:** overlapping jobs for the **same** org member serialize writes to that agent’s durable SQLite session so ADK does not raise “stale session” errors. Mesh workers default to **unattended tool approval** (env **`GEMCODE_MESH_WORKER_UNATTENDED`**, on by default) so background shell / delegation / writes do not block the main TUI on HITL. See [`../docs/configuration.md`](../docs/configuration.md) and [`../docs/orchestration.md`](../docs/orchestration.md).
208
+ **Mesh / habits reliability:** overlapping jobs for the **same** org member serialize writes to that agent’s durable SQLite session so ADK does not raise “stale session” errors. Mesh workers default to **unattended tool approval** (env **`GEMCODE_MESH_WORKER_UNATTENDED`**, on by default) so background shell / delegation / writes do not block the main TUI on HITL. Fleet auto-continue digests the inbox after **assistant** turns; while idle at ❯, use **`/fleet`** / **`/fleet show`** or any message — see **`GEMCODE_FLEET_TUI_NOTIFY`** in [`../docs/configuration.md`](../docs/configuration.md). See [`../docs/orchestration.md`](../docs/orchestration.md).
209
209
 
210
210
  Docs:
211
211
  - [`../docs/orchestration.md`](../docs/orchestration.md)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.4.13"
7
+ version = "0.4.15"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -324,9 +324,11 @@ def make_habits_tools(cfg: GemCodeConfig) -> list:
324
324
  They fire as long as GemCode is open (REPL/TUI session).
325
325
 
326
326
  Results go to the fleet inbox (.gemcode/fleet_reports.jsonl). Fleet auto-continue
327
- (GEMCODE_FLEET_REPORTS_AUTO_CONTINUE, default on) can inject digest turns after each assistant
328
- reply so the main agent summarizes habit output. Set GEMCODE_FLEET_REPORTS_AUTO_CONTINUE=0 to
329
- only drain on your next normal message.
327
+ (GEMCODE_FLEET_REPORTS_AUTO_CONTINUE, default on) runs digest turns **after each assistant
328
+ reply** when the inbox still has entries it does **not** wake the model while the TUI is
329
+ idle at the prompt. While waiting at ❯, use **`/fleet`** (digest) or **`/fleet show`** (peek),
330
+ or send any message; the TUI also prints a throttled hint when mesh jobs finish
331
+ (GEMCODE_FLEET_TUI_NOTIFY).
330
332
 
331
333
  Args:
332
334
  name: Unique name for this habit (e.g., "test-watch", "nightly-audit").
@@ -16,8 +16,10 @@ Slash **`/agent assign`** / **`trigger`** publish `org.assign` over IPC when the
16
16
  from __future__ import annotations
17
17
 
18
18
  import asyncio
19
+ import concurrent.futures
19
20
  import copy
20
21
  import os
22
+ import threading
21
23
  import time
22
24
  import uuid
23
25
  from dataclasses import dataclass, field
@@ -42,6 +44,8 @@ class AgentJob:
42
44
  result: str = ""
43
45
  error: str = ""
44
46
  created_ms: int = field(default_factory=lambda: int(time.time() * 1000))
47
+ # When set, completed with this AgentJob from the mesh thread (cross-thread wait).
48
+ completion_future: concurrent.futures.Future | None = None
45
49
 
46
50
 
47
51
  def _apply_mesh_worker_unattended_policy(cfg: GemCodeConfig) -> None:
@@ -80,6 +84,11 @@ class AgentMesh:
80
84
  self._sem = asyncio.Semaphore(self.max_concurrency)
81
85
  self._running: dict[str, asyncio.Task] = {}
82
86
  self._completed: list[AgentJob] = []
87
+ self._completed_lock = threading.Lock()
88
+ self._enqueue_lock = threading.Lock()
89
+ self._bg_thread_ident: int | None = None
90
+ # >0 while executing a mesh job on the background loop (nested delegate avoids deadlock).
91
+ self._mesh_job_depth: int = 0
83
92
  self._scheduler_task: asyncio.Task | None = None
84
93
  self._stop: asyncio.Event | None = None # Created in background loop
85
94
  self._bg_thread: "threading.Thread | None" = None
@@ -172,6 +181,7 @@ class AgentMesh:
172
181
  """Background thread entry: create a new event loop and run the scheduler."""
173
182
  self._bg_loop = asyncio.new_event_loop()
174
183
  asyncio.set_event_loop(self._bg_loop)
184
+ self._bg_thread_ident = threading.current_thread().ident
175
185
  try:
176
186
  self._bg_loop.run_until_complete(self._bg_main())
177
187
  except Exception:
@@ -190,7 +200,9 @@ class AgentMesh:
190
200
  self._stop = asyncio.Event() # Create in the correct loop
191
201
 
192
202
  # Start all sub-systems in this loop
193
- self._scheduler_task = asyncio.create_task(self._scheduler_loop())
203
+ # Tests can set PYTEST_GEMCODE_MESH_SCHEDULER=0 to inspect the queue without workers consuming it.
204
+ if os.environ.get("PYTEST_GEMCODE_MESH_SCHEDULER", "").strip() != "0":
205
+ self._scheduler_task = asyncio.create_task(self._scheduler_loop())
194
206
 
195
207
  if self._trigger_engine is not None:
196
208
  self._trigger_engine.start()
@@ -217,6 +229,40 @@ class AgentMesh:
217
229
  except Exception:
218
230
  pass
219
231
 
232
+ def _is_on_mesh_thread(self) -> bool:
233
+ ident = threading.current_thread().ident
234
+ return self._bg_thread_ident is not None and ident == self._bg_thread_ident
235
+
236
+ def _wait_bg_loop_ready(self, timeout_s: float = 5.0) -> bool:
237
+ """Wait until the background thread has assigned ``_bg_loop``."""
238
+ deadline = time.time() + timeout_s
239
+ while time.time() < deadline:
240
+ if self._bg_loop is not None:
241
+ return True
242
+ if self._bg_thread is not None and not self._bg_thread.is_alive():
243
+ return False
244
+ time.sleep(0.001)
245
+ return self._bg_loop is not None
246
+
247
+ def wait_for_pending_enqueues(self, timeout: float = 5.0) -> None:
248
+ """
249
+ Block until callbacks scheduled with ``call_soon_threadsafe`` (for enqueues
250
+ from other threads) have run on the mesh loop. Useful in tests.
251
+ """
252
+ if self._bg_loop is None:
253
+ return
254
+ fut: concurrent.futures.Future[None] = concurrent.futures.Future()
255
+
256
+ def _poke() -> None:
257
+ if not fut.done():
258
+ try:
259
+ fut.set_result(None)
260
+ except Exception:
261
+ pass
262
+
263
+ self._bg_loop.call_soon_threadsafe(_poke)
264
+ fut.result(timeout=timeout)
265
+
220
266
  def enqueue(
221
267
  self,
222
268
  *,
@@ -225,10 +271,13 @@ class AgentMesh:
225
271
  session_id: str = "",
226
272
  member_name: str = "",
227
273
  meta: dict[str, Any] | None = None,
274
+ completion_future: concurrent.futures.Future | None = None,
228
275
  ) -> str:
229
- """Enqueue a job and return its job_id. Thread-safe."""
276
+ """Enqueue a job and return its job_id. Safe to call from any thread."""
230
277
  job_id = f"mesh_{uuid.uuid4().hex[:10]}"
231
- self._seq += 1
278
+ with self._enqueue_lock:
279
+ self._seq += 1
280
+ seq = self._seq
232
281
  job = AgentJob(
233
282
  job_id=job_id,
234
283
  prompt=prompt,
@@ -236,22 +285,45 @@ class AgentMesh:
236
285
  session_id=session_id or str(uuid.uuid4()),
237
286
  member_name=member_name,
238
287
  meta=meta or {},
288
+ completion_future=completion_future,
239
289
  )
240
- # Higher priority = runs first (negate for min-heap)
241
- self._queue.put_nowait((-priority, self._seq, job))
290
+ item = (-priority, seq, job)
242
291
 
243
- # Publish queued event
244
- self._bus.publish_sync(BusMessage(
245
- topic="job.queued",
246
- from_addr="mesh",
247
- to_addr="manager",
248
- payload={"job_id": job_id, "member": member_name, "priority": priority},
249
- ))
292
+ def do_put() -> None:
293
+ try:
294
+ self._queue.put_nowait(item)
295
+ except Exception as e:
296
+ fut = job.completion_future
297
+ if fut is not None and not fut.done():
298
+ try:
299
+ fut.set_exception(e)
300
+ except Exception:
301
+ pass
302
+ return
303
+ self._bus.publish_sync(BusMessage(
304
+ topic="job.queued",
305
+ from_addr="mesh",
306
+ to_addr="manager",
307
+ payload={"job_id": job_id, "member": member_name, "priority": priority},
308
+ ))
250
309
 
251
- # Auto-start the background thread if not running
252
310
  if self._bg_thread is None or not self._bg_thread.is_alive():
253
311
  self.start()
254
312
 
313
+ if not self._wait_bg_loop_ready():
314
+ fut = job.completion_future
315
+ if fut is not None and not fut.done():
316
+ try:
317
+ fut.set_exception(RuntimeError("mesh background loop failed to start"))
318
+ except Exception:
319
+ pass
320
+ return job_id
321
+
322
+ if self._is_on_mesh_thread():
323
+ do_put()
324
+ else:
325
+ self._bg_loop.call_soon_threadsafe(do_put)
326
+
255
327
  return job_id
256
328
 
257
329
  async def delegate_to_member(
@@ -303,39 +375,60 @@ class AgentMesh:
303
375
  if context:
304
376
  full_prompt += "\n\nContext:\n" + context
305
377
 
378
+ org_meta = {
379
+ "org": {
380
+ "member": m.to_dict() if hasattr(m, "to_dict") else {},
381
+ "task": task,
382
+ "context": context,
383
+ }
384
+ }
385
+
386
+ if not wait:
387
+ job_id = self.enqueue(
388
+ prompt=full_prompt,
389
+ priority=priority,
390
+ session_id="",
391
+ member_name=m.name,
392
+ meta=org_meta,
393
+ )
394
+ return {"ok": True, "job_id": job_id, "delegated_to": m.name, "async": True}
395
+
396
+ # Nested wait=True from inside a running mesh job would deadlock the scheduler
397
+ # (parent holds a concurrency slot while the child waits for another slot).
398
+ if self._mesh_job_depth > 0:
399
+ job_id = f"mesh_{uuid.uuid4().hex[:10]}"
400
+ with self._enqueue_lock:
401
+ self._seq += 1
402
+ inline_job = AgentJob(
403
+ job_id=job_id,
404
+ prompt=full_prompt,
405
+ priority=priority,
406
+ session_id="",
407
+ member_name=m.name,
408
+ meta=org_meta,
409
+ )
410
+ await self._run_job_inner(inline_job)
411
+ if inline_job.status == "finished":
412
+ return {"ok": True, "job_id": job_id, "result": inline_job.result}
413
+ return {"ok": False, "job_id": job_id, "error": inline_job.error}
414
+
415
+ loop = asyncio.get_running_loop()
416
+ fut: concurrent.futures.Future[AgentJob] = concurrent.futures.Future()
306
417
  job_id = self.enqueue(
307
418
  prompt=full_prompt,
308
419
  priority=priority,
309
420
  session_id="",
310
421
  member_name=m.name,
311
- meta={
312
- "org": {
313
- "member": m.to_dict() if hasattr(m, "to_dict") else {},
314
- "task": task,
315
- "context": context,
316
- }
317
- },
422
+ meta=org_meta,
423
+ completion_future=fut,
318
424
  )
319
-
320
- if not wait:
321
- return {"ok": True, "job_id": job_id, "delegated_to": m.name, "async": True}
322
-
323
- # Wait for completion
324
- result = await self._wait_for_job(job_id, timeout=300.0)
325
- return result
326
-
327
- async def _wait_for_job(self, job_id: str, timeout: float = 300.0) -> dict[str, Any]:
328
- """Wait for a specific job to complete."""
329
- deadline = time.time() + timeout
330
- while time.time() < deadline:
331
- for job in self._completed:
332
- if job.job_id == job_id:
333
- if job.status == "finished":
334
- return {"ok": True, "job_id": job_id, "result": job.result}
335
- else:
336
- return {"ok": False, "job_id": job_id, "error": job.error}
337
- await asyncio.sleep(0.1)
338
- return {"ok": False, "job_id": job_id, "error": "timeout"}
425
+ try:
426
+ job = await asyncio.wait_for(asyncio.wrap_future(fut, loop=loop), timeout=300.0)
427
+ except asyncio.TimeoutError:
428
+ return {"ok": False, "job_id": job_id, "error": "timeout"}
429
+ if job.status == "finished":
430
+ return {"ok": True, "job_id": job_id, "result": job.result}
431
+ return {"ok": False, "job_id": job_id, "error": job.error}
339
432
 
340
433
  async def _handle_org_assign(self, msg: BusMessage) -> None:
341
434
  """Handle org.assign bus messages (A2A-style delegation)."""
@@ -373,6 +466,14 @@ class AgentMesh:
373
466
 
374
467
  async def _run_job(self, job: AgentJob) -> None:
375
468
  """Execute a single job using a fresh ADK Runner."""
469
+ self._mesh_job_depth += 1
470
+ try:
471
+ await self._run_job_inner(job)
472
+ finally:
473
+ self._mesh_job_depth -= 1
474
+
475
+ async def _run_job_inner(self, job: AgentJob) -> None:
476
+ """Full job lifecycle (shared by the scheduler and nested inline delegation)."""
376
477
  job.status = "running"
377
478
  start_ms = int(time.time() * 1000)
378
479
 
@@ -391,18 +492,22 @@ class AgentMesh:
391
492
  duration_ms = int(time.time() * 1000) - start_ms
392
493
 
393
494
  # Publish completion
495
+ _jr_finished: dict[str, Any] = {
496
+ "job_id": job.job_id,
497
+ "session_id": job.session_id,
498
+ "status": "finished",
499
+ "member": job.member_name,
500
+ "report": result_text[:8000],
501
+ "duration_ms": duration_ms,
502
+ }
503
+ _hm0 = job.meta.get("habit") if isinstance(job.meta, dict) else None
504
+ if isinstance(_hm0, dict):
505
+ _jr_finished["habit"] = _hm0
394
506
  await self._bus.publish(BusMessage(
395
507
  topic="job.report",
396
508
  from_addr=job.member_name or "mesh",
397
509
  to_addr="manager",
398
- payload={
399
- "job_id": job.job_id,
400
- "session_id": job.session_id,
401
- "status": "finished",
402
- "member": job.member_name,
403
- "report": result_text[:8000],
404
- "duration_ms": duration_ms,
405
- },
510
+ payload=_jr_finished,
406
511
  ))
407
512
 
408
513
  # Also publish org.report if this was an org delegation
@@ -459,17 +564,21 @@ class AgentMesh:
459
564
  job.status = "failed"
460
565
  job.error = f"{type(e).__name__}: {e}"
461
566
 
567
+ _jr_failed: dict[str, Any] = {
568
+ "job_id": job.job_id,
569
+ "session_id": job.session_id,
570
+ "status": "failed",
571
+ "member": job.member_name,
572
+ "error": job.error,
573
+ }
574
+ _hm1 = job.meta.get("habit") if isinstance(job.meta, dict) else None
575
+ if isinstance(_hm1, dict):
576
+ _jr_failed["habit"] = _hm1
462
577
  await self._bus.publish(BusMessage(
463
578
  topic="job.report",
464
579
  from_addr=job.member_name or "mesh",
465
580
  to_addr="manager",
466
- payload={
467
- "job_id": job.job_id,
468
- "session_id": job.session_id,
469
- "status": "failed",
470
- "member": job.member_name,
471
- "error": job.error,
472
- },
581
+ payload=_jr_failed,
473
582
  ))
474
583
 
475
584
  # Persist failure to fleet reports
@@ -491,10 +600,17 @@ class AgentMesh:
491
600
  pass
492
601
 
493
602
  finally:
494
- self._completed.append(job)
495
- # Keep completed list bounded
496
- if len(self._completed) > 200:
497
- self._completed = self._completed[-100:]
603
+ cf = job.completion_future
604
+ if cf is not None and not cf.done():
605
+ try:
606
+ cf.set_result(job)
607
+ except Exception:
608
+ pass
609
+ with self._completed_lock:
610
+ self._completed.append(job)
611
+ # Keep completed list bounded
612
+ if len(self._completed) > 200:
613
+ self._completed = self._completed[-100:]
498
614
 
499
615
  async def _execute_agent_turn(self, job: AgentJob) -> str:
500
616
  """
@@ -732,15 +848,18 @@ class AgentMesh:
732
848
 
733
849
  def status(self) -> dict[str, Any]:
734
850
  """Get mesh status for debugging/display."""
851
+ with self._completed_lock:
852
+ recent = [
853
+ {"job_id": j.job_id, "member": j.member_name, "status": j.status}
854
+ for j in self._completed[-10:]
855
+ ]
856
+ n_completed = len(self._completed)
735
857
  return {
736
858
  "running_jobs": len(self._running),
737
859
  "queued_jobs": self._queue.qsize(),
738
- "completed_jobs": len(self._completed),
860
+ "completed_jobs": n_completed,
739
861
  "max_concurrency": self.max_concurrency,
740
- "recent_completed": [
741
- {"job_id": j.job_id, "member": j.member_name, "status": j.status}
742
- for j in self._completed[-10:]
743
- ],
862
+ "recent_completed": recent,
744
863
  }
745
864
 
746
865
 
@@ -366,3 +366,60 @@ def drain_for_prompt(project_root: Path, *, max_chars: int | None = None) -> str
366
366
  "increase GEMCODE_FLEET_REPORTS_MAX_CHARS to drain more per turn)"
367
367
  )
368
368
  return header + body
369
+
370
+
371
+ def preview_fleet_inbox(project_root: Path, *, max_chars: int | None = None) -> str:
372
+ """
373
+ Format pending fleet inbox lines without draining the file (for ``/fleet show``).
374
+ """
375
+ if max_chars is None:
376
+ try:
377
+ max_chars = int(os.environ.get("GEMCODE_FLEET_REPORTS_MAX_CHARS", "14000"))
378
+ except Exception:
379
+ max_chars = 14_000
380
+ try:
381
+ from gemcode.org import resolve_fleet_root
382
+
383
+ fleet_root = resolve_fleet_root(project_root)
384
+ except Exception:
385
+ fleet_root = project_root
386
+ p = _fleet_reports_path(fleet_root)
387
+ if not p.is_file():
388
+ return "(no `.gemcode/fleet_reports.jsonl` yet — background jobs append here when they finish)"
389
+ try:
390
+ raw = p.read_text(encoding="utf-8", errors="replace")
391
+ except Exception:
392
+ return "(could not read fleet_reports.jsonl)"
393
+ if not raw.strip():
394
+ return "(fleet inbox is empty)"
395
+
396
+ lines_in = [ln.strip() for ln in raw.splitlines() if ln.strip()]
397
+ blocks: list[str] = []
398
+ total = 0
399
+ truncated = False
400
+ for line in lines_in:
401
+ try:
402
+ rec = json.loads(line)
403
+ except Exception:
404
+ continue
405
+ if not isinstance(rec, dict):
406
+ continue
407
+ b = _format_record(rec)
408
+ if not b:
409
+ continue
410
+ need = len(b) + 2
411
+ if total + need > max_chars:
412
+ truncated = True
413
+ break
414
+ blocks.append(b)
415
+ total += need
416
+
417
+ if not blocks:
418
+ return "(fleet inbox has no readable entries)"
419
+ header = "Fleet / agent reports (preview — inbox not cleared):\n\n"
420
+ body = "\n\n".join(blocks)
421
+ if truncated:
422
+ body += (
423
+ "\n\n… (truncated; increase GEMCODE_FLEET_REPORTS_MAX_CHARS or run /fleet show after /fleet digest)"
424
+ )
425
+ return header + body
@@ -236,6 +236,7 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
236
236
  ("runtime", "Fleet socket status · gemcode runtime · attach/connect"),
237
237
  ("bus", "Runtime bus — send/publish lightweight messages over IPC"),
238
238
  ("inbox", "Bus inbox filters for this UI (to/topics)"),
239
+ ("fleet", "Fleet inbox — /fleet show | digest (habits / mesh reports)"),
239
240
  ("agent", "Create/manage a child agent workspace (folder + registry)"),
240
241
  # NOTE: /org and /delegate are deprecated aliases; keep working but do not list.
241
242
  ("limits", "Execution limits (calls, context, …)"),
@@ -91,6 +91,61 @@ async def process_repl_slash(
91
91
  out()
92
92
  return ReplSlashResult(skip_model_turn=True)
93
93
 
94
+ # ── /fleet (fleet_reports.jsonl — habits / mesh without waiting for a model turn) ─
95
+ if name in ("fleet", "fleet_reports", "reports"):
96
+ from gemcode.fleet_reports import (
97
+ fleet_digest_prompt,
98
+ has_pending_fleet_reports,
99
+ inject_enabled,
100
+ preview_fleet_inbox,
101
+ )
102
+
103
+ try:
104
+ object.__setattr__(cfg, "_fleet_auto_chain", 0)
105
+ except Exception:
106
+ pass
107
+
108
+ sub = (sc.args or "").strip()
109
+ sub_l = sub.lower()
110
+ first = sub_l.split()[0] if sub_l else ""
111
+
112
+ if sub_l in ("help", "?") or (not sub_l and not has_pending_fleet_reports(cfg.project_root)):
113
+ out("Fleet inbox (`.gemcode/fleet_reports.jsonl` at fleet root):")
114
+ out(" /fleet If reports are pending: run a digest turn (drains inbox into the model).")
115
+ out(" /fleet digest Same, when you want to force digest.")
116
+ out(" /fleet show Print pending lines without draining (peek only).")
117
+ out("While the TUI is idle at ❯, auto-continue only runs after an assistant reply;")
118
+ out("mesh/habit completions still land in the inbox — use /fleet or any message to drain.")
119
+ if not inject_enabled():
120
+ out(" (GEMCODE_FLEET_REPORTS_INJECT=0 — inbox is not written.)")
121
+ out()
122
+ return ReplSlashResult(skip_model_turn=True)
123
+
124
+ if sub_l in ("show", "peek", "cat") or first in ("show", "peek", "cat"):
125
+ out(preview_fleet_inbox(cfg.project_root))
126
+ out()
127
+ return ReplSlashResult(skip_model_turn=True)
128
+
129
+ if sub_l in ("digest", "summarize", "sum") or first in ("digest", "summarize", "sum"):
130
+ if not has_pending_fleet_reports(cfg.project_root):
131
+ out("[fleet] no pending reports in inbox.")
132
+ out()
133
+ return ReplSlashResult(skip_model_turn=True)
134
+ return ReplSlashResult(model_prompt=fleet_digest_prompt())
135
+
136
+ if not sub_l:
137
+ if has_pending_fleet_reports(cfg.project_root):
138
+ return ReplSlashResult(model_prompt=fleet_digest_prompt())
139
+ out("[fleet] no pending reports. Try `/fleet help`.")
140
+ out()
141
+ return ReplSlashResult(skip_model_turn=True)
142
+
143
+ if not has_pending_fleet_reports(cfg.project_root):
144
+ out("[fleet] no pending reports in inbox.")
145
+ out()
146
+ return ReplSlashResult(skip_model_turn=True)
147
+ return ReplSlashResult(model_prompt=fleet_digest_prompt())
148
+
94
149
  # ── /attach (queue files for the next user message: PDF, images, audio, …) ─
95
150
  if name in ("attach", "file", "image", "img"):
96
151
  raw_i = (sc.args or "").strip()
@@ -4,6 +4,7 @@ import asyncio
4
4
  import json
5
5
  import os
6
6
  import sys
7
+ import time
7
8
  from dataclasses import dataclass
8
9
 
9
10
  from google.adk.agents.run_config import RunConfig
@@ -278,6 +279,84 @@ async def run_gemcode_scrollback_tui(
278
279
  get_cfg=lambda: cfg,
279
280
  )
280
281
 
282
+ # When mesh/habit jobs finish, fleet_reports.jsonl updates while we are idle at ❯.
283
+ # Auto-continue only runs after an *assistant* turn, so print a throttled hint.
284
+ _tui_loop = asyncio.get_running_loop()
285
+ _last_fleet_notify_s = [0.0]
286
+
287
+ def _on_mesh_job_report_notify(msg: object) -> None:
288
+ if os.environ.get("GEMCODE_FLEET_TUI_NOTIFY", "1").strip().lower() in (
289
+ "0", "false", "no", "off",
290
+ ):
291
+ return
292
+ try:
293
+ from gemcode.event_bus import BusMessage
294
+
295
+ if not isinstance(msg, BusMessage) or msg.topic != "job.report":
296
+ return
297
+ pl = msg.payload if isinstance(msg.payload, dict) else {}
298
+ if str(pl.get("status") or "").strip().lower() not in ("finished", "failed"):
299
+ return
300
+ except Exception:
301
+ return
302
+ try:
303
+ min_gap = float(os.environ.get("GEMCODE_FLEET_TUI_NOTIFY_MIN_S", "8") or "8")
304
+ except Exception:
305
+ min_gap = 8.0
306
+ now = time.time()
307
+ if now - _last_fleet_notify_s[0] < min_gap:
308
+ return
309
+ _last_fleet_notify_s[0] = now
310
+
311
+ def _print_hint() -> None:
312
+ try:
313
+ mem = ""
314
+ if isinstance(msg.payload, dict):
315
+ mem = str(msg.payload.get("member") or "").strip()
316
+ habit = ""
317
+ if isinstance(msg.payload, dict):
318
+ hm = msg.payload.get("habit")
319
+ if isinstance(hm, dict):
320
+ habit = str(hm.get("name") or "").strip()
321
+ bits = []
322
+ if mem:
323
+ bits.append(mem)
324
+ if habit:
325
+ bits.append(f"habit:{habit}")
326
+ extra = f" ({' · '.join(bits)})" if bits else ""
327
+ line = (
328
+ f"{ansi.dim}[gemcode] Background job finished{extra} — "
329
+ f"/fleet to summarize inbox (any message also drains).{ansi.reset}"
330
+ )
331
+ if input_handler.is_interactive():
332
+ try:
333
+ from prompt_toolkit.patch_stdout import patch_stdout
334
+
335
+ with patch_stdout():
336
+ print(line, flush=True)
337
+ except Exception:
338
+ print(line, flush=True)
339
+ else:
340
+ print(line, flush=True)
341
+ except Exception:
342
+ pass
343
+
344
+ try:
345
+ _tui_loop.call_soon_threadsafe(_print_hint)
346
+ except Exception:
347
+ pass
348
+
349
+ try:
350
+ from gemcode.event_bus import get_bus
351
+
352
+ get_bus().subscribe(
353
+ topic="job.report",
354
+ to_addr="manager",
355
+ callback=_on_mesh_job_report_notify,
356
+ )
357
+ except Exception:
358
+ pass
359
+
281
360
  # ── Optional: embed Kaira daemon in this TUI ─────────────────────────────
282
361
  # This enables continuous background automation without requiring a second
283
362
  # terminal. The TUI will also auto-subscribe to IPC events (below) so job
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.4.13
3
+ Version: 0.4.15
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -398,7 +398,7 @@ The LLM calls `transfer_to_agent(agent_name='verifier')` → ADK routes natively
398
398
 
399
399
  For background work: `org_delegate("kaira", "run tests")` → mesh runs kaira as a full GemCode session → result flows back via fleet reports.
400
400
 
401
- **Mesh / habits reliability:** overlapping jobs for the **same** org member serialize writes to that agent’s durable SQLite session so ADK does not raise “stale session” errors. Mesh workers default to **unattended tool approval** (env **`GEMCODE_MESH_WORKER_UNATTENDED`**, on by default) so background shell / delegation / writes do not block the main TUI on HITL. See [`../docs/configuration.md`](../docs/configuration.md) and [`../docs/orchestration.md`](../docs/orchestration.md).
401
+ **Mesh / habits reliability:** overlapping jobs for the **same** org member serialize writes to that agent’s durable SQLite session so ADK does not raise “stale session” errors. Mesh workers default to **unattended tool approval** (env **`GEMCODE_MESH_WORKER_UNATTENDED`**, on by default) so background shell / delegation / writes do not block the main TUI on HITL. Fleet auto-continue digests the inbox after **assistant** turns; while idle at ❯, use **`/fleet`** / **`/fleet show`** or any message — see **`GEMCODE_FLEET_TUI_NOTIFY`** in [`../docs/configuration.md`](../docs/configuration.md). See [`../docs/orchestration.md`](../docs/orchestration.md).
402
402
 
403
403
  Docs:
404
404
  - [`../docs/orchestration.md`](../docs/orchestration.md)