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.
- {meshcode-2.11.132 → meshcode-2.11.134}/PKG-INFO +1 -1
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/__init__.py +1 -1
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/backend.py +16 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/server.py +69 -1
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/protocol_handler.py +414 -20
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/rpc_allowlist.py +1 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode.egg-info/SOURCES.txt +1 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/pyproject.toml +1 -1
- meshcode-2.11.134/tests/test_task_progress.py +147 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/README.md +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/__main__.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/_session_handoff_template.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/cli.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/comms_v4.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/compat.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/daemon.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/doctor.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/hooks/__init__.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/hooks/repo_path_lock.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/hostd.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/invites.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/launcher.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/swarm.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/meshcode_mcp/test_swarm.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/preferences.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/run_agent.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/secrets.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/self_update.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/up.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode/upload.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/setup.cfg +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_core.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_doctor.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_hostd_zombie_sessions.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_live_mesh_guard.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_session_replay_gate.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_stop_ghost_terminal.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_swarm_events.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_terminal_lifecycle.py +0 -0
- {meshcode-2.11.132 → meshcode-2.11.134}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -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
|
-
|
|
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 —
|
|
179
|
-
# routes were tested live on the target box 2026-06-11 and
|
|
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'
|
|
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 =
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
364
|
-
|
|
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
|
-
|
|
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
|
|
616
|
-
#
|
|
617
|
-
#
|
|
618
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
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
|