meshcode 2.11.132__tar.gz → 2.11.134__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 (102) hide show
  1. {meshcode-2.11.132 → meshcode-2.11.134}/PKG-INFO +1 -1
  2. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/backend.py +16 -0
  4. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/server.py +69 -1
  5. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/protocol_handler.py +414 -20
  6. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/rpc_allowlist.py +1 -0
  7. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode.egg-info/PKG-INFO +1 -1
  8. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode.egg-info/SOURCES.txt +1 -0
  9. {meshcode-2.11.132 → meshcode-2.11.134}/pyproject.toml +1 -1
  10. meshcode-2.11.134/tests/test_task_progress.py +147 -0
  11. {meshcode-2.11.132 → meshcode-2.11.134}/README.md +0 -0
  12. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/__main__.py +0 -0
  13. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/_session_handoff_template.py +0 -0
  14. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/_stop_hook_template.py +0 -0
  15. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/ascii_art.py +0 -0
  16. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/atomic_push.py +0 -0
  17. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/claude_update.py +0 -0
  18. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/cli.py +0 -0
  19. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/comms_v4.py +0 -0
  20. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/compat.py +0 -0
  21. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/daemon.py +0 -0
  22. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/date_parse.py +0 -0
  23. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/doctor.py +0 -0
  24. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/error_hints.py +0 -0
  25. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/exceptions.py +0 -0
  26. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/hooks/__init__.py +0 -0
  27. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/hooks/repo_path_lock.py +0 -0
  28. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/hostd.py +0 -0
  29. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/invites.py +0 -0
  30. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/launcher.py +0 -0
  31. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/launcher_install.py +0 -0
  32. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/__init__.py +0 -0
  33. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/__main__.py +0 -0
  34. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/realtime.py +0 -0
  35. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  36. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/swarm.py +0 -0
  37. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/test_backend.py +0 -0
  38. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  39. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  40. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  41. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  42. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  43. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/test_swarm.py +0 -0
  44. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/preferences.py +0 -0
  45. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/protocol_v2.py +0 -0
  46. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/quickstart.py +0 -0
  47. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/run_agent.py +0 -0
  48. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/scripts/check_secrets.py +0 -0
  49. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/scripts/race_rate_harness.py +0 -0
  50. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/secrets.py +0 -0
  51. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/self_update.py +0 -0
  52. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/setup_clients.py +0 -0
  53. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/supervisor.py +0 -0
  54. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/up.py +0 -0
  55. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/upload.py +0 -0
  56. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode.egg-info/dependency_links.txt +0 -0
  57. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode.egg-info/entry_points.txt +0 -0
  58. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode.egg-info/requires.txt +0 -0
  59. {meshcode-2.11.132 → meshcode-2.11.134}/meshcode.egg-info/top_level.txt +0 -0
  60. {meshcode-2.11.132 → meshcode-2.11.134}/setup.cfg +0 -0
  61. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_auto_update_hardening.py +0 -0
  62. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_autonomous_closegap_1.py +0 -0
  63. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_autonomous_closegap_2.py +0 -0
  64. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_autonomous_closegap_3.py +0 -0
  65. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_autonomous_prompt_inject.py +0 -0
  66. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_boot_bug_regression.py +0 -0
  67. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_color_truecolor.py +0 -0
  68. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_core.py +0 -0
  69. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_cross_agent_messaging.py +0 -0
  70. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_date_parse.py +0 -0
  71. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_doctor.py +0 -0
  72. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_epistemic_v1_python_sdk.py +0 -0
  73. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  74. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_esc_deaf_state.py +0 -0
  75. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_exceptions.py +0 -0
  76. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_file_upload.py +0 -0
  77. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_hostd_zombie_sessions.py +0 -0
  78. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_init_device_code.py +0 -0
  79. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_install_guard.py +0 -0
  80. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_lease_sigterm_release.py +0 -0
  81. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_live_mesh_guard.py +0 -0
  82. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_mark_read_batch.py +0 -0
  83. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_marketplace_ratings.py +0 -0
  84. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_migration_integrity.py +0 -0
  85. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_realtime_event_freshness.py +0 -0
  86. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_rls_cross_tenant.py +0 -0
  87. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_rpc_grants.py +0 -0
  88. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_rpc_migrations.py +0 -0
  89. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_run_agent_dry_run.py +0 -0
  90. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_run_agent_no_server_import.py +0 -0
  91. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_security_regressions.py +0 -0
  92. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_self_update_user_site.py +0 -0
  93. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_sentinel.py +0 -0
  94. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_session_replay_gate.py +0 -0
  95. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_setup_path.py +0 -0
  96. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_sleep_signals.py +0 -0
  97. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_status_enum_coverage.py +0 -0
  98. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_stay_on_loop_hook.py +0 -0
  99. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_stop_ghost_terminal.py +0 -0
  100. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_swarm_events.py +0 -0
  101. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_terminal_lifecycle.py +0 -0
  102. {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_wait_open_tasks_contradiction.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.132
3
+ Version: 2.11.134
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,5 +1,5 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.11.132"
2
+ __version__ = "2.11.134"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -1389,6 +1389,22 @@ def task_complete(api_key, project_id, task_id, completing_agent, summary=""):
1389
1389
  })
1390
1390
 
1391
1391
 
1392
+ def task_progress(api_key, project_id, task_id, pct, note=None):
1393
+ """Report real progress pct (0-100) on a claimed/in_progress/in_review task.
1394
+
1395
+ Server-side: public.mc_task_progress (mig 516) — claim-holder or owner;
1396
+ updates progress_pct/note/last_progress_at, extends the claim lease 4h,
1397
+ appends to deliverables.progress_log. The dashboard pbar reads this as
1398
+ progress_source='reported' (task 6b2afa5d: pbar accuracy)."""
1399
+ return sb_rpc("mc_task_progress", {
1400
+ "p_api_key": api_key,
1401
+ "p_project_id": project_id,
1402
+ "p_task_id": task_id,
1403
+ "p_pct": pct,
1404
+ "p_note": note,
1405
+ })
1406
+
1407
+
1392
1408
  def feature_flag_enabled(api_key, flag_name):
