@deeeed/metamask-harness 0.14.7 → 0.15.0

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 (100) hide show
  1. package/CHANGELOG.md +81 -6
  2. package/adapters/core/cleanup.sh +0 -0
  3. package/adapters/core/inject.sh +0 -0
  4. package/adapters/extension/cleanup.mjs +0 -0
  5. package/adapters/extension/ensure-browser.sh +4 -2
  6. package/adapters/extension/inject.mjs +0 -0
  7. package/adapters/extension/launch-browser.cjs +73 -34
  8. package/adapters/extension/launch.sh +0 -0
  9. package/adapters/extension/live.sh +21 -10
  10. package/adapters/extension/readiness.mjs +0 -0
  11. package/adapters/extension/reattach.sh +3 -3
  12. package/adapters/extension/refresh-build.sh +0 -0
  13. package/adapters/extension/seed-fixture.sh +0 -0
  14. package/adapters/extension/sidepanel-toggle.sh +10 -4
  15. package/adapters/extension/snapshot-dist.sh +0 -0
  16. package/adapters/extension/start-watch.sh +2 -2
  17. package/adapters/extension/stop-viewers.sh +0 -0
  18. package/adapters/extension/verify.sh +19 -2
  19. package/adapters/extension/wallet-fixture-state.cjs +14 -1
  20. package/adapters/manifest.json +19 -3
  21. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
  22. package/adapters/mobile/bridge-runtime/setup-wallet.sh +6 -4
  23. package/adapters/mobile/cleanup.sh +0 -0
  24. package/adapters/mobile/inject.sh +0 -0
  25. package/adapters/mobile/launch-metro.cjs +74 -0
  26. package/adapters/mobile/lib/metro-listener.sh +0 -0
  27. package/adapters/mobile/lib/tmux-viewer.sh +4 -4
  28. package/adapters/mobile/open-device.sh +3 -1
  29. package/adapters/mobile/prewarm-bundle.sh +0 -0
  30. package/adapters/mobile/start-metro.sh +12 -18
  31. package/adapters/mobile/stop-metro.sh +1 -0
  32. package/adapters/mobile/verify.sh +5 -5
  33. package/adapters/mobile/wait-for-bridge.sh +0 -0
  34. package/adapters/mobile/yarn-setup.sh +0 -0
  35. package/adapters/shared/activate-repo-node.sh +0 -0
  36. package/adapters/shared/activate-repo-ruby.sh +0 -0
  37. package/adapters/shared/cli-ux.sh +0 -0
  38. package/adapters/shared/ensure-runner-deps.sh +0 -0
  39. package/adapters/shared/harness-path.sh +0 -0
  40. package/adapters/shared/hash-helpers.sh +0 -0
  41. package/adapters/shared/install-repo-deps.sh +29 -0
  42. package/adapters/shared/json-field.sh +0 -0
  43. package/adapters/shared/open-debug.mjs +35 -4
  44. package/adapters/shared/open-log-window.sh +1 -1
  45. package/adapters/shared/reap-checkout-metros.sh +0 -0
  46. package/adapters/shared/resolve-farmslot-ports.mjs +0 -0
  47. package/adapters/shared/resolve-farmslot-ports.sh +0 -0
  48. package/adapters/shared/resolve-slot-ports-core.mjs +72 -1
  49. package/adapters/shared/resolve-slot-ports.mjs +0 -0
  50. package/adapters/shared/resolve-slot-ports.sh +21 -17
  51. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  52. package/adapters/shared/tmux-session.sh +0 -5
  53. package/adapters/shared/tmux-viewer.sh +7 -2
  54. package/dist/adapters/core/surface.js +3 -0
  55. package/dist/adapters/extension/ensure-ready.js +0 -7
  56. package/dist/adapters/extension/runtime-decision.js +93 -3
  57. package/dist/adapters/extension/surface.js +17 -6
  58. package/dist/adapters/mobile/metro-env.js +67 -0
  59. package/dist/adapters/mobile/perps-env.js +101 -0
  60. package/dist/adapters/mobile/prepare.js +32 -14
  61. package/dist/adapters/mobile/runtime-decision.js +21 -2
  62. package/dist/adapters/mobile/source-freshness.js +116 -0
  63. package/dist/adapters/mobile/surface.js +3 -0
  64. package/dist/adapters/resolve-slot-ports.js +4 -0
  65. package/dist/adapters/slot-ports.js +131 -49
  66. package/dist/adapters.js +7 -2
  67. package/dist/checkout-lock.js +72 -0
  68. package/dist/cli.js +2 -0
  69. package/dist/commands/call.js +91 -54
  70. package/dist/commands/check.js +266 -46
  71. package/dist/commands/checklist.js +136 -0
  72. package/dist/commands/debug.js +21 -2
  73. package/dist/commands/doctor.js +187 -12
  74. package/dist/commands/fixtures.js +177 -42
  75. package/dist/commands/launch/extension.js +6 -1
  76. package/dist/commands/launch/index.js +54 -22
  77. package/dist/commands/logs.js +24 -4
  78. package/dist/commands/run-engine.js +223 -7
  79. package/dist/commands/run.js +67 -50
  80. package/dist/commands/shared.js +86 -5
  81. package/dist/commands/stop.js +1 -2
  82. package/dist/doctor.js +39 -17
  83. package/dist/harness.js +5 -2
  84. package/dist/heal-bounds.js +1 -1
  85. package/dist/mm-harness-cli.js +37 -9
  86. package/dist/runner.js +35 -5
  87. package/dist/runtime-context.js +228 -0
  88. package/docs/CLI-SPEC.md +15 -11
  89. package/docs/MENTAL-MODEL.md +6 -6
  90. package/library/actions/extension/perps/perps.mjs +29 -8
  91. package/library/actions/extension/platform/cdp.mjs +128 -28
  92. package/library/actions/extension/ui/navigate.mjs +1 -1
  93. package/library/actions/mobile/perps/perps.mjs +13 -1
  94. package/library/actions/mobile/platform/bridge.mjs +302 -29
  95. package/library/actions/mobile/wallet/ensure_unlocked.mjs +7 -2
  96. package/library/manifests/extension.action-manifest.json +32 -12
  97. package/library/manifests/mobile.action-manifest.json +25 -3
  98. package/package.json +5 -5
  99. package/scripts/completions.sh +7 -4
  100. package/scripts/install-completions.sh +0 -0
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { spawn } = require('node:child_process');
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+
8
+ function parseArgs(argv) {
9
+ const values = {};
10
+ for (let index = 0; index < argv.length; index += 1) {
11
+ const arg = argv[index];
12
+ if (arg === '-h' || arg === '--help') {
13
+ values.help = true;
14
+ continue;
15
+ }
16
+ if (arg === '--clear') {
17
+ values.clear = true;
18
+ continue;
19
+ }
20
+ if (!arg.startsWith('--') || index + 1 >= argv.length) throw new Error(`invalid argument: ${arg}`);
21
+ values[arg.slice(2)] = argv[++index];
22
+ }
23
+ return values;
24
+ }
25
+
26
+ async function main() {
27
+ const args = parseArgs(process.argv.slice(2));
28
+ if (args.help) {
29
+ console.log('Usage: launch-metro.cjs --target <path> --port <port> --log <path> --pid-file <path> [--workers <n>] [--clear]');
30
+ return;
31
+ }
32
+ for (const required of ['target', 'port', 'log', 'pid-file']) {
33
+ if (!args[required]) throw new Error(`--${required} is required`);
34
+ }
35
+ const target = path.resolve(args.target);
36
+ const log = path.resolve(args.log);
37
+ const pidFile = path.resolve(args['pid-file']);
38
+ fs.mkdirSync(path.dirname(log), { recursive: true });
39
+ fs.mkdirSync(path.dirname(pidFile), { recursive: true });
40
+ const logFd = fs.openSync(log, 'a');
41
+ const childArgs = ['expo', 'start', '--port', String(args.port)];
42
+ if (args.clear) childArgs.push('--clear');
43
+ const env = { ...process.env };
44
+ if (args.workers) env.METRO_MAX_WORKERS = String(args.workers);
45
+ const child = spawn('yarn', childArgs, {
46
+ cwd: target,
47
+ detached: true,
48
+ env,
49
+ stdio: ['ignore', logFd, logFd],
50
+ });
51
+ await new Promise((resolve, reject) => {
52
+ child.once('spawn', resolve);
53
+ child.once('error', reject);
54
+ });
55
+ fs.closeSync(logFd);
56
+ fs.writeFileSync(pidFile, `${String(child.pid)}\n`);
57
+ fs.writeFileSync(
58
+ path.join(path.dirname(pidFile), 'metro-launch.json'),
59
+ `${JSON.stringify({
60
+ schemaVersion: 1,
61
+ pid: child.pid,
62
+ startedAt: new Date().toISOString(),
63
+ target,
64
+ port: Number(args.port),
65
+ clear: Boolean(args.clear),
66
+ }, null, 2)}\n`,
67
+ );
68
+ child.unref();
69
+ }
70
+
71
+ main().catch((error) => {
72
+ console.error(`launch-metro: ${error.message}`);
73
+ process.exit(1);
74
+ });
File without changes
@@ -11,11 +11,11 @@
11
11
  # resolved, skip the viewer entirely (Metro still runs detached to the log).
