webbee 0.6.7__tar.gz → 0.6.9__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 (149) hide show
  1. {webbee-0.6.7 → webbee-0.6.9}/CHANGELOG.md +14 -0
  2. {webbee-0.6.7 → webbee-0.6.9}/PKG-INFO +2 -2
  3. {webbee-0.6.7 → webbee-0.6.9}/pyproject.toml +2 -2
  4. webbee-0.6.9/src/webbee/__init__.py +1 -0
  5. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/repl.py +19 -0
  6. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/session.py +18 -0
  7. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/tools.py +42 -0
  8. webbee-0.6.9/tests/test_mode_and_remote_0_6_8.py +107 -0
  9. webbee-0.6.7/src/webbee/__init__.py +0 -1
  10. {webbee-0.6.7 → webbee-0.6.9}/.github/workflows/publish.yml +0 -0
  11. {webbee-0.6.7 → webbee-0.6.9}/.gitignore +0 -0
  12. {webbee-0.6.7 → webbee-0.6.9}/LICENSE +0 -0
  13. {webbee-0.6.7 → webbee-0.6.9}/README.md +0 -0
  14. {webbee-0.6.7 → webbee-0.6.9}/install.sh +0 -0
  15. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/__main__.py +0 -0
  16. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/_launch.py +0 -0
  17. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/account.py +0 -0
  18. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/active_sessions.py +0 -0
  19. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/autoupdate.py +0 -0
  20. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/banner_art.py +0 -0
  21. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/boot.py +0 -0
  22. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/browser.py +0 -0
  23. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/checkpoints.py +0 -0
  24. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/cli.py +0 -0
  25. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/clipboard.py +0 -0
  26. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/clipboard_read.py +0 -0
  27. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/clipboard_session.py +0 -0
  28. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/coding_context.py +0 -0
  29. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/commands.py +0 -0
  30. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/config.py +0 -0
  31. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/consent.py +0 -0
  32. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/details.py +0 -0
  33. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/events.py +0 -0
  34. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/frames.py +0 -0
  35. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/home.py +0 -0
  36. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/home_view.py +0 -0
  37. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/http.py +0 -0
  38. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/instance_lock.py +0 -0
  39. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/intel/__init__.py +0 -0
  40. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/intel/graph.py +0 -0
  41. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/intel/outline.py +0 -0
  42. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/intel/query.py +0 -0
  43. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/intel/service.py +0 -0
  44. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/intel/store.py +0 -0
  45. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/intel/vectors.py +0 -0
  46. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/intel/watch.py +0 -0
  47. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/media.py +0 -0
  48. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/modals.py +0 -0
  49. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/mode_store.py +0 -0
  50. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/native_files.py +0 -0
  51. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/native_read.py +0 -0
  52. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/newtab_mode.py +0 -0
  53. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/output_pane.py +0 -0
  54. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/pricing_constants.py +0 -0
  55. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/queue_panel.py +0 -0
  56. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/reflow.py +0 -0
  57. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/remote.py +0 -0
  58. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/render.py +0 -0
  59. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/repo.py +0 -0
  60. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/selection.py +0 -0
  61. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/sessions.py +0 -0
  62. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/sizing.py +0 -0
  63. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/slots.py +0 -0
  64. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/steer.py +0 -0
  65. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/stream.py +0 -0
  66. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/tab_store.py +0 -0
  67. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/tabs.py +0 -0
  68. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/thread.py +0 -0
  69. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/tier_store.py +0 -0
  70. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/todo_panel.py +0 -0
  71. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/tokens.py +0 -0
  72. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/tui.py +0 -0
  73. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/update.py +0 -0
  74. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/urlopen.py +0 -0
  75. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/wallet.py +0 -0
  76. {webbee-0.6.7 → webbee-0.6.9}/src/webbee/worktrees.py +0 -0
  77. {webbee-0.6.7 → webbee-0.6.9}/tests/__init__.py +0 -0
  78. {webbee-0.6.7 → webbee-0.6.9}/tests/conftest.py +0 -0
  79. {webbee-0.6.7 → webbee-0.6.9}/tests/test_account.py +0 -0
  80. {webbee-0.6.7 → webbee-0.6.9}/tests/test_active_sessions.py +0 -0
  81. {webbee-0.6.7 → webbee-0.6.9}/tests/test_audit_2026_07_25_findings.py +0 -0
  82. {webbee-0.6.7 → webbee-0.6.9}/tests/test_autoupdate.py +0 -0
  83. {webbee-0.6.7 → webbee-0.6.9}/tests/test_checkpoints.py +0 -0
  84. {webbee-0.6.7 → webbee-0.6.9}/tests/test_cli.py +0 -0
  85. {webbee-0.6.7 → webbee-0.6.9}/tests/test_clipboard.py +0 -0
  86. {webbee-0.6.7 → webbee-0.6.9}/tests/test_clipboard_read.py +0 -0
  87. {webbee-0.6.7 → webbee-0.6.9}/tests/test_coding_context.py +0 -0
  88. {webbee-0.6.7 → webbee-0.6.9}/tests/test_commands.py +0 -0
  89. {webbee-0.6.7 → webbee-0.6.9}/tests/test_config.py +0 -0
  90. {webbee-0.6.7 → webbee-0.6.9}/tests/test_cpc_contract_stable.py +0 -0
  91. {webbee-0.6.7 → webbee-0.6.9}/tests/test_cpc_outline.py +0 -0
  92. {webbee-0.6.7 → webbee-0.6.9}/tests/test_cross_loop_token.py +0 -0
  93. {webbee-0.6.7 → webbee-0.6.9}/tests/test_details.py +0 -0
  94. {webbee-0.6.7 → webbee-0.6.9}/tests/test_events.py +0 -0
  95. {webbee-0.6.7 → webbee-0.6.9}/tests/test_freeze_fix.py +0 -0
  96. {webbee-0.6.7 → webbee-0.6.9}/tests/test_home.py +0 -0
  97. {webbee-0.6.7 → webbee-0.6.9}/tests/test_home_view.py +0 -0
  98. {webbee-0.6.7 → webbee-0.6.9}/tests/test_home_view_mouse.py +0 -0
  99. {webbee-0.6.7 → webbee-0.6.9}/tests/test_inspect_page.py +0 -0
  100. {webbee-0.6.7 → webbee-0.6.9}/tests/test_instance_lock.py +0 -0
  101. {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_chunker.py +0 -0
  102. {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_embed.py +0 -0
  103. {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_graph.py +0 -0
  104. {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_no_numpy.py +0 -0
  105. {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_query.py +0 -0
  106. {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_service.py +0 -0
  107. {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_store.py +0 -0
  108. {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_vectors.py +0 -0
  109. {webbee-0.6.7 → webbee-0.6.9}/tests/test_intel_watch.py +0 -0
  110. {webbee-0.6.7 → webbee-0.6.9}/tests/test_launch.py +0 -0
  111. {webbee-0.6.7 → webbee-0.6.9}/tests/test_marathon.py +0 -0
  112. {webbee-0.6.7 → webbee-0.6.9}/tests/test_modals_e2e.py +0 -0
  113. {webbee-0.6.7 → webbee-0.6.9}/tests/test_mode_store.py +0 -0
  114. {webbee-0.6.7 → webbee-0.6.9}/tests/test_native_files.py +0 -0
  115. {webbee-0.6.7 → webbee-0.6.9}/tests/test_newtab_mode.py +0 -0
  116. {webbee-0.6.7 → webbee-0.6.9}/tests/test_packaging.py +0 -0
  117. {webbee-0.6.7 → webbee-0.6.9}/tests/test_probe_cost.py +0 -0
  118. {webbee-0.6.7 → webbee-0.6.9}/tests/test_queue_manage_0337.py +0 -0
  119. {webbee-0.6.7 → webbee-0.6.9}/tests/test_reflow.py +0 -0
  120. {webbee-0.6.7 → webbee-0.6.9}/tests/test_render.py +0 -0
  121. {webbee-0.6.7 → webbee-0.6.9}/tests/test_repl.py +0 -0
  122. {webbee-0.6.7 → webbee-0.6.9}/tests/test_repo.py +0 -0
  123. {webbee-0.6.7 → webbee-0.6.9}/tests/test_resilient_http.py +0 -0
  124. {webbee-0.6.7 → webbee-0.6.9}/tests/test_session.py +0 -0
  125. {webbee-0.6.7 → webbee-0.6.9}/tests/test_sessions.py +0 -0
  126. {webbee-0.6.7 → webbee-0.6.9}/tests/test_sizing.py +0 -0
  127. {webbee-0.6.7 → webbee-0.6.9}/tests/test_slots.py +0 -0
  128. {webbee-0.6.7 → webbee-0.6.9}/tests/test_steer.py +0 -0
  129. {webbee-0.6.7 → webbee-0.6.9}/tests/test_stream.py +0 -0
  130. {webbee-0.6.7 → webbee-0.6.9}/tests/test_tab_bar_stability.py +0 -0
  131. {webbee-0.6.7 → webbee-0.6.9}/tests/test_tab_restore_0337.py +0 -0
  132. {webbee-0.6.7 → webbee-0.6.9}/tests/test_tabs.py +0 -0
  133. {webbee-0.6.7 → webbee-0.6.9}/tests/test_terminal_ux_0336.py +0 -0
  134. {webbee-0.6.7 → webbee-0.6.9}/tests/test_thread.py +0 -0
  135. {webbee-0.6.7 → webbee-0.6.9}/tests/test_tier_invalidation.py +0 -0
  136. {webbee-0.6.7 → webbee-0.6.9}/tests/test_tier_store.py +0 -0
  137. {webbee-0.6.7 → webbee-0.6.9}/tests/test_todo_panel.py +0 -0
  138. {webbee-0.6.7 → webbee-0.6.9}/tests/test_tokens.py +0 -0
  139. {webbee-0.6.7 → webbee-0.6.9}/tests/test_tools.py +0 -0
  140. {webbee-0.6.7 → webbee-0.6.9}/tests/test_tools_vision_websearch.py +0 -0
  141. {webbee-0.6.7 → webbee-0.6.9}/tests/test_tui.py +0 -0
  142. {webbee-0.6.7 → webbee-0.6.9}/tests/test_tui_hardening.py +0 -0
  143. {webbee-0.6.7 → webbee-0.6.9}/tests/test_update.py +0 -0
  144. {webbee-0.6.7 → webbee-0.6.9}/tests/test_update_badge_0340.py +0 -0
  145. {webbee-0.6.7 → webbee-0.6.9}/tests/test_urlopen.py +0 -0
  146. {webbee-0.6.7 → webbee-0.6.9}/tests/test_version.py +0 -0
  147. {webbee-0.6.7 → webbee-0.6.9}/tests/test_version_badge_corner.py +0 -0
  148. {webbee-0.6.7 → webbee-0.6.9}/tests/test_wallet.py +0 -0
  149. {webbee-0.6.7 → webbee-0.6.9}/tests/test_worktrees.py +0 -0
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.9
4
+
5
+ * imperal-mcp 0.8.5 at least: a server's repo that cannot be answered says why; parallel code questions never mix
6
+ their freshness; a change's second-depth users are real ones.
7
+
8
+ ## 0.6.8
9
+
10
+ * **Autopilot asks nothing**: the coding mode rides every turn, and a switch (Shift-Tab, /mode, a remote flip) reaches
11
+ the running session at once -- in autopilot the kernel sends no consent request at all, so no turn waits for one.
12
+ A production release still runs only on your own words.
13
+ * **A server's repo is synced only where it is**: a folder that does not exist, or is in no repository, is refused at
14
+ once (a wrong path walked `/home` for half an hour), and the repo's HEAD is read in a few small reads.
15
+ * imperal-mcp 0.8.4 at least.
16
+
3
17
  ## 0.6.7
4
18
 
5
19
  * **The OS's file-watch limit never crashes webbee**: on a large tree (Linux `fs.inotify.max_user_watches`, a repo with
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: webbee
3
- Version: 0.6.7
3
+ Version: 0.6.9
4
4
  Summary: Webbee 🐝 — the Imperal Cloud coding agent in your terminal
5
5
  Project-URL: Homepage, https://imperal.io
6
6
  Project-URL: Documentation, https://docs.imperal.io
@@ -31,7 +31,7 @@ Requires-Python: >=3.7
31
31
  Requires-Dist: asyncssh>=2.14; python_version >= '3.10'
32
32
  Requires-Dist: httpx-sse>=0.4; python_version >= '3.10'
33
33
  Requires-Dist: httpx>=0.27; python_version >= '3.10'
34
- Requires-Dist: imperal-mcp>=0.8.3; python_version >= '3.10'
34
+ Requires-Dist: imperal-mcp>=0.8.5; python_version >= '3.10'
35
35
  Requires-Dist: imperal-sdk>=6.3.6; python_version >= '3.10'
36
36
  Requires-Dist: model2vec>=0.8.2; python_version >= '3.10' and (sys_platform != 'darwin' or platform_machine != 'x86_64' or python_version < '3.13')
37
37
  Requires-Dist: numpy>=2; python_version >= '3.10' and (sys_platform != 'darwin' or platform_machine != 'x86_64' or python_version < '3.13')
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "webbee"
3
- version = "0.6.7"
3
+ version = "0.6.9"
4
4
  description = "Webbee 🐝 — the Imperal Cloud coding agent in your terminal"
5
5
  readme = "README.md"
6
6
  license = "GPL-3.0-or-later"
@@ -13,7 +13,7 @@ dependencies = [
13
13
  # release on a Python 3.12 it sets up once with uv -- `uv` comes as a wheel on 3.8+,
14
14
  # 3.7 fetches it with uv's own installer.
15
15
  'uv>=0.4; python_version >= "3.8" and python_version < "3.10"',
16
- 'imperal-mcp>=0.8.3; python_version >= "3.10"', # raised to the newest at each release
16
+ 'imperal-mcp>=0.8.5; python_version >= "3.10"', # raised to the newest at each release
17
17
  'imperal-sdk>=6.3.6; python_version >= "3.10"',
18
18
  'asyncssh>=2.14; python_version >= "3.10"',
19
19
  'httpx>=0.27; python_version >= "3.10"',
@@ -0,0 +1 @@
1
+ __version__ = "0.6.9"
@@ -265,10 +265,29 @@ def set_slot_mode(slot: SessionSlot, mode: str) -> None:
265
265
  slot.mode = mode
266
266
  if slot.agent is not None:
267
267
  slot.agent.mode = mode
268
+ _notify_mode(slot.agent, mode)
268
269
  from webbee.mode_store import save_mode
269
270
  save_mode(slot.workspace, mode)
270
271
 
271
272
 
273
+ _MODE_POSTS: set = set() # in-flight mode notices (a task kept alive until it is done)
274
+
275
+
276
+ def _notify_mode(agent, mode: str) -> None:
277
+ """The running session hears the switch at once (``AgentSession.post_mode``): fire and forget on the running
278
+ loop; outside a loop (tests, boot) nothing is sent -- the next turn's coding_context carries the mode."""
279
+ post = getattr(agent, "post_mode", None)
280
+ if post is None or not getattr(agent, "session_id", ""):
281
+ return
282
+ try:
283
+ loop = asyncio.get_running_loop()
284
+ except RuntimeError:
285
+ return
286
+ task = loop.create_task(post(mode))
287
+ _MODE_POSTS.add(task)
288
+ task.add_done_callback(_MODE_POSTS.discard)
289
+
290
+
272
291
  def set_slot_tier(slot: SessionSlot, tier: str) -> None:
273
292
  """The ONE place a slot's model tier is ever assigned (webbee-code-
274
293
  model-tier-slash-command-v1) -- mutates `slot.model_tier` + the live
@@ -176,6 +176,21 @@ class AgentSession:
176
176
  token = await self.token_provider()
177
177
  return {"Authorization": f"Bearer {token}"}
178
178
 
179
+ async def post_mode(self, mode: str) -> bool:
180
+ """Tell the running session its new coding mode (the gateway signals the kernel's ``set_mode``): autopilot
181
+ is asked nothing from now on, the others are asked again. False when there is no session yet or no live one
182
+ took it -- the next turn carries the mode anyway. Never raises."""
183
+ if not self.session_id or self.cfg is None or self.token_provider is None:
184
+ return False
185
+ try:
186
+ headers = await self._headers()
187
+ async with make_client(self.cfg, timeout=15) as client:
188
+ resp = await client.post(f"/v1/agent/sessions/{self.session_id}/mode", json={"mode": mode},
189
+ headers=headers)
190
+ return resp.status_code == 200 and bool((resp.json() or {}).get("ok"))
191
+ except Exception:
192
+ return False
193
+
179
194
  async def run(self, task: str, sink, *, marathon: bool = False, goal: str = "",
180
195
  surface: str = "", steer_iid: str = "") -> str:
181
196
  import httpx
@@ -199,6 +214,9 @@ class AgentSession:
199
214
  coding_context = await asyncio.to_thread(build_coding_context, self.workspace_root, self._intel)
200
215
  if self.model_tier:
201
216
  coding_context = {**coding_context, "model_tier": self.model_tier}
217
+ # the mode this turn runs under: in autopilot the kernel asks no consent at all (no card, no park); a switch
218
+ # mid-turn reaches the running session through post_mode
219
+ coding_context = {**coding_context, "mode": str(self.mode or "default")}
202
220
  if marathon:
203
221
  # verify_cmd is CLIENT-detected here and carried in coding_context —
204
222
  # the trusted proof-of-done the kernel runs (never brain-authored).
@@ -1141,6 +1141,44 @@ async def _open_sftp(host: str):
1141
1141
  return await conn.start_sftp_client(), conn, fps, ""
1142
1142
 
1143
1143
 
1144
+ async def _no_repo_at(sftp, ci, root: str) -> str:
1145
+ """Why ``root`` on the server is no repository to sync ("" when it is one): it must exist and be (inside) a
1146
+ repository -- a ``.git`` or a manifest at it or above."""
1147
+ try:
1148
+ await sftp.readdir(root)
1149
+ except Exception as e:
1150
+ return f"{root} does not exist or is not a readable directory ({type(e).__name__})"
1151
+ repo_root = await ci.sftp_repo_root(sftp, root)
1152
+ try:
1153
+ names = {e.filename for e in await sftp.readdir(repo_root)}
1154
+ except Exception:
1155
+ names = set()
1156
+ if not names & (set(getattr(ci, "_REPO_MARKERS", (".git",))) | set(getattr(ci, "_MANIFESTS", ()))):
1157
+ return f"{root} is in no repository (no .git or project manifest at it or above)"
1158
+ return ""
1159
+
1160
+
1161
+ async def remote_head(host: str, root: str) -> dict:
1162
+ """The commit the server's repo at ``root`` is on -- a few small SFTP reads (``.git/HEAD`` and its ref), never a
1163
+ listing: ``{"commit", "branch", "root", "why"}``; ``why`` says what failed (an unknown host, a missing folder)."""
1164
+ from imperal_sdk import code_index as ci
1165
+ sftp, conn, _fps, why = await _open_sftp(host)
1166
+ if sftp is None:
1167
+ return {"commit": "", "branch": "", "root": root, "why": f"{host}: {why}"}
1168
+ try:
1169
+ missing = await _no_repo_at(sftp, ci, root)
1170
+ if missing:
1171
+ return {"commit": "", "branch": "", "root": root, "why": f"{host}: {missing}"}
1172
+ repo_root = await ci.sftp_repo_root(sftp, root)
1173
+ ident = ci.git_identity((await ci.sftp_read_meta(sftp, repo_root))["files"])
1174
+ return {"commit": ident.get("commit") or "", "branch": ident.get("branch") or "", "root": repo_root, "why": ""}
1175
+ except Exception as e:
1176
+ return {"commit": "", "branch": "", "root": root, "why": f"{host}: {type(e).__name__}: {e}"}
1177
+ finally:
1178
+ if conn is not None:
1179
+ conn.close()
1180
+
1181
+
1144
1182
  async def remote_sync(client, host: str, root: str, paths: list) -> dict:
1145
1183
  """List and read the repo holding ``root`` on ``host`` over SFTP (the sdk's one walk: .gitignore,
1146
1184
  secrets and dependency trees skipped), extract and embed here, and sync it into the vault through
@@ -1155,6 +1193,10 @@ async def remote_sync(client, host: str, root: str, paths: list) -> dict:
1155
1193
  if sftp is None:
1156
1194
  return {"repo_key": "", "partial_reason": why, "content": f"{host}: {why}"}
1157
1195
  try:
1196
+ missing = await _no_repo_at(sftp, ci, root)
1197
+ if missing:
1198
+ # never a walk of the folder above (live 2026-10-25: a root that did not exist synced /home for 30 min)
1199
+ return {"repo_key": "", "partial_reason": missing, "content": f"{host}: {missing}"}
1158
1200
  repo_root = await ci.sftp_repo_root(sftp, root)
1159
1201
  try: # the repo's identity is its real path: a symlinked dir names the release it points into
1160
1202
  real_root = str(await sftp.realpath(repo_root) or repo_root) if hasattr(sftp, "realpath") else repo_root
@@ -0,0 +1,107 @@
1
+ """0.6.8: the kernel hears the coding mode (autopilot asks nothing -- owner 2026-10-25: "remove the cards, they stall
2
+ her"); a server's repo is synced only where it is (a missing root walked /home for 30 min) and its HEAD is cheap."""
3
+ import asyncio
4
+ from types import SimpleNamespace
5
+
6
+ import webbee.tools as wt
7
+
8
+
9
+ class _Entry:
10
+ def __init__(self, name):
11
+ self.filename = name
12
+
13
+
14
+ class _Sftp:
15
+ def __init__(self, tree, files=None):
16
+ self.tree, self.files = tree, files or {}
17
+
18
+ async def readdir(self, path):
19
+ if path not in self.tree:
20
+ raise FileNotFoundError(path)
21
+ return [_Entry(n) for n in self.tree[path]]
22
+
23
+
24
+ def test_a_missing_root_is_refused_before_any_walk(monkeypatch):
25
+ from imperal_sdk import code_index as ci
26
+ sftp = _Sftp({"/home": ["ipw-repo"], "/": ["home"]})
27
+ why = asyncio.run(wt._no_repo_at(sftp, ci, "/home/no-such-repo"))
28
+ assert "does not exist" in why
29
+
30
+
31
+ def test_a_folder_in_no_repository_is_refused(monkeypatch):
32
+ from imperal_sdk import code_index as ci
33
+ sftp = _Sftp({"/srv/photos": ["a.jpg"], "/srv": ["photos"], "/": ["srv"]})
34
+ assert "in no repository" in asyncio.run(wt._no_repo_at(sftp, ci, "/srv/photos"))
35
+ repo = _Sftp({"/r": [".git", "a.py"], "/": ["r"]})
36
+ assert asyncio.run(wt._no_repo_at(repo, ci, "/r")) == ""
37
+
38
+
39
+ def test_remote_sync_of_a_missing_root_says_why_and_lists_nothing(monkeypatch):
40
+ sftp = _Sftp({"/": ["home"], "/home": []})
41
+
42
+ async def opened(host):
43
+ return sftp, None, [], ""
44
+ monkeypatch.setattr(wt, "_open_sftp", opened)
45
+ out = asyncio.run(wt.remote_sync(None, "h", "/home/nope", []))
46
+ assert out["repo_key"] == "" and "does not exist" in out["partial_reason"]
47
+
48
+
49
+ def test_remote_head_reads_the_commit(monkeypatch):
50
+ from imperal_sdk import code_index as ci
51
+ sftp = _Sftp({"/r": [".git"], "/": ["r"]})
52
+
53
+ async def opened(host):
54
+ return sftp, None, [], ""
55
+
56
+ async def meta(s, root):
57
+ return {"files": {".git/HEAD": b"ref: refs/heads/master\n", ".git/refs/heads/master": b"fd751713" + b"0" * 32}}
58
+ monkeypatch.setattr(wt, "_open_sftp", opened)
59
+ monkeypatch.setattr(ci, "sftp_read_meta", meta)
60
+ got = asyncio.run(wt.remote_head("h", "/r"))
61
+ assert got["commit"].startswith("fd751713") and got["branch"] == "master" and got["why"] == ""
62
+
63
+ async def unreachable(host):
64
+ return None, None, [], "OSError: Name or service not known"
65
+ monkeypatch.setattr(wt, "_open_sftp", unreachable)
66
+ assert "Name or service not known" in asyncio.run(wt.remote_head("nope", "/r"))["why"]
67
+
68
+
69
+ def test_a_mode_switch_reaches_the_running_session(monkeypatch):
70
+ import webbee.repl as R
71
+ sent = []
72
+
73
+ class _Agent:
74
+ session_id = "imp_u_a-r1"
75
+ mode = "default"
76
+
77
+ async def post_mode(self, mode):
78
+ sent.append(mode)
79
+ return True
80
+ slot = SimpleNamespace(mode="default", agent=_Agent(), workspace="/w")
81
+ monkeypatch.setattr("webbee.mode_store.save_mode", lambda ws, m: None)
82
+
83
+ async def go():
84
+ R.set_slot_mode(slot, "autopilot")
85
+ await asyncio.sleep(0)
86
+ await asyncio.sleep(0)
87
+ asyncio.run(go())
88
+ assert slot.agent.mode == "autopilot" and sent == ["autopilot"]
89
+
90
+
91
+ def test_no_session_yet_sends_nothing(monkeypatch):
92
+ import webbee.repl as R
93
+
94
+ class _Agent:
95
+ session_id = ""
96
+ mode = "default"
97
+
98
+ async def post_mode(self, mode):
99
+ raise AssertionError("posted without a session")
100
+ slot = SimpleNamespace(mode="default", agent=_Agent(), workspace="/w")
101
+ monkeypatch.setattr("webbee.mode_store.save_mode", lambda ws, m: None)
102
+
103
+ async def go():
104
+ R.set_slot_mode(slot, "plan")
105
+ await asyncio.sleep(0)
106
+ asyncio.run(go())
107
+ assert slot.agent.mode == "plan"
@@ -1 +0,0 @@
1
- __version__ = "0.6.7"
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
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
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
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