1393
1409
  """Agent-facing feature-flag check (mig478). The FE's mc_check_feature_flag
1394
1410
  is auth.uid()-only, so agents use this api-key variant. Returns the raw
@@ -5348,6 +5348,64 @@ def meshcode_tasks(status_filter: Optional[str] = None, verbose: bool = False) -
5348
5348
  return {"ok": True, "tasks": compact}
5349
5349
 
5350
5350
 
5351
+ def _auto_task_progress(task_id: str, pct: int, note: str) -> None:
5352
+ """Best-effort lifecycle progress floor (task 6b2afa5d: pbar accuracy).
5353
+
5354
+ Writes pct via mc_task_progress (mig 516) ONLY if the task's current
5355
+ reported pct is unknown or lower — a re-claimed task that was already at
5356
+ 60% must never get reset to the claim/start milestone. Swallows every
5357
+ failure (RPC missing pre-mig-516, auth, state): the lifecycle action this
5358
+ rides on must never be blocked by progress telemetry.
5359
+ """
5360
+ try:
5361
+ api_key = _get_api_key()
5362
+ if pct < 100: # 100 is monotonic by definition — skip the read
5363
+ listing = be.task_list(api_key, _PROJECT_ID, AGENT_NAME,
5364
+ status_filter=None, include_done=False)
5365
+ if isinstance(listing, dict) and listing.get("ok"):
5366
+ for t in listing.get("tasks", []):
5367
+ if t.get("id") == task_id:
5368
+ current = t.get("progress_pct")
5369
+ if isinstance(current, int) and current >= pct:
5370
+ return # real progress already reported — keep it
5371
+ break
5372
+ be.task_progress(api_key, _PROJECT_ID, task_id, pct, note)
5373
+ except Exception:
5374
+ pass # auto-progress is a fallback, never a dependency
5375
+
5376
+
5377
+ @mcp.tool()
5378
+ @with_working_status
5379
+ def meshcode_task_progress(task_id: str, pct: int, note: str = "") -> Dict[str, Any]:
5380
+ """Report REAL progress (0-100) on your claimed/in_progress task — feeds the dashboard pbar.
5381
+
5382
+ Samuel directive (250e131e / msg d88bb9c7): every in_progress task must
5383
+ report advance at real milestones, not vibes. Call at meaningful points
5384
+ (e.g. 10 on claim, 25-40 design done, 70 code+tests, 90 smoke green).
5385
+ Server-side mc_task_progress (mig 516) also extends your claim lease 4h
5386
+ and appends {ts, by, pct, note} to deliverables.progress_log.
5387
+
5388
+ Args:
5389
+ task_id: task you hold the claim on (UUID or 8-char prefix NOT
5390
+ accepted — pass the full UUID).
5391
+ pct: integer 0-100. Monotonicity is NOT enforced — you can correct
5392
+ an overestimate downward.
5393
+ note: short human-readable milestone (shows in the panel tooltip).
5394
+ """
5395
+ if not isinstance(pct, int) or pct < 0 or pct > 100:
5396
+ return {"ok": False, "error": "pct must be an integer 0-100",
5397
+ "error_code": "invalid_arg"}
5398
+ api_key = _get_api_key()
5399
+ result = be.task_progress(api_key, _PROJECT_ID, task_id, pct, note or None)
5400
+ if isinstance(result, dict) and result.get("error"):
5401
+ err = result.get("error")
5402
+ msg = err.get("message", "") if isinstance(err, dict) else str(err)
5403
+ if "Could not find the function" in msg or "PGRST202" in msg:
5404
+ return {"ok": False, "error_code": "rpc_missing",
5405
+ "error": "mc_task_progress RPC not deployed (mig 516 not applied) — progress not recorded, work unaffected"}
5406
+ return result
5407
+
5408
+
5351
5409
  @mcp.tool()
5352
5410
  @with_working_status
5353
5411
  def meshcode_task_claim(task_id: str) -> Dict[str, Any]:
@@ -5368,6 +5426,8 @@ def meshcode_task_claim(task_id: str) -> Dict[str, Any]:
5368
5426
  api_key=api_key)
5369
5427
  except Exception:
5370
5428
  pass # status surfacing is best-effort
5429
+ # pbar accuracy fallback (task 6b2afa5d): claim = at least 10%
5430
+ _auto_task_progress(resp.get("task_id") or task_id, 10, "auto: claimed")
5371
5431
  return resp
5372
5432
 
5373
5433
 
@@ -5399,6 +5459,10 @@ def meshcode_task_complete(task_id: str, summary: str = "", force: bool = False)
5399
5459
  }
5400
5460
  except Exception:
5401
5461
  pass # Best-effort check; don't block on listing failure.
5462
+ # pbar accuracy (task 6b2afa5d): stamp 100 BEFORE complete — mc_task_progress
5463
+ # only accepts claimed/in_progress/in_review, so post-complete is too late,
5464
+ # and a done task otherwise freezes at its last reported pct on the panel.
5465
+ _auto_task_progress(task_id, 100, "auto: completed")
5402
5466
  result = be.task_complete(api_key, _PROJECT_ID, task_id, AGENT_NAME, summary=summary)
5403
5467
  # Task data persists in the task system — do NOT duplicate to memory.
5404
5468
  # Samuel: "los tasks no deben guardarse en memoria, para eso salen en tasks"
@@ -5428,12 +5492,16 @@ def meshcode_task_complete(task_id: str, summary: str = "", force: bool = False)
5428
5492
  def meshcode_task_start(task_id: str) -> Dict[str, Any]:
5429
5493
  """Flip a claimed task to in_progress (mig 349 invariant: one in_progress per agent — any sibling in_progress for this agent is demoted to 'claimed' atomically)."""
5430
5494
  api_key = _get_api_key()
5431
- return be.sb_rpc("mc_task_start", {
5495
+ result = be.sb_rpc("mc_task_start", {
5432
5496
  "p_api_key": api_key,
5433
5497
  "p_project_id": _PROJECT_ID,
5434
5498
  "p_task_id": task_id,
5435
5499
  "p_starting_agent": AGENT_NAME,
5436
5500
  })
5501
+ if isinstance(result, dict) and result.get("ok"):
5502
+ # pbar accuracy fallback (task 6b2afa5d): started = at least 25%
5503
+ _auto_task_progress(task_id, 25, "auto: started")
5504
+ return result
5437
5505
 
5438
5506
 
5439
5507
  @mcp.tool()
@@ -175,16 +175,28 @@ def _launcher_label(cmd: str) -> str:
175
175
  # Samuel 22c9f31d: "Launch All de N agentes = 1 ventana con N tabs, NO N
176
176
  # ventanas sueltas")
177
177
  # ============================================================
178
- # macOS Terminal.app has NO reliable programmatic tab API — all four native
179
- # routes were tested live on the target box 2026-06-11 and fail:
178
+ # macOS Terminal.app has NO reliable programmatic tab API — SEVEN native
179
+ # routes were tested live on the target box (macOS 26.4.1, 2026-06-11) and
180
+ # ALL fail (task 2ac3f111 first four; task 4df5a126 the last three):
180
181
  # - AppleScript `do script ... in window id N` reuses the selected tab
181
182
  # (never creates one)
182
183
  # - AppleScript `make new tab` errors -10000 (unsupported by Terminal)
183
184
  # - `defaults write com.apple.Terminal AppleWindowTabbingMode always` is
184
185
  # ignored (Terminal's tab model predates NSWindow tabbing)
186
+ # - `defaults write -g AppleWindowTabbingMode always` (global domain) is
187
+ # ALSO ignored for `open -a Terminal x.command` — still N windows
185
188
  # - System Events Cmd+T opened a NEW WINDOW even with Accessibility
186
189
  # granted, the target window raised AND frontmost (flaky beyond repair;
187
190
  # also needs Accessibility + steals focus = two extra failure modes)
191
+ # - System Events click of menu item Shell > New Tab > <profile> (the
192
+ # deterministic cousin of Cmd+T) ALSO creates a NEW WINDOW, with the
193
+ # target window verified frontmost at click time
194
+ # - AppleScript bare `do script` (new-window form) with tabbing=always
195
+ # set in either domain still creates separate windows
196
+ # (Window > Merge All Windows would coerce tabs but swallows the user's
197
+ # PERSONAL Terminal windows — prohibited.) Native tabs on Terminal.app can
198
+ # only be created by a human gesture; if true native tabs are required, the
199
+ # supported path is a terminal with a real tab API (e.g. iTerm2).
188
200
  # So the fleet window is a tmux session inside ONE Terminal window: each
189
201
  # agent = a named tmux window = a clickable tab in the status bar. This buys
190
202
  # exactly the spec, with zero Apple Events from the daemon (the TCC
@@ -202,6 +214,43 @@ def _launcher_label(cmd: str) -> str:
202
214
  _FLEET_SESSION = "meshcode-fleet"
203
215
 
204
216
 
217
+ def _fleet_tab_name(cmd: str) -> str:
218
+ """Short per-agent tab name for the fleet bar (task 4df5a126).
219
+
220
+ hostd targets are `<project>/<agent>` so _launcher_label yields
221
+ `meshcode-self-improve_backend` — at 3+ agents those long names overflow
222
+ the status bar and Samuel sees ONE truncated tab ("la barra verde
223
+ truncada"). The tab must read like a native Terminal tab: just the agent.
224
+ Take the part after the LAST '/' of the raw `meshcode run` target
225
+ (project names can't contain '/'; launch-batch passes bare agent names,
226
+ which are unchanged). Falls back to the full launcher label.
227
+ """
228
+ m = re.search(r"meshcode\s+run\s+(?:'([^']*)'|\"([^\"]*)\"|(\S+))", cmd)
229
+ raw = ((m.group(1) or m.group(2) or m.group(3)) if m else "") or ""
230
+ agent = raw.rsplit("/", 1)[-1].strip()
231
+ safe = re.sub(r"[^A-Za-z0-9_.-]", "_", agent).strip("_")
232
+ return safe[:24] or _launcher_label(cmd)
233
+
234
+
235
+ # Native-looking tab bar (task 4df5a126: Samuel rejected the default tmux
236
+ # status line — invisible/truncated). Top-positioned (where Terminal tabs
237
+ # live), one padded block per agent, active tab high-contrast, all
238
+ # mouse-clickable (mouse on). Plain ASCII, no emojis (Samuel rule 52b291b9).
239
+ # Applied idempotently on EVERY spawn so an already-running fleet session
240
+ # picks the style up the next time any agent launches.
241
+ _FLEET_STYLE: tuple = (
242
+ ("status-position", "top"),
243
+ ("status-style", "bg=#16161e,fg=#a9b1d6"),
244
+ ("status-left", "#[bg=#3d59a1,fg=#ffffff,bold] MESHCODE #[default] "),
245
+ ("status-left-length", "14"),
246
+ ("status-right", "#[fg=#565f89] #{session_windows} agents | click: switch | right-click: stop "),
247
+ ("status-right-length", "48"),
248
+ ("window-status-format", "#[bg=#24283b,fg=#a9b1d6] #W #[default]"),
249
+ ("window-status-current-format", "#[bg=#7aa2f7,fg=#16161e,bold] #W #[default]"),
250
+ ("window-status-separator", " "),
251
+ )
252
+
253
+
205
254
  def _find_tmux() -> Optional[str]:
206
255
  """tmux binary or None. The launchd daemon's PATH is minimal, so probe the
207
256
  common install prefixes + the daemon venv bin explicitly."""
@@ -227,11 +276,23 @@ def _fleet_wrap(cmd: str) -> str:
227
276
  Drop the LAST `exec ` (same trick as the .command launcher) so the shell
228
277
  survives the agent's exit and can translate rc 143 (SIGTERM = hostd
229
278
  stop/ghost-kill, task 91201315: that's the job FINISHING) to 0 — otherwise
230
- `remain-on-exit failed` would keep a dead pane for every plain Stop."""
279
+ `remain-on-exit failed` would keep a dead pane for every plain Stop.
280
+
281
+ CLOSE-TAB=STOP (task 4df5a126, Samuel 68b1c17b: "cierro su terminal y
282
+ ya"): export MESHCODE_CLOSE_STOP_SIGHUP=1 so the MCP server's gated POSIX
283
+ close=stop handler (b6da0d54, default OFF) arms INSIDE fleet tabs only.
284
+ In a tmux pane SIGHUP is unambiguous — only kill-window / kill-session /
285
+ tmux-server death close the pty (a window-close of the shared Terminal
286
+ just DETACHES; ESC never closes the pty) — so the false-stop risk that
287
+ kept the flag off for plain windows (task dfa17461) does not exist here.
288
+ Right-click a tab -> Kill => SIGHUP => agent flips desired_state=stopped
289
+ server-side and exits clean: offline, NO hostd ghost-respawn (mig 494
290
+ coherent). A real crash never runs the handler => crash-respawn intact."""
231
291
  if "exec " in cmd:
232
292
  head, _, tail = cmd.rpartition("exec ")
233
293
  cmd = head + tail
234
- return (f'{cmd}; MC_RC=$?; if [ "$MC_RC" = "143" ]; then exit 0; fi; '
294
+ return (f'export MESHCODE_CLOSE_STOP_SIGHUP=1; '
295
+ f'{cmd}; MC_RC=$?; if [ "$MC_RC" = "143" ]; then exit 0; fi; '
235
296
  f'echo "[meshcode] agent exited rc=$MC_RC — tab kept for debugging"; exit $MC_RC')
236
297
 
237
298
 
@@ -296,7 +357,7 @@ def _spawn_fleet_tab(cmd: str) -> tuple[bool, str]:
296
357
  tmux = _find_tmux()
297
358
  if not tmux:
298
359
  return False, "tmux not installed (brew install tmux -> tabbed fleet window)"
299
- label = _launcher_label(cmd)
360
+ label = _fleet_tab_name(cmd)
300
361
  wrapped = _fleet_wrap(cmd)
301
362
  try:
302
363
  fresh_session = _tmux(tmux, "has-session", "-t", f"={_FLEET_SESSION}").returncode != 0
@@ -317,13 +378,14 @@ def _spawn_fleet_tab(cmd: str) -> tuple[bool, str]:
317
378
  "-n", label, "-P", "-F", "#{window_id}", wrapped)
318
379
  if r.returncode != 0:
319
380
  return False, f"tmux new-window: {(r.stderr or '').strip()}"
320
- else:
321
- # Session bootstrap (once). mouse = click a tab name in the status
322
- # bar to switch; detach-on-destroy = when the last tab closes, the
323
- # attach client exits and the fleet.command closes its own window.
324
- for opt in (("mouse", "on"), ("detach-on-destroy", "on"),
325
- ("status-left", "[MeshCode Fleet] "), ("status-left-length", "20")):
326
- _tmux(tmux, "set-option", "-t", f"={_FLEET_SESSION}:", *opt)
381
+ # Session options, applied on EVERY spawn (idempotent): mouse = click a
382
+ # tab in the bar to switch; detach-on-destroy = when the last tab
383
+ # closes, the attach client exits and the fleet.command closes its own
384
+ # window; _FLEET_STYLE = the native-looking top tab bar. Always-apply
385
+ # (was bootstrap-only) so a session created by an OLDER wheel restyles
386
+ # the moment any agent launches through this code.
387
+ for opt in (("mouse", "on"), ("detach-on-destroy", "on"), *_FLEET_STYLE):
388
+ _tmux(tmux, "set-option", "-t", f"={_FLEET_SESSION}:", *opt)
327
389
  win_id = (r.stdout or "").strip()
328
390
  if win_id.startswith("@"):
329
391
  # Per-tab window options: a CRASHED agent's pane stays (rc!=0 ->
@@ -360,8 +422,11 @@ def _spawn_fleet_tab(cmd: str) -> tuple[bool, str]:
360
422
  ok, info = _fleet_attach_linux(tmux)
361
423
  if not ok:
362
424
  # Tab exists + agent runs, but invisibly — that breaks the
363
- # visibility contract. Kill the tab and let legacy spawn a window.
364
- _tmux(tmux, "kill-window", "-t", f"={_FLEET_SESSION}:{label}")
425
+ # visibility contract. Kill the tab and let legacy spawn a
426
+ # window. Target by @id when we have it (tab names are now
427
+ # short agent names and CAN collide across projects).
428
+ _tmux(tmux, "kill-window", "-t",
429
+ win_id if win_id.startswith("@") else f"={_FLEET_SESSION}:{label}")
365
430
  return False, f"fleet attach window failed: {info}"
366
431
  return True, f"{info}(fleet-window+tab)"
367
432
  return True, "fleet-tab"
@@ -369,6 +434,326 @@ def _spawn_fleet_tab(cmd: str) -> tuple[bool, str]:
369
434
  return False, f"tmux fleet failed: {e}"
370
435
 
371
436
 
437
+ # ============================================================
438
+ # fleet NATIVE Terminal.app tabs — macOS (task 4df5a126, Samuel GO 9a6e0f02)
439
+ # ============================================================
440
+ # Samuel visually CONFIRMED (screenshot 2026-06-11 11:47) that the System
441
+ # Events click of Shell > New Tab > <profile> creates a REAL native tab.
442
+ # Why every scripted probe said "window": macOS native tabs are SEPARATE
443
+ # NSWindows grouped into one frame — Terminal's AppleScript `windows`
444
+ # enumerates each grouped tab as its own window object with `tabs=1`, so
445
+ # AppleScript COUNTS CANNOT distinguish a grouped tab from a loose window.
446
+ # All earlier "FAIL: new window created" verdicts were this measurement
447
+ # blindness, not failures.
448
+ #
449
+ # Architecture (TCC-safe — the back-2 Mac poisoning note in
450
+ # _spawn_terminal_macos forbids Apple Events FROM THE DAEMON):
451
+ # - The FIRST agent launches via `open -a Terminal <agent>.fleet.command`
452
+ # (the blessed no-Apple-Events path) and becomes the ANCHOR: its shell
453
+ # starts the lock-gated fleet WATCHER in the background. The watcher
454
+ # runs INSIDE Terminal, so Terminal stays its own TCC responsible
455
+ # process for both Apple Events and the Accessibility click (verified
456
+ # granted + working live on the target box 2026-06-11).
457
+ # - Later agents are SPOOLED (~/.meshcode/launchers/fleet-spool/): the
458
+ # watcher runs the GREEN RECIPE per spool entry — raise anchor window,
459
+ # READ the New Tab submenu, click profile item 1, find what appeared
460
+ # (same-window tab OR new grouped NSWindow), `do script` the agent
461
+ # into it. hostd blocks on a .done marker; "fallback"/timeout cascades
462
+ # to the tmux fleet bar, then to legacy one-window-per-agent.
463
+ # - EVERY fleet agent shell offers to become watcher (lock-gated, stale
464
+ # lock stolen after 12s) so closing the anchor tab only moves the
465
+ # watcher to another tab within seconds.
466
+ # - close-tab=STOP (Samuel 68b1c17b): tabs export
467
+ # MESHCODE_CLOSE_STOP_SIGHUP=1 — closing a native tab closes its pty,
468
+ # SIGHUP arms the MCP close=stop handler (b6da0d54), agent flips
469
+ # desired_state=stopped and exits clean (no hostd ghost-respawn,
470
+ # mig 494 coherent). Clean exits (rc 0/143) self-close ONLY their tab;
471
+ # crashes keep the tab + scrollback for debugging.
472
+ # Known trade-off: each spooled spawn raises Terminal + the fleet frame
473
+ # (the menu click needs frontmost) — focus moves during a Launch All burst,
474
+ # which is the moment the spec wants focus anyway.
475
+
476
+ _FLEET_NATIVE_DISABLED_TTL_S = 1800 # back off after a watcher "fallback"
477
+
478
+
479
+ def _fleet_native_paths() -> dict:
480
+ d = Path.home() / ".meshcode" / "launchers"
481
+ return {
482
+ "dir": d,
483
+ "spool": d / "fleet-spool",
484
+ "alive": d / "fleet-native-alive",
485
+ "pending": d / "fleet-native-pending",
486
+ "disabled": d / "fleet-native-disabled",
487
+ "watcher": d / "meshcode-fleet-watcher.sh",
488
+ "log": d / "fleet-native.log",
489
+ }
490
+
491
+
492
+ _FLEET_NATIVE_DRIVER_AS = r'''on run argv
493
+ set myTTY to item 1 of argv
494
+ set agentCmd to item 2 of argv
495
+ tell application "Terminal"
496
+ set myWin to missing value
497
+ repeat with w in windows
498
+ repeat with t in tabs of w
499
+ try
500
+ if (tty of t) is myTTY then set myWin to w
501
+ end try
502
+ end repeat
503
+ end repeat
504
+ if myWin is missing value then error "anchor window not found"
505
+ set idsBefore to id of every window
506
+ set tabsBefore to count of tabs of myWin
507
+ activate
508
+ set index of myWin to 1
509
+ try
510
+ set frontmost of myWin to true
511
+ end try
512
+ end tell
513
+ delay 0.8
514
+ tell application "System Events"
515
+ tell process "Terminal"
516
+ set frontmost to true
517
+ delay 0.3
518
+ -- GREEN-RECIPE RC (commander 9a6e0f02, Samuel screenshot 11:47):
519
+ -- READ the New Tab submenu BEFORE clicking. The read forces AppKit to
520
+ -- populate the menu; the click on an unpopulated menu intermittently
521
+ -- degraded to a loose window. DO NOT REMOVE THIS LINE.
522
+ set subItems to name of menu items of menu 1 of menu item "New Tab" of menu "Shell" of menu bar 1
523
+ click menu item 1 of menu 1 of menu item "New Tab" of menu "Shell" of menu bar 1
524
+ end tell
525
+ end tell
526
+ set targetId to 0
527
+ set targetKind to ""
528
+ repeat with i from 1 to 12
529
+ delay 0.5
530
+ tell application "Terminal"
531
+ if (count of tabs of myWin) > tabsBefore then
532
+ set targetKind to "tab"
533
+ else
534
+ repeat with w in windows
535
+ if idsBefore does not contain (id of w) then
536
+ set targetId to id of w
537
+ set targetKind to "win"
538
+ end if
539
+ end repeat
540
+ end if
541
+ end tell
542
+ if targetKind is not "" then exit repeat
543
+ end repeat
544
+ if targetKind is "" then error "no new tab appeared after menu click"
545
+ tell application "Terminal"
546
+ if targetKind is "tab" then
547
+ do script agentCmd in tab (tabsBefore + 1) of myWin
548
+ else
549
+ -- grouped NSWindow tab: `do script in window id` reuses its fresh
550
+ -- selected shell (verified live 2026-06-11) — exactly what we want.
551
+ do script agentCmd in window id targetId
552
+ end if
553
+ end tell
554
+ return "ok"
555
+ end run'''
556
+
557
+
558
+ def _write_fleet_native_watcher() -> Path:
559
+ """Write (idempotently) the lock-gated spool watcher that creates native
560
+ tabs from INSIDE Terminal. See the architecture block above."""
561
+ p = _fleet_native_paths()
562
+ body = '''#!/bin/bash
563
+ # meshcode fleet native-tabs watcher — GENERATED by protocol_handler.py
564
+ # (task 4df5a126). Runs INSIDE a Terminal tab (TCC: Terminal stays its own
565
+ # responsible process). One holder at a time (lock dir, stale-steal 12s).
566
+ LAUNCH="$HOME/.meshcode/launchers"
567
+ SPOOL="$LAUNCH/fleet-spool"
568
+ LOCK="$LAUNCH/fleet-native-lock"
569
+ ALIVE="$LAUNCH/fleet-native-alive"
570
+ MYTTY="$1"
571
+ exec >>"$LAUNCH/fleet-native.log" 2>&1
572
+ mkdir -p "$SPOOL"
573
+ while :; do
574
+ if mkdir "$LOCK" 2>/dev/null; then
575
+ echo "$$ $MYTTY $(date)" > "$LOCK/owner"
576
+ break
577
+ fi
578
+ AGE=999
579
+ if [ -f "$ALIVE" ]; then
580
+ AGE=$(( $(date +%s) - $(stat -f %m "$ALIVE" 2>/dev/null || echo 0) ))
581
+ fi
582
+ if [ "$AGE" -gt 12 ]; then rm -rf "$LOCK"; continue; fi
583
+ sleep 3
584
+ done
585
+ trap 'rm -rf "$LOCK"; exit 0' EXIT HUP TERM INT
586
+ echo "[watcher] $$ holds lock (tty $MYTTY)"
587
+ N=0
588
+ while :; do
589
+ date +%s > "$ALIVE"
590
+ for f in "$SPOOL"/*.cmd; do
591
+ [ -e "$f" ] || continue
592
+ CMD="$(cat "$f")"
593
+ BASE="${f%.cmd}"
594
+ echo "[watcher] tab spawn: $CMD"
595
+ if /usr/bin/osascript "$LAUNCH/fleet-native-driver.applescript" "$MYTTY" "$CMD"
596
+ then
597
+ echo ok > "$BASE.done"
598
+ else
599
+ echo fallback > "$BASE.done"
600
+ fi
601
+ rm -f "$f"
602
+ done
603
+ N=$((N+1))
604
+ if [ $((N % 300)) -eq 0 ]; then
605
+ find "$SPOOL" -name '*.done' -mmin +60 -delete 2>/dev/null
606
+ fi
607
+ sleep 1
608
+ done
609
+ '''
610
+ p["dir"].mkdir(parents=True, exist_ok=True)
611
+ driver = p["dir"] / "fleet-native-driver.applescript"
612
+ driver.write_text(_FLEET_NATIVE_DRIVER_AS + "\n", encoding="utf-8")
613
+ p["watcher"].write_text(body, encoding="utf-8")
614
+ os.chmod(p["watcher"], 0o755)
615
+ return p["watcher"]
616
+
617
+
618
+ def _write_fleet_native_agent(cmd: str) -> Path:
619
+ """Write the per-agent fleet command file (runs the agent inside its
620
+ native tab; offers to become watcher; close-tab=stop; clean exits close
621
+ ONLY their own tab). Filename keyed by _launcher_label (unique per
622
+ project/agent target); the visible TAB TITLE is the short agent name."""
623
+ p = _fleet_native_paths()
624
+ title = _fleet_tab_name(cmd)
625
+ script_path = p["dir"] / f"{_launcher_label(cmd)}.fleet.command"
626
+ try:
627
+ venv_bin = str(Path(sys.executable).parent)
628
+ except Exception:
629
+ venv_bin = ""
630
+ if "exec " in cmd:
631
+ _head, _, _tail = cmd.rpartition("exec ")
632
+ run_line = _head + _tail # agent as CHILD so bash survives to close the tab
633
+ else:
634
+ run_line = cmd
635
+ lines = [
636
+ "#!/bin/bash",
637
+ 'cd "$HOME" 2>/dev/null || cd /',
638
+ rf"printf '\033]0;{title}\007\033]1;{title}\007'",
639
+ ]
640
+ if venv_bin:
641
+ lines.append(f'export PATH={shlex.quote(venv_bin)}:"$PATH"')
642
+ lines += [
643
+ # close-tab=STOP (Samuel 68b1c17b) — see architecture block.
644
+ "export MESHCODE_CLOSE_STOP_SIGHUP=1",
645
+ 'MC_TTY="$(tty 2>/dev/null)"',
646
+ # every fleet tab offers to run the watcher (lock-gated)
647
+ f'/bin/bash {shlex.quote(str(p["watcher"]))} "$MC_TTY" >/dev/null 2>&1 &',
648
+ "disown",
649
+ run_line,
650
+ "MC_RC=$?",
651
+ # clean exit (0, or 143 = hostd stop sweep): close ONLY this tab.
652
+ # Crash: keep tab + scrollback (debugging > clean window).
653
+ 'if { [ "$MC_RC" = "0" ] || [ "$MC_RC" = "143" ]; } && [ -n "$MC_TTY" ]; then',
654
+ " /usr/bin/osascript"
655
+ " -e 'on run argv'"
656
+ " -e 'tell application \"Terminal\"'"
657
+ " -e 'repeat with w in windows'"
658
+ " -e 'repeat with t in tabs of w'"
659
+ " -e 'try'"
660
+ " -e 'if (tty of t) is (item 1 of argv) then close t saving no'"
661
+ " -e 'end try'"
662
+ " -e 'end repeat'"
663
+ " -e 'end repeat'"
664
+ " -e 'end tell'"
665
+ " -e 'end run' \"$MC_TTY\" >/dev/null 2>&1",
666
+ "else",
667
+ ' echo "[meshcode] agent exited rc=$MC_RC — tab kept for debugging"',
668
+ "fi",
669
+ ]
670
+ p["dir"].mkdir(parents=True, exist_ok=True)
671
+ script_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
672
+ os.chmod(script_path, 0o755)
673
+ return script_path
674
+
675
+
676
+ def _spawn_fleet_native_macos(cmd: str) -> tuple[bool, str]:
677
+ """Spawn `cmd` as a NATIVE Terminal.app tab of the shared fleet window.
678
+
679
+ Returns (False, reason) on ANY failure — caller cascades to the tmux
680
+ fleet bar, then legacy windows, so this can never make launches worse."""
681
+ p = _fleet_native_paths()
682
+ # back-off marker: a recent watcher "fallback" (Accessibility revoked,
683
+ # menu shape changed, ...) disables the native path for a while instead
684
+ # of paying a focus-steal + multi-second failure on every launch.
685
+ try:
686
+ if time.time() - p["disabled"].stat().st_mtime < _FLEET_NATIVE_DISABLED_TTL_S:
687
+ return False, "fleet-native recently failed (back-off)"
688
+ except OSError:
689
+ pass
690
+ try:
691
+ p["spool"].mkdir(parents=True, exist_ok=True)
692
+ _write_fleet_native_watcher()
693
+ agent_file = _write_fleet_native_agent(cmd)
694
+ except Exception as e:
695
+ return False, f"could not write fleet-native scripts: {e}"
696
+
697
+ def _alive_age() -> float:
698
+ try:
699
+ return time.time() - p["alive"].stat().st_mtime
700
+ except OSError:
701
+ return 1e9
702
+
703
+ if _alive_age() > 8:
704
+ pend_fresh = False
705
+ try:
706
+ pend_fresh = (time.time() - p["pending"].stat().st_mtime) < 25
707
+ except OSError:
708
+ pass
709
+ if not pend_fresh:
710
+ # become the ANCHOR: first visible tab, watcher host
711
+ try:
712
+ p["pending"].touch()
713
+ except OSError:
714
+ pass
715
+ r = subprocess.run(["open", "-a", "Terminal", str(agent_file)],
716
+ capture_output=True, text=True)
717
+ if r.returncode == 0:
718
+ return True, "terminal(fleet-native-anchor)"
719
+ return False, (r.stderr or "open failed").strip()
720
+ # an anchor is booting — wait for its watcher before spooling
721
+ deadline = time.time() + 25
722
+ while time.time() < deadline and _alive_age() > 8:
723
+ time.sleep(0.5)
724
+ if _alive_age() > 8:
725
+ return False, "fleet-native anchor never came alive"
726
+
727
+ sp = p["spool"] / f"{time.time_ns()}-{_launcher_label(cmd)}.cmd"
728
+ done = sp.with_suffix(".done")
729
+ try:
730
+ sp.write_text(f"/bin/bash {shlex.quote(str(agent_file))}\n", encoding="utf-8")
731
+ except Exception as e:
732
+ return False, f"fleet-native spool write failed: {e}"
733
+ deadline = time.time() + 20
734
+ while time.time() < deadline:
735
+ if done.exists():
736
+ try:
737
+ verdict = done.read_text().strip()
738
+ done.unlink()
739
+ except OSError:
740
+ verdict = ""
741
+ if verdict == "ok":
742
+ return True, "fleet-native-tab"
743
+ try:
744
+ p["disabled"].touch()
745
+ except OSError:
746
+ pass
747
+ return False, f"fleet-native watcher reported {verdict or 'error'}"
748
+ time.sleep(0.4)
749
+ # timeout: pull the spool entry so a late watcher can't double-spawn
750
+ try:
751
+ sp.unlink()
752
+ except OSError:
753
+ pass
754
+ return False, "fleet-native spool timeout"
755
+
756
+
372
757
  def _spawn_terminal_macos(cmd: str) -> tuple[bool, str]:
373
758
  """Spawn `cmd` in a new VISIBLE Terminal/iTerm window (detached).
374
759
 
@@ -595,7 +980,10 @@ def _spawn_terminal_windows(cmd: str) -> tuple[bool, str]:
595
980
  # carries one → the cmdline splits and wt fails with 0x80070002.
596
981
  # Escape ONLY here; the cmd.exe fallback below stays unescaped
597
982
  # (cmd.exe does not split on ';').
598
- label = _launcher_label(cmd)
983
+ # Short agent-only tab title (task 4df5a126) — same readability fix
984
+ # as the macOS fleet bar; sanitized [A-Za-z0-9_.-] so it can't
985
+ # smuggle wt args.
986
+ label = _fleet_tab_name(cmd)
599
987
  subprocess.Popen([wt, "-w", "meshcode-fleet", "nt",
600
988
  "--title", label or "meshcode-agent",
601
989
  "--suppressApplicationTitle",
@@ -612,16 +1000,22 @@ def _spawn_terminal_windows(cmd: str) -> tuple[bool, str]:
612
1000
 
613
1001
  def _spawn_terminal(cmd: str) -> tuple[bool, str]:
614
1002
  p = platform.system()
615
- if p in ("Darwin", "Linux"):
616
- # Fleet tabs first (task 2ac3f111): one shared window, one tab per agent.
617
- # Any failure (tmux absent, session error, attach window failed) falls
618
- # back to the legacy one-window-per-agent spawn below — never worse.
1003
+ if p == "Darwin":
1004
+ # 1) NATIVE Terminal tabs (task 4df5a126, Samuel GO) one window,
1005
+ # one real tab per agent, close-tab=stop.
1006
+ ok, info = _spawn_fleet_native_macos(cmd)
1007
+ if ok:
1008
+ return True, info
1009
+ # 2) tmux fleet bar (task 2ac3f111) — shared window, styled tab bar.
619
1010
  ok, info = _spawn_fleet_tab(cmd)
620
1011
  if ok:
621
1012
  return True, info
622
- if p == "Darwin":
1013
+ # 3) legacy one-window-per-agent — never worse than before.
623
1014
  return _spawn_terminal_macos(cmd)
624
1015
  if p == "Linux":
1016
+ ok, info = _spawn_fleet_tab(cmd)
1017
+ if ok:
1018
+ return True, info
625
1019
  return _spawn_terminal_linux(cmd)
626
1020
  if p == "Windows":
627
1021
  # Windows Terminal tabs are native: _spawn_terminal_windows targets the
@@ -31,6 +31,7 @@ AGENT_CALLABLE_RPCS = [
31
31
  "mc_task_reject",
32
32
  "mc_task_approve",
33
33
  "mc_task_search",
34
+ "mc_task_progress", # mig 516 — pbar accuracy (task 6b2afa5d)
34
35
  "mc_task_reschedule", # mig 330
35
36
  "mc_task_get_deps", # mig 330
36
37
  "mc_tasks_due_soon", # mig 329
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.132
3
+ Version: 2.11.134
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -95,5 +95,6 @@ tests/test_status_enum_coverage.py
95
95
  tests/test_stay_on_loop_hook.py
96
96
  tests/test_stop_ghost_terminal.py
97
97
  tests/test_swarm_events.py
98
+ tests/test_task_progress.py
98
99
  tests/test_terminal_lifecycle.py
99
100
  tests/test_wait_open_tasks_contradiction.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.11.132"
7
+ version = "2.11.134"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -0,0 +1,147 @@
1
+ """Tests for the pbar-accuracy wheel half (task 6b2afa5d).
2
+
3
+ Samuel (msg d88bb9c7): "la barra de progreso no es accurate". Root cause:
4
+ mig 516 shipped the server-side RPC public.mc_task_progress, but the wheel
5
+ MCP never exposed a wrapper (QA msg ba932abc) — agents physically could not
6
+ report real pct, so the panel pbar showed elapsed-time guesses or froze.
7
+
8
+ This file asserts the contract of the fix:
9
+ 1. backend.task_progress maps args to the mig-516 RPC signature.
10
+ 2. server.py exposes the meshcode_task_progress MCP tool.
11
+ 3. Lifecycle auto-progress fallback exists: claim->10, start->25,
12
+ complete->100 — all routed through _auto_task_progress, which is
13
+ monotonic-guarded (never resets a re-claimed 60% task to 10%) and
14
+ best-effort (never blocks the lifecycle action).
15
+ 4. The complete->100 stamp happens BEFORE be.task_complete in source
16
+ order (mc_task_progress refuses status='done', so after is too late).
17
+ 5. mc_task_progress is on the agent-callable RPC allowlist.
18
+
19
+ server.py has module-level side effects requiring live Supabase, so the
20
+ server-side assertions are static (source/AST) — same pattern as
21
+ test_wait_open_tasks_contradiction.py. backend.py imports clean, so the
22
+ param-mapping test runs the real function with sb_rpc monkeypatched.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import unittest
28
+ from pathlib import Path
29
+
30
+ REPO = Path(__file__).resolve().parent.parent
31
+ SERVER_PY = REPO / "meshcode" / "meshcode_mcp" / "server.py"
32
+
33
+
34
+ class TestBackendTaskProgress(unittest.TestCase):
35
+ """backend.task_progress → mc_task_progress param mapping (mocked RPC)."""
36
+
37
+ def test_param_mapping_matches_mig_516_signature(self):
38
+ from meshcode.meshcode_mcp import backend as be
39
+ captured = {}
40
+
41
+ def fake_rpc(fn_name, params, **kw):
42
+ captured["fn"] = fn_name
43
+ captured["params"] = params
44
+ return {"ok": True, "task_id": params["p_task_id"], "progress_pct": params["p_pct"]}
45
+
46
+ orig = be.sb_rpc
47
+ be.sb_rpc = fake_rpc
48
+ try:
49
+ out = be.task_progress("key-x", "proj-1", "task-1", 40, "mitad")
50
+ finally:
51
+ be.sb_rpc = orig
52
+
53
+ self.assertEqual(captured["fn"], "mc_task_progress")
54
+ self.assertEqual(captured["params"], {
55
+ "p_api_key": "key-x",
56
+ "p_project_id": "proj-1",
57
+ "p_task_id": "task-1",
58
+ "p_pct": 40,
59
+ "p_note": "mitad",
60
+ })
61
+ self.assertTrue(out["ok"])
62
+
63
+ def test_note_defaults_to_none(self):
64
+ from meshcode.meshcode_mcp import backend as be
65
+ captured = {}
66
+
67
+ def fake_rpc(fn_name, params, **kw):
68
+ captured["params"] = params
69
+ return {"ok": True}
70
+
71
+ orig = be.sb_rpc
72
+ be.sb_rpc = fake_rpc
73
+ try:
74
+ be.task_progress("k", "p", "t", 70)
75
+ finally:
76
+ be.sb_rpc = orig
77
+ self.assertIsNone(captured["params"]["p_note"])
78
+
79
+
80
+ class TestServerWiring(unittest.TestCase):
81
+ """Static assertions on server.py — tool + lifecycle hooks."""
82
+
83
+ @classmethod
84
+ def setUpClass(cls):
85
+ cls.src = SERVER_PY.read_text(encoding="utf-8")
86
+
87
+ def test_tool_exposed(self):
88
+ self.assertIn("def meshcode_task_progress(", self.src,
89
+ "wheel must expose the meshcode_task_progress MCP tool "
90
+ "(the missing wrapper WAS the bug — QA msg ba932abc)")
91
+
92
+ def test_tool_validates_pct_client_side(self):
93
+ body = self._fn_body("def meshcode_task_progress(")
94
+ self.assertIn("pct must be an integer 0-100", body,
95
+ "tool must reject bad pct before burning an RPC")
96
+
97
+ def test_auto_progress_helper_exists_and_is_guarded(self):
98
+ self.assertIn("def _auto_task_progress(", self.src)
99
+ body = self._fn_body("def _auto_task_progress(")
100
+ self.assertIn("current >= pct", body,
101
+ "monotonic guard: a re-claimed task at 60% must not be "
102
+ "reset to the 10/25 lifecycle floor")
103
+ self.assertIn("except Exception", body,
104
+ "auto-progress must swallow all failures — it is a "
105
+ "fallback, never a dependency")
106
+
107
+ def test_lifecycle_hooks_present(self):
108
+ self.assertIn('"auto: claimed"', self.src, "claim → 10 hook missing")
109
+ self.assertIn('"auto: started"', self.src, "start → 25 hook missing")
110
+ self.assertIn('"auto: completed"', self.src, "complete → 100 hook missing")
111
+
112
+ def test_complete_stamps_100_before_rpc(self):
113
+ # mc_task_progress refuses status='done' — the 100 stamp must come
114
+ # BEFORE be.task_complete in meshcode_task_complete's body.
115
+ body = self._fn_body("def meshcode_task_complete(")
116
+ idx_stamp = body.find('_auto_task_progress(task_id, 100')
117
+ idx_complete = body.find("be.task_complete(")
118
+ self.assertGreater(idx_stamp, 0, "complete must stamp 100")
119
+ self.assertGreater(idx_complete, idx_stamp,
120
+ "100 stamp must precede be.task_complete — after "
121
+ "complete the task is 'done' and the RPC refuses")
122
+
123
+ def test_task_marker_present(self):
124
+ self.assertIn("6b2afa5d", self.src,
125
+ "code must reference the task id for future git-blame")
126
+
127
+ def _fn_body(self, needle: str) -> str:
128
+ start = self.src.find(needle)
129
+ assert start > 0, f"{needle!r} not found"
130
+ # body ends at the next top-level decorator/def
131
+ end = self.src.find("\n@mcp.tool()", start)
132
+ if end < 0:
133
+ end = self.src.find("\ndef ", start + 1)
134
+ return self.src[start:end if end > 0 else len(self.src)]
135
+
136
+
137
+ class TestAllowlist(unittest.TestCase):
138
+
139
+ def test_mc_task_progress_on_allowlist(self):
140
+ from meshcode.rpc_allowlist import AGENT_CALLABLE_RPCS
141
+ self.assertIn("mc_task_progress", AGENT_CALLABLE_RPCS,
142
+ "mig 516 grants it to anon — the allowlist is the "
143
+ "single source of truth and must agree")
144
+
145
+
146
+ if __name__ == "__main__":
147
+ unittest.main()
File without changes
File without changes
File without changes
File without changes