12
12
  #
13
13
  # Inputs (env/args): $1 = metro log path; PORT, LOG_DIR, and optional `slot` from
14
- # the caller; TMUX indicates we are inside a tmux client.
14
+ # the caller.
15
15
 
16
- # Session resolution (RECIPE_TMUX_SESSION → agentic-runtime.json session → current
17
- # tmux client). Sourced relative to this lib; harness-path fallback for the
18
- # installed layout where shared libs live beside this file.
16
+ # Session resolution (RECIPE_TMUX_SESSION → agentic-runtime.json session). Sourced
17
+ # relative to this lib; harness-path fallback for the installed layout where shared
18
+ # libs live beside this file.
19
19
  _tv_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
20
  # shellcheck disable=SC1091
21
21
  for _tv_src in "$_tv_dir/tmux-session.sh" "$_tv_dir/../../shared/tmux-session.sh"; do
@@ -220,7 +220,9 @@ if [ "$PLATFORM" = "ios" ]; then
220
220
  DEV_CLIENT_SCHEME="${IOS_DEV_CLIENT_SCHEME:-expo-metamask}"
221
221
 
222
222
  boot_simulator_if_needed "$SIM_TARGET" || exit 1
223
- show_simulator "$SIM_TARGET"
223
+ if [ "${MM_HARNESS_FOCUS_SIMULATOR:-0}" = "1" ]; then
224
+ show_simulator "$SIM_TARGET"
225
+ fi
224
226
 
225
227
  LAUNCHED=false
226
228
  for BUNDLE_ID in "${BUNDLE_IDS[@]}"; do
File without changes
@@ -89,11 +89,8 @@ start_console_forwarder() {
89
89
  # Only one debugger can own a React Native page. Reap any forwarder already
90
90
  # attached to this Metro port, even if it was launched from another checkout or
91
91
  # package install path and therefore does not own this runtime dir's pid file.
92
- while IFS= read -r line; do
93
- local pid cmd
94
- pid="${line%% *}"
95
- cmd="${line#* }"
96
- [ "$pid" != "$line" ] || continue
92
+ local pid cmd
93
+ while read -r pid cmd; do
97
94
  case "$cmd" in
98
95
  *console-forwarder.cjs*" --port $PORT "*|*console-forwarder.cjs*" --port=$PORT "*)
99
96
  kill "$pid" 2>/dev/null || true
@@ -227,19 +224,16 @@ if ! start_metro_tmux; then
227
224
  export EXPO_NO_TYPESCRIPT_SETUP=1
228
225
  export WATCHER_PORT="${PORT}" METRO_PORT="${PORT}"
229
226
  export METRO_MAX_WORKERS="${METRO_WORKERS}"
230
- # shellcheck disable=SC2094
231
- # nohup sets SIGHUP to ignored (inherited across exec), so Metro survives the
232
- # launching shell or tmux window closing; when this subshell exits the process
233
- # is reparented to init. nohup.out is suppressed (stdout/stderr both go to
234
- # $LOG_FILE via >>).
235
- metro_args=(expo start --port "$PORT")
236
- [ "$CLEAR" = true ] && metro_args+=(--clear)
237
- nohup yarn "${metro_args[@]}" \
238
- </dev/null >> "$LOG_FILE" 2>&1 &
239
- metro_pid="$!"
240
- echo "$metro_pid" > "$PID_FILE"
241
- disown "$metro_pid" 2>/dev/null || true
242
- )
227
+ launcher_args=(
228
+ --target "$TARGET"
229
+ --port "$PORT"
230
+ --log "$LOG_FILE"
231
+ --pid-file "$PID_FILE"
232
+ --workers "$METRO_WORKERS"
233
+ )
234
+ [ "$CLEAR" = true ] && launcher_args+=(--clear)
235
+ node "$SCRIPT_DIR/launch-metro.cjs" "${launcher_args[@]}"
236
+ ) || exit 1
243
237
 
244
238
  start_viewer_window "$LOG_FILE"
245
239
  fi
@@ -61,6 +61,7 @@ fi
61
61
  # incidental non-zero (platform-dependent) can never turn success into exit 1.
62
62
  set +e
63
63
  rm -f "$PID_FILE"
64
+ rm -f "$LOG_DIR/metro-launch.json"
64
65
 
65
66
  # start-metro runs a per-checkout console-forwarder holding the device debugger
66
67
  # slot; Metro going down must take it too, or the orphan keeps polling and later
@@ -283,7 +283,7 @@ const fs = require('fs');
283
283
  const path = require('path');
284
284
  const name = 'agentic overlay matches runner (HUD freshness)';
285
285
  const harnessDir = process.env.HARNESS_DIR_FOR_DRIFT;
286
- const targetDir = path.join(process.env.TARGET_FOR_DRIFT, 'app', 'core', 'AgenticService');
286
+ const targetDir = path.join(process.env.TARGET_FOR_DRIFT, 'app', 'dev-tools', 'AgenticService');
287
287
  function readTrimmed(file) {
288
288
  try { return fs.readFileSync(file, 'utf8').trim(); } catch { return ''; }
289
289
  }
@@ -298,7 +298,7 @@ if (!runnerDir) {
298
298
  process.stdout.write(JSON.stringify({ name, status: 'warn', detail: 'runner source unavailable; overlay freshness compare skipped' }));
299
299
  process.exit(0);
300
300
  }
301
- const overlayDir = path.join(runnerDir, 'adapters', 'mobile', 'overlay', 'app', 'core', 'AgenticService');
301
+ const overlayDir = path.join(runnerDir, 'adapters', 'mobile', 'overlay', 'app', 'dev-tools', 'AgenticService');
302
302
  let checked = 0;
303
303
  const drifted = [];
304
304
  const missing = [];
@@ -318,13 +318,13 @@ try {
318
318
  process.stdout.write(JSON.stringify({ name, status: 'warn', detail: 'runner overlay compare skipped: ' + String((error && error.message) || error) }));
319
319
  process.exit(0);
320
320
  }
321
- if (checked === 0) {
322
- process.stdout.write(JSON.stringify({ name, status: 'pass' }));
323
- } else if (drifted.length || missing.length) {
321
+ if (drifted.length || missing.length) {
324
322
  const parts = [];
325
323
  if (drifted.length) parts.push('behind runner: ' + drifted.join(', '));
326
324
  if (missing.length) parts.push('absent in repo: ' + missing.join(', '));
327
325
  process.stdout.write(JSON.stringify({ name, status: 'warn', detail: parts.join('; ') + ' — rerun mm-harness install --force-overlay or mm-harness fixtures sync to refresh the in-repo HUD/AgenticService from the runner' }));
326
+ } else if (checked === 0) {
327
+ process.stdout.write(JSON.stringify({ name, status: 'pass' }));
328
328
  } else {
329
329
  process.stdout.write(JSON.stringify({ name, status: 'pass' }));
330
330
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ TARGET=""
5
+ while [[ $# -gt 0 ]]; do
6
+ case "$1" in
7
+ --target)
8
+ [[ $# -ge 2 ]] || { echo "Usage: install-repo-deps.sh --target <checkout>" >&2; exit 2; }
9
+ TARGET="$2"
10
+ shift 2
11
+ ;;
12
+ -h|--help)
13
+ echo "Usage: install-repo-deps.sh --target <checkout>"
14
+ exit 0
15
+ ;;
16
+ *) echo "Unknown flag: $1" >&2; exit 2 ;;
17
+ esac
18
+ done
19
+
20
+ [[ -n "$TARGET" ]] || { echo "install-repo-deps: --target is required" >&2; exit 2; }
21
+ TARGET="$(cd "$TARGET" && pwd)"
22
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
+
24
+ # shellcheck disable=SC1091
25
+ . "$SCRIPT_DIR/activate-repo-node.sh"
26
+ require_repo_node "$TARGET"
27
+
28
+ cd "$TARGET"
29
+ corepack yarn install --immutable
File without changes
@@ -75,6 +75,12 @@ function emit(result, json) {
75
75
  if (result.ok) {
76
76
  const detail = result.method ? ` via ${result.method}` : '';
77
77
  const where = result.port ? ` (port ${result.port})` : result.cdpPort ? ` (cdp ${result.cdpPort})` : '';
78
+ if (result.opened === false) {
79
+ if (result.endpoint) console.log(`Resolved debugger endpoint${detail}${where}: ${result.endpoint}`);
80
+ else console.log(`Debugger action not invoked${detail}${where}`);
81
+ if (result.detail) console.log(result.detail);
82
+ return;
83
+ }
78
84
  console.log(`Opened debugger${detail}${where}`);
79
85
  if (result.target?.url) console.log(`Target: ${result.target.url}`);
80
86
  if (result.configureHint) console.log(`Configure chrome://inspect target: ${result.configureHint}`);
@@ -144,16 +150,30 @@ async function openMobileDebugger(port, action, shouldOpen) {
144
150
  if (!Number.isInteger(numericPort) || numericPort <= 0) {
145
151
  return { ok: false, adapter: 'mobile', action: normalizedAction, error: 'invalid-port', port };
146
152
  }
153
+ const endpoint = `http://127.0.0.1:${numericPort}${urlPath}`;
154
+ if (!shouldOpen) {
155
+ return {
156
+ ok: true,
157
+ adapter: 'mobile',
158
+ action: normalizedAction,
159
+ method: 'metro-http',
160
+ endpoint,
161
+ port: numericPort,
162
+ opened: false,
163
+ detail: 'Open action not invoked (--no-open).',
164
+ };
165
+ }
147
166
 
148
167
  let metroError = null;
149
168
  try {
150
- const opened = await fetchOk(`http://127.0.0.1:${numericPort}${urlPath}`, { method: 'POST' });
169
+ const opened = await fetchOk(endpoint, { method: 'POST' });
151
170
  if (opened) {
152
171
  return {
153
172
  ok: true,
154
173
  adapter: 'mobile',
155
174
  action: normalizedAction,
156
175
  method: 'metro-http',
176
+ endpoint,
157
177
  port: numericPort,
158
178
  opened: shouldOpen,
159
179
  };
@@ -171,6 +191,9 @@ async function openMobileDebugger(port, action, shouldOpen) {
171
191
  adapter: 'mobile',
172
192
  action: normalizedAction,
173
193
  method: 'cdp-eval',
194
+ endpoint: normalizedAction === 'dev-menu'
195
+ ? 'NativeModules.DevSettings.show()'
196
+ : 'NativeModules.DevSettings.openDebugger()',
174
197
  port: numericPort,
175
198
  opened: shouldOpen,
176
199
  };
@@ -181,6 +204,7 @@ async function openMobileDebugger(port, action, shouldOpen) {
181
204
  adapter: 'mobile',
182
205
  action: normalizedAction,
183
206
  method: 'metro-http',
207
+ endpoint,
184
208
  port: numericPort,
185
209
  error: metroError,
186
210
  hint: 'Metro may be down or the debug target is not ready. Check metro status and try again.',
@@ -214,9 +238,11 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
214
238
 
215
239
  const inspectUrl = 'chrome://inspect/#devices';
216
240
  const configureHint = `127.0.0.1:${numericPort}`;
241
+ const versionEndpoint = `http://127.0.0.1:${numericPort}/json/version`;
242
+ const listEndpoint = `http://127.0.0.1:${numericPort}/json/list`;
217
243
 
218
244
  try {
219
- await fetchJson(`http://127.0.0.1:${numericPort}/json/version`);
245
+ await fetchJson(versionEndpoint);
220
246
  } catch (error) {
221
247
  return {
222
248
  ok: false,
@@ -224,6 +250,7 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
224
250
  targetKind,
225
251
  cdpPort: numericPort,
226
252
  error: 'cdp-unreachable',
253
+ endpoint: versionEndpoint,
227
254
  inspectUrl,
228
255
  configureHint,
229
256
  hint: `Start the extension runtime first (mm-harness launch). Then open chrome://inspect and add ${configureHint}.`,
@@ -233,7 +260,7 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
233
260
 
234
261
  let targets;
235
262
  try {
236
- targets = await fetchJson(`http://127.0.0.1:${numericPort}/json/list`);
263
+ targets = await fetchJson(listEndpoint);
237
264
  } catch (error) {
238
265
  return {
239
266
  ok: false,
@@ -241,6 +268,7 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
241
268
  targetKind,
242
269
  cdpPort: numericPort,
243
270
  error: 'cdp-list-failed',
271
+ endpoint: listEndpoint,
244
272
  inspectUrl,
245
273
  configureHint,
246
274
  hint: `CDP is up but /json/list failed. Open chrome://inspect and add ${configureHint}.`,
@@ -255,6 +283,7 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
255
283
  targetKind,
256
284
  cdpPort: numericPort,
257
285
  error: 'cdp-list-invalid',
286
+ endpoint: listEndpoint,
258
287
  inspectUrl,
259
288
  configureHint,
260
289
  hint: `Open chrome://inspect and add ${configureHint}.`,
@@ -269,6 +298,7 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
269
298
  adapter: 'extension',
270
299
  targetKind,
271
300
  method: 'devtools-frontend',
301
+ endpoint: picked.devtoolsFrontendUrl,
272
302
  cdpPort: numericPort,
273
303
  target: {
274
304
  type: picked.type,
@@ -291,6 +321,7 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
291
321
  adapter: 'extension',
292
322
  targetKind,
293
323
  method: picked?.devtoolsFrontendUrl ? 'devtools-frontend' : 'chrome-inspect',
324
+ endpoint: picked?.devtoolsFrontendUrl || inspectUrl,
294
325
  cdpPort: numericPort,
295
326
  target: picked
296
327
  ? { type: picked.type, url: picked.url, title: picked.title || null }
@@ -341,4 +372,4 @@ main().catch((error) => {
341
372
  console.error(payload.error);
342
373
  }
343
374
  process.exit(1);
344
- });
375
+ });
@@ -45,7 +45,7 @@ session=""
45
45
  command -v resolve_run_tmux_session >/dev/null 2>&1 && session="$(resolve_run_tmux_session "$RUNTIME_DIR")"
46
46
  if [ -z "$session" ] || ! tmux has-session -t "=$session" 2>/dev/null; then
47
47
  echo "open-log-window: no run-owned tmux session to place the window in." >&2
48
- echo " Next: run inside your slot's tmux session, or set RECIPE_TMUX_SESSION=<session>, then retry" >&2
48
+ echo " Next: set RECIPE_TMUX_SESSION=<session>, or configure agentic-runtime.json session, then retry" >&2
49
49
  exit 1
50
50
  fi
51
51
 
File without changes
File without changes
File without changes
@@ -139,8 +139,77 @@ export function resolveDefaultExtensionPorts(repo) {
139
139
  });
140
140
  }
141
141
 
142
+ function parseKvLines(output) {
143
+ if (!output?.trim()) return {};
144
+ const kv = {};
145
+ for (const line of output.split('\n')) {
146
+ const trimmed = line.trim();
147
+ if (!trimmed) continue;
148
+ const eq = trimmed.indexOf('=');
149
+ if (eq <= 0) continue;
150
+ const key = trimmed.slice(0, eq);
151
+ const val = trimmed.slice(eq + 1);
152
+ if (val === 'null' || val === 'undefined' || val === '') continue;
153
+ kv[key] = val;
154
+ }
155
+ return kv;
156
+ }
157
+
158
+ function mergeKvLayers(...layers) {
159
+ const result = {};
160
+ for (const layer of layers) {
161
+ const source = typeof layer === 'string' ? layer : layer?.source;
162
+ const overwrite = typeof layer === 'string' ? false : Boolean(layer?.overwrite);
163
+ const kv = parseKvLines(source);
164
+ for (const [key, val] of Object.entries(kv)) {
165
+ if (overwrite || result[key] === undefined) result[key] = val;
166
+ }
167
+ }
168
+ return Object.keys(result).length ? formatKvLines(result) : '';
169
+ }
170
+
171
+ function extensionRuntimeContextPath(repo) {
172
+ const repoRoot = path.resolve(repo);
173
+ const envCtx = process.env.RECIPE_RUNTIME_CONTEXT;
174
+ if (envCtx) {
175
+ const resolved = path.resolve(envCtx);
176
+ if (resolved === repoRoot || resolved.startsWith(`${repoRoot}${path.sep}`)) {
177
+ return resolved;
178
+ }
179
+ }
180
+ return path.join(repoRoot, recipeRuntimeDir(), 'agentic-runtime.json');
181
+ }
182
+
183
+ export function resolveExtensionRuntimeContext(repo) {
184
+ const ctxPath = extensionRuntimeContextPath(repo);
185
+ if (!fs.existsSync(ctxPath)) return null;
186
+ try {
187
+ const c = JSON.parse(fs.readFileSync(ctxPath, 'utf8'));
188
+ const cdp = c.cdpPort;
189
+ const watcher = c.devServerPort ?? c.watcherPort ?? c.metroPort;
190
+ if ((cdp == null || cdp === '') && (watcher == null || watcher === '') && !c.slotId) return null;
191
+ return formatKvLines({
192
+ ...(cdp != null && cdp !== '' ? { CDP_PORT: cdp } : {}),
193
+ ...(watcher != null && watcher !== '' ? { WATCHER_PORT: watcher } : {}),
194
+ ...(c.slotId ? { SLOT_ID: c.slotId } : {}),
195
+ });
196
+ } catch {
197
+ return null;
198
+ }
199
+ }
200
+
201
+ // Checkout-scoped extension ports: runtime context (authoritative) → pool → suffix formula.
202
+ export function formatExtensionSlotPorts(repo) {
203
+ const realRepo = path.resolve(repo);
204
+ return mergeKvLayers(
205
+ { source: resolveExtensionRuntimeContext(realRepo), overwrite: true },
206
+ { source: resolveSlotPortsByRepo(realRepo), overwrite: false },
207
+ { source: resolveDefaultExtensionPorts(realRepo), overwrite: false },
208
+ );
209
+ }
210
+
142
211
  export function resolveExtensionRuntimePorts(repo) {
143
- return resolveSlotPortsByRepo(repo) ?? resolveDefaultExtensionPorts(repo) ?? '';
212
+ return formatExtensionSlotPorts(repo);
144
213
  }
145
214
 
146
215
  export function resolveMobileSlotDefaults(repo) {
@@ -216,6 +285,8 @@ export const cliFns = {
216
285
  // Back-compat cli key for one release.
217
286
  resolve_farmslot_ports_by_repo: resolveSlotPortsByRepo,
218
287
  resolve_default_extension_ports: resolveDefaultExtensionPorts,
288
+ resolve_extension_runtime_context: resolveExtensionRuntimeContext,
289
+ resolve_extension_slot_ports: (repo) => formatExtensionSlotPorts(repo) || null,
219
290
  resolve_extension_runtime_ports: (repo) => resolveExtensionRuntimePorts(repo) || null,
220
291
  resolve_mobile_runtime_context: resolveMobileRuntimeContext,
221
292
  resolve_mobile_slot_defaults: resolveMobileSlotDefaults,
File without changes
@@ -43,33 +43,37 @@ resolve_extension_runtime_ports() {
43
43
  _resolve_ports_cli resolve_extension_runtime_ports "${1:-.}"
44
44
  }
45
45
 
46
- apply_resolved_extension_ports() {
47
- local repo="${1:-.}" line key val from_pool=false resolved=""
48
- if resolved="$(_resolve_ports_cli resolve_slot_ports_by_repo "$repo" 2>/dev/null)"; then
49
- from_pool=true
50
- elif resolved="$(_resolve_ports_cli resolve_default_extension_ports "$repo" 2>/dev/null)"; then
51
- from_pool=false
46
+ resolve_extension_slot_ports() {
47
+ _resolve_ports_cli resolve_extension_slot_ports "${1:-.}"
48
+ }
49
+
50
+ # Apply the checkout-scoped extension port ladder (context pool → formula).
51
+ apply_extension_slot_ports() {
52
+ local repo="${1:-.}" line key val resolved="" ctx_file="" runtime_rel="${RECIPE_RUNTIME_DIR:-temp/recipe/runtime}"
53
+ repo="$(cd "$repo" && pwd)"
54
+ ctx_file="$repo/$runtime_rel/agentic-runtime.json"
55
+ if [ -f "$ctx_file" ]; then
56
+ export RECIPE_RUNTIME_CONTEXT="$ctx_file"
52
57
  else
53
- return 0
58
+ unset RECIPE_RUNTIME_CONTEXT
54
59
  fi
60
+ resolved="$(_resolve_ports_cli resolve_extension_slot_ports "$repo" 2>/dev/null)" || return 0
55
61
  while IFS= read -r line; do
56
62
  [ -n "$line" ] || continue
57
63
  key="${line%%=*}"
58
64
  val="${line#*=}"
59
65
  case "$key" in
60
- CDP_PORT)
61
- if [ "$from_pool" = true ] || [ -z "${CDP_PORT:-}" ]; then CDP_PORT="$val"; fi
62
- ;;
63
- WATCHER_PORT)
64
- if [ "$from_pool" = true ] || [ -z "${WATCHER_PORT:-}" ]; then WATCHER_PORT="$val"; fi
65
- ;;
66
- SLOT_ID)
67
- if [ "$from_pool" = true ] || [ -z "${RECIPE_SLOT_ID:-}" ]; then RECIPE_SLOT_ID="$val"; fi
68
- ;;
66
+ CDP_PORT) CDP_PORT="$val" ;;
67
+ WATCHER_PORT) WATCHER_PORT="$val" ;;
68
+ SLOT_ID) RECIPE_SLOT_ID="$val" ;;
69
69
  esac
70
70
  done <<< "$resolved"
71
71
  }
72
72
 
73
+ apply_resolved_extension_ports() {
74
+ apply_extension_slot_ports "$@"
75
+ }
76
+
73
77
  resolve_mobile_slot_defaults() {
74
78
  _resolve_ports_cli resolve_mobile_slot_defaults "$1"
75
79
  }
@@ -107,4 +111,4 @@ apply_resolved_mobile_ports() {
107
111
  ;;
108
112
  esac
109
113
  done <<< "$resolved"
110
- }
114
+ }
File without changes
@@ -7,8 +7,6 @@
7
7
  # tail -F) in the wrong place. Resolution ladder, first hit wins:
8
8
  # 1. RECIPE_TMUX_SESSION — the orchestrator's explicit hand-off.
9
9
  # 2. agentic-runtime.json `session` — the prepared checkout's recorded session.
10
- # 3. the current session — but ONLY inside a tmux client (never the
11
- # last-attached session reported outside one, which would be a foreign leak).
12
10
  # Prints the resolved name (empty when none). Callers still gate on has-session
13
11
  # before creating a window.
14
12
 
@@ -28,8 +26,5 @@ resolve_run_tmux_session() {
28
26
  return 0
29
27
  fi
30
28
  fi
31
- if [ -n "${TMUX:-}" ]; then
32
- tmux display-message -p '#S' 2>/dev/null || true
33
- fi
34
29
  return 0
35
30
  }
@@ -53,9 +53,14 @@ tmux_viewer_close_marker() {
53
53
  local marker_file="$1"
54
54
  command -v tmux >/dev/null 2>&1 || { rm -f "$marker_file"; return 0; }
55
55
  if [ -f "$marker_file" ]; then
56
- local target
56
+ local target marker_session owned_session runtime_dir
57
57
  target="$(cat "$marker_file" 2>/dev/null || true)"
58
- [ -z "$target" ] || tmux kill-window -t "$target" >/dev/null 2>&1 || true
58
+ marker_session="${target%%:*}"
59
+ runtime_dir="$(dirname "$marker_file")"
60
+ owned_session="$(resolve_run_tmux_session "$runtime_dir")"
61
+ if [ -n "$target" ] && [ -n "$owned_session" ] && [ "$marker_session" = "$owned_session" ]; then
62
+ tmux kill-window -t "$target" >/dev/null 2>&1 || true
63
+ fi
59
64
  fi
60
65
  rm -f "$marker_file"
61
66
  }
@@ -44,6 +44,9 @@ const coreSurface = {
44
44
  logSources() {
45
45
  return [];
46
46
  },
47
+ appLogSource() {
48
+ return null;
49
+ },
47
50
  hints: {
48
51
  launch: "mm-harness run <recipe> # run recipes against the headless core",
49
52
  relaunch: "mm-harness verify"
@@ -27,12 +27,6 @@ async function closeTab(port, id) {
27
27
  } catch {
28
28
  }
29
29
  }
30
- async function activateTab(port, id) {
31
- try {
32
- await fetch(`http://127.0.0.1:${port}/json/activate/${id}`, { signal: AbortSignal.timeout(4e3) });
33
- } catch {
34
- }
35
- }
36
30
  function isStrayTab(url) {
37
31
  if (typeof url !== "string") return false;
38
32
  return /^chrome:\/\/newtab\/?$/.test(url) || /^chrome:\/\/new-tab-page\b/.test(url) || url === "about:blank";
@@ -131,7 +125,6 @@ async function ensureExtensionReady(target, options) {
131
125
  for (const t of convergedTargets) {
132
126
  if (t.type === "page" && t.id && isStrayTab(t.url)) await closeTab(cdpPort, String(t.id));
133
127
  }
134
- if (homesNow[0]?.id) await activateTab(cdpPort, String(homesNow[0].id));
135
128
  }
136
129
  const finalListing = await jsonList(cdpPort);
137
130
  if (!finalListing.ok) {