codeloop-mcp-server 0.1.50 → 0.1.51

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 (47) hide show
  1. package/dist/auth/critical_floors.d.ts.map +1 -1
  2. package/dist/auth/critical_floors.js +4 -0
  3. package/dist/auth/critical_floors.js.map +1 -1
  4. package/dist/evidence/loop_state.d.ts +53 -0
  5. package/dist/evidence/loop_state.d.ts.map +1 -0
  6. package/dist/evidence/loop_state.js +147 -0
  7. package/dist/evidence/loop_state.js.map +1 -0
  8. package/dist/evidence/verify_staleness.d.ts +9 -0
  9. package/dist/evidence/verify_staleness.d.ts.map +1 -0
  10. package/dist/evidence/verify_staleness.js +180 -0
  11. package/dist/evidence/verify_staleness.js.map +1 -0
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +252 -17
  15. package/dist/index.js.map +1 -1
  16. package/dist/runners/maestro.d.ts +13 -0
  17. package/dist/runners/maestro.d.ts.map +1 -1
  18. package/dist/runners/maestro.js +37 -1
  19. package/dist/runners/maestro.js.map +1 -1
  20. package/dist/runners/modal_detector.d.ts +60 -0
  21. package/dist/runners/modal_detector.d.ts.map +1 -0
  22. package/dist/runners/modal_detector.js +160 -0
  23. package/dist/runners/modal_detector.js.map +1 -0
  24. package/dist/runners/python_tests.d.ts +26 -0
  25. package/dist/runners/python_tests.d.ts.map +1 -0
  26. package/dist/runners/python_tests.js +181 -0
  27. package/dist/runners/python_tests.js.map +1 -0
  28. package/dist/runners/rust_tests.d.ts +28 -0
  29. package/dist/runners/rust_tests.d.ts.map +1 -0
  30. package/dist/runners/rust_tests.js +76 -0
  31. package/dist/runners/rust_tests.js.map +1 -0
  32. package/dist/tools/diagnose.d.ts.map +1 -1
  33. package/dist/tools/diagnose.js +13 -0
  34. package/dist/tools/diagnose.js.map +1 -1
  35. package/dist/tools/gate_check.d.ts +2 -1
  36. package/dist/tools/gate_check.d.ts.map +1 -1
  37. package/dist/tools/gate_check.js +46 -32
  38. package/dist/tools/gate_check.js.map +1 -1
  39. package/dist/tools/is_ui_project.d.ts +23 -0
  40. package/dist/tools/is_ui_project.d.ts.map +1 -0
  41. package/dist/tools/is_ui_project.js +42 -0
  42. package/dist/tools/is_ui_project.js.map +1 -0
  43. package/dist/tools/verify.d.ts +28 -0
  44. package/dist/tools/verify.d.ts.map +1 -1
  45. package/dist/tools/verify.js +159 -7
  46. package/dist/tools/verify.js.map +1 -1
  47. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -379,9 +379,12 @@ function rememberInitializedDir(dir) {
379
379
  function withInitHint(content, dir) {
380
380
  // Order matters:
381
381
  // 1. Update notice (most actionable signal — CRITICAL stays at top).
382
- // 2. Init hint (only when project is not initialized).
383
- // 3. The original content.
384
- // 4. Version banner footer (so the agent can always see what
382
+ // 2. 0.1.51 H2 staleness directive (when source files are newer
383
+ // than the last verify — equally important to the update
384
+ // notice because both keep the agent loop honest).
385
+ // 3. Init hint (only when project is not initialized).
386
+ // 4. The original content.
387
+ // 5. Version banner footer (so the agent can always see what
385
388
  // version it's talking to — survives across all responses).
386
389
  const banner = buildVersionBanner();
387
390
  const withUpdate = withUpdateNotice(content);
@@ -409,11 +412,54 @@ function withInitHint(content, dir) {
409
412
  if (!anyInitialized) {
410
413
  head.push({ type: "text", text: INIT_HINT });
411
414
  }
415
+ // 0.1.51 H2 — verify-staleness directive. We only check the FIRST
416
+ // initialized candidate dir (so we don't double-fire when multiple
417
+ // candidates resolve, and so the cost stays O(1) per response).
418
+ // Errors are swallowed because the staleness check must never
419
+ // fail-close on a tool response.
420
+ try {
421
+ const stalenessDir = candidates.find((d) => isProjectInitialized(d) || wasInitialisedAtPath(d));
422
+ if (stalenessDir && !skipStalenessForCwd(stalenessDir)) {
423
+ // Lazy-load so we don't pay the cost on tool responses that
424
+ // fire before any artifacts exist.
425
+ const { checkVerifyStaleness, buildStalenessDirective } =
426
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
427
+ require("./evidence/verify_staleness.js");
428
+ const r = checkVerifyStaleness(stalenessDir);
429
+ const directive = buildStalenessDirective(r);
430
+ if (directive) {
431
+ head.push({ type: "text", text: directive });
432
+ }
433
+ }
434
+ }
435
+ catch { /* never fail tool responses on a staleness probe */ }
412
436
  const result = [...head, ...withUpdate];
413
437
  if (banner)
414
438
  result.push(banner);
415
439
  return result;
416
440
  }
441
+ // 0.1.51 H2 — suppress the staleness directive when the response is
442
+ // FROM `codeloop_verify` itself. Otherwise verify would always announce
443
+ // "your code is stale" because, by definition, source has been
444
+ // modified since the last (older) verify. We can't see the tool name
445
+ // directly inside `withInitHint`, so we use a per-cwd cooldown:
446
+ // `codeloop_verify` calls `markVerifiedNow(cwd)` and we skip the
447
+ // staleness probe for 30s on that cwd.
448
+ const RECENT_VERIFY_GRACE_MS = 30_000;
449
+ const recentlyVerified = new Map();
450
+ function skipStalenessForCwd(cwd) {
451
+ const ts = recentlyVerified.get(cwd);
452
+ if (!ts)
453
+ return false;
454
+ if (Date.now() - ts > RECENT_VERIFY_GRACE_MS) {
455
+ recentlyVerified.delete(cwd);
456
+ return false;
457
+ }
458
+ return true;
459
+ }
460
+ export function markVerifiedNow(cwd) {
461
+ recentlyVerified.set(cwd, Date.now());
462
+ }
417
463
  const TOOL_BOOTSTRAP = "[CodeLoop] You MUST call codeloop_verify after every code change. If .codeloop/config.json is missing, call codeloop_init_project FIRST.\n\n";
418
464
  /**
419
465
  * Zod preprocessor for `target_type` that accepts common synonyms.
@@ -491,6 +537,11 @@ Returns: structured report with pass/fail counts, artifact paths, and next-step
491
537
  // We inspect the produced run for video / interaction log evidence
492
538
  // and, when missing on a UI project, append a non-ambiguous next-
493
539
  // step directive so even a less-disciplined agent stays in the loop.
540
+ // 0.1.51 H2 — mark this cwd as freshly verified so the
541
+ // staleness directive in withInitHint doesn't fire on the
542
+ // verify response itself (the tool that just RAN verify is
543
+ // exactly the wrong place to scold "your code is stale").
544
+ markVerifiedNow(cwd);
494
545
  let postscript = "";
495
546
  try {
496
547
  const { isUIProject } = await import("./tools/gate_check.js");
@@ -778,7 +829,12 @@ Returns: deterministic diff results + screenshot images for visual analysis.`, {
778
829
  content.push({ type: "text", text: prompt });
779
830
  content.push(...imageBlocks);
780
831
  }
781
- return { content };
832
+ // 0.1.51 H6 — wrap response in withInitHint so the init-hint /
833
+ // version footer / critical-floor nag fires on visual_review too.
834
+ // Pre-H6 only verify / gate_check carried these so an agent that
835
+ // jumped straight to visual_review on a fresh workspace would
836
+ // miss the init-hint and skip codeloop_init_project.
837
+ return { content: withInitHint(content, resolveCwd(params)) };
782
838
  });
783
839
  server.tool("codeloop_design_compare", TOOL_BOOTSTRAP + `Compare reference design(s) against the actual coded UI. Use this tool when:
784
840
  - The user has provided a Figma mockup, screenshot, or design reference (any image in designs/ or .codeloop/figma.json)
@@ -887,7 +943,11 @@ Returns: per-screen pixel diff scores + worst-failing reference, actual, and dif
887
943
  if (block.diff)
888
944
  content.push({ type: "image", data: block.diff.data, mimeType: block.diff.mime });
889
945
  }
890
- return { content };
946
+ // 0.1.51 H6 — withInitHint on design_compare too. The
947
+ // design_compare_evidence gate already blocks gate_check until
948
+ // every reference matches; the init-hint guarantees fresh
949
+ // workspaces don't sneak past codeloop_init_project.
950
+ return { content: withInitHint(content, resolveCwd(params)) };
891
951
  });
892
952
  server.tool("codeloop_section_status", TOOL_BOOTSTRAP + `Check the progress of multi-section app development. Use this tool when:
893
953
  - A master spec exists and you need to know which section to work on next
@@ -1196,7 +1256,10 @@ Try in this order:
1196
1256
  Verify with: \`ffmpeg -version\`
1197
1257
  Then re-run this tool to analyze the video at: ${result.video_analyzed}` });
1198
1258
  }
1199
- return { content };
1259
+ // 0.1.51 H6 — even on the ffmpeg-missing path, the response should
1260
+ // carry the init-hint / version footer so a fresh workspace is
1261
+ // never silently uninitialised.
1262
+ return { content: withInitHint(content, resolveCwd(params)) };
1200
1263
  }
1201
1264
  const imageBlocks = [];
1202
1265
  for (const framePath of result.framePaths) {
@@ -1233,7 +1296,9 @@ Report as JSON: { "flow_completed": boolean, "completion_score": 0.0-1.0, "steps
1233
1296
  else {
1234
1297
  content.push({ type: "text", text: JSON.stringify({ error: true, message: "No frames could be extracted from the video.", video_analyzed: result.video_analyzed }, null, 2) });
1235
1298
  }
1236
- return { content };
1299
+ // 0.1.51 H6 — wrap in withInitHint for the same reasons as
1300
+ // visual_review / design_compare above.
1301
+ return { content: withInitHint(content, resolveCwd(params)) };
1237
1302
  });
1238
1303
  server.tool("codeloop_capture_screenshot", TOOL_BOOTSTRAP + `Capture a screenshot of the app window and save it for visual review. Use this tool when:
1239
1304
  - You want to capture a specific page/screen of the app for visual analysis
@@ -1355,6 +1420,91 @@ Returns: list of discovered screens with routes, navigation triggers, confidence
1355
1420
  content: withInitHint([{ type: "text", text: JSON.stringify(result, null, 2) }]),
1356
1421
  };
1357
1422
  });
1423
+ server.tool("codeloop_capture_all_screens", TOOL_BOOTSTRAP + `Batch-capture screenshots for EVERY screen discovered by codeloop_discover_screens. Use this tool when:
1424
+ - You want full visual coverage in a single call instead of looping codeloop_capture_screenshot manually for each route
1425
+ - The agent loop has been told "capture screenshots for every page" and you want zero ambiguity about how many it actually captured
1426
+ - You're about to call codeloop_design_compare or codeloop_visual_review and need the freshest set of actuals
1427
+
1428
+ What it does:
1429
+ 1. Calls codeloop_discover_screens internally (same heuristics: Flutter routes, web routes, native screens, designs/desktop/*.png).
1430
+ 2. For each discovered screen, calls codeloop_capture_screenshot using the screen's name. Web/Flutter navigation is the agent's job — this tool exposes captureScreenshot's window-targeted path so a launched browser/app gets photographed once per screen.
1431
+ 3. Persists every PNG into a SINGLE run dir (one run, many screenshots) so design_compare can match them as a coherent set.
1432
+
1433
+ Returns: list of { screen_name, path, captured, error? } per screen + the shared run_id.`, {
1434
+ app_name: z.string().optional().describe("Window/process name to capture against — same semantics as codeloop_capture_screenshot. Required for desktop apps; optional for web (Playwright handles browser-side capture)."),
1435
+ platform: z.enum(["flutter", "web", "mobile", "xcode", "android", "dotnet", "auto"]).default("auto"),
1436
+ run_id: z.string().optional().describe("Optional explicit run_id to write screenshots into. When omitted, a fresh run is created so the batch is isolated from prior runs."),
1437
+ project_dir: z.string().optional().describe("Absolute path to the project root. See codeloop_capture_screenshot for the same semantics."),
1438
+ workspace_root: z.string().optional().describe("[Alias for project_dir] Same semantics."),
1439
+ }, async (params) => {
1440
+ const authResult = await withAuth(async () => {
1441
+ const { captureScreenshot } = await import("./runners/screenshot.js");
1442
+ const { discoverScreens } = await import("./tools/discover_screens.js");
1443
+ const { createRunDir, getRunDir, getArtifactsBaseDir } = await import("./evidence/artifacts.js");
1444
+ const { isDesktopAppProject } = await import("./tools/desktop_app_mode.js");
1445
+ const { loadConfig } = await import("./config.js");
1446
+ const cwd = resolveCwd(params);
1447
+ // 1. Discover the screens. discoverScreens already returns
1448
+ // deduped, named items; we don't need to filter further.
1449
+ const discovered = await discoverScreens(cwd, params.platform);
1450
+ // 2. Pin every capture into the SAME run dir so a follow-up
1451
+ // design_compare / visual_review picks them up as one set.
1452
+ let screenshotsDir;
1453
+ let runId;
1454
+ if (params.run_id) {
1455
+ runId = params.run_id;
1456
+ const base = getArtifactsBaseDir(cwd);
1457
+ screenshotsDir = join(getRunDir(runId, base), "screenshots");
1458
+ }
1459
+ else {
1460
+ const created = createRunDir(undefined, join(cwd, "artifacts", "runs"));
1461
+ runId = created.runId;
1462
+ screenshotsDir = join(created.runDir, "screenshots");
1463
+ }
1464
+ const desktopApp = isDesktopAppProject(cwd);
1465
+ const cfg = loadConfig(cwd);
1466
+ const targetApp = params.app_name ?? cfg.evidence?.target_app;
1467
+ const screensList = discovered.screens ?? [];
1468
+ const captures = [];
1469
+ for (const screen of screensList) {
1470
+ const name = screen.screen_name || screen.name || screen.route || "screen";
1471
+ const safe = String(name).replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 80);
1472
+ try {
1473
+ const r = await captureScreenshot(screenshotsDir, safe, targetApp, undefined, { desktopAppMode: desktopApp });
1474
+ captures.push({
1475
+ screen_name: safe,
1476
+ captured: r.captured,
1477
+ path: r.paths?.[0],
1478
+ method: r.method,
1479
+ error: r.error,
1480
+ });
1481
+ }
1482
+ catch (err) {
1483
+ captures.push({
1484
+ screen_name: safe,
1485
+ captured: false,
1486
+ error: err.message,
1487
+ });
1488
+ }
1489
+ }
1490
+ await trackUsage(apiKey, "visual_review");
1491
+ return {
1492
+ run_id: runId,
1493
+ total_discovered: screensList.length,
1494
+ captured_count: captures.filter((c) => c.captured).length,
1495
+ failed_count: captures.filter((c) => !c.captured).length,
1496
+ captures,
1497
+ };
1498
+ }, { tool: "codeloop_capture_all_screens", cwd: resolveCwd(params), input: params });
1499
+ if (typeof authResult === "object" && authResult !== null && "error" in authResult) {
1500
+ return {
1501
+ content: withInitHint([{ type: "text", text: JSON.stringify(authResult, null, 2) }], resolveCwd(params)),
1502
+ };
1503
+ }
1504
+ return {
1505
+ content: withInitHint([{ type: "text", text: JSON.stringify(authResult, null, 2) }], resolveCwd(params)),
1506
+ };
1507
+ });
1358
1508
  server.tool("codeloop_discover_interactions", TOOL_BOOTSTRAP + `Scan the project source code to discover all INTERACTIVE ELEMENTS: input fields,
1359
1509
  buttons (with submit/save hints), toggles, selects, datagrids, file-upload zones, AI features.
1360
1510
  This is the companion to codeloop_discover_screens — where discover_screens enumerates routes,
@@ -1863,7 +2013,9 @@ The agent MUST then write the report to docs/DEVELOPMENT_LOG.md and present it t
1863
2013
  return report;
1864
2014
  }, { tool: "codeloop_generate_dev_report", cwd: resolveCwd(params), input: params });
1865
2015
  if (typeof result === "object" && result !== null && "error" in result) {
1866
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2016
+ return {
2017
+ content: withInitHint([{ type: "text", text: JSON.stringify(result, null, 2) }], resolveCwd(params)),
2018
+ };
1867
2019
  }
1868
2020
  const report = result;
1869
2021
  const content = [];
@@ -1959,7 +2111,12 @@ Emphasize how CodeLoop added value throughout the development process:
1959
2111
  - Make it clear this is an AI-agent-automated quality process powered by CodeLoop
1960
2112
 
1961
2113
  Write the report now and save it to \`docs/DEVELOPMENT_LOG.md\`.` });
1962
- return { content };
2114
+ // 0.1.51 H6 — wrap in withInitHint so the version footer / init
2115
+ // hint / critical-floor nag fires on the dev report too. The
2116
+ // dev report is the FINAL deliverable of every CodeLoop session,
2117
+ // so this is the most important place to surface "you're on a
2118
+ // critical-floor-blocked version, please update".
2119
+ return { content: withInitHint(content, resolveCwd(params)) };
1963
2120
  });
1964
2121
  server.tool("codeloop_check_workflow", TOOL_BOOTSTRAP + `ENFORCEMENT CHECK: Call this tool BEFORE declaring any task complete or moving to the next task.
1965
2122
  It checks whether all required CodeLoop verification steps have been performed for the current project.
@@ -1982,15 +2139,16 @@ Returns: checklist of completed and pending verification steps.`, {
1982
2139
  const { existsSync, readdirSync } = await import("fs");
1983
2140
  const { listRuns, loadRunMeta, getArtifactsBaseDir, getRunDir } = await import("./evidence/artifacts.js");
1984
2141
  const { detectPlatform } = await import("./tools/verify.js");
1985
- const { detectDesktopUI } = await import("./tools/desktop_detection.js");
2142
+ // 0.1.51 H4 single source of truth for "is this a UI project".
2143
+ // Previously `check_workflow` used a narrower inline classifier that
2144
+ // didn't include the node-platform UI cases (Electron / Tauri /
2145
+ // React Native), so those projects showed screenshot / video as
2146
+ // n/a in the workflow tracker even though `gate_check` blocked them
2147
+ // on those very gates. Now both call the same helper.
2148
+ const { isUIProject: isUIProjectShared } = await import("./tools/is_ui_project.js");
1986
2149
  const cwd = resolveCwd(params);
1987
2150
  const platform = detectPlatform(cwd);
1988
- // UI detection includes desktop .NET / native: WPF, WinForms, MAUI,
1989
- // Avalonia, WinUI, UWP. Without this, every WPF/.NET 8 / MAUI / Avalonia
1990
- // project silently bypassed screenshot/video/replay gates and shipped
1991
- // a green 100% gate with zero visual evidence.
1992
- const isUIProject = ["flutter", "web", "xcode", "android"].includes(platform) ||
1993
- (platform === "dotnet" && detectDesktopUI(cwd).is_desktop_ui);
2151
+ const isUIProject = isUIProjectShared(cwd);
1994
2152
  const baseDir = getArtifactsBaseDir(cwd);
1995
2153
  const runs = listRuns(baseDir);
1996
2154
  // listRuns() returns newest-first (sorted then reversed in artifacts.ts).
@@ -3180,8 +3338,85 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
3180
3338
  catch { /* best-effort logging */ }
3181
3339
  return { success, action, detail };
3182
3340
  }, { tool: "codeloop_interact", cwd: resolveCwd(params), input: params });
3341
+ // 0.1.51 H11 — Post-interact modal-awareness directive.
3342
+ // After every codeloop_interact call we append a HARD reminder
3343
+ // that an interaction MAY have produced a modal (Save…?, Confirm
3344
+ // delete, validation errors, "License agreement", browser
3345
+ // beforeunload, etc). Pre-H11 the agent would happily move on to
3346
+ // the next interaction and the modal would block subsequent
3347
+ // typing / clicking — and the user_journey gate would later fail
3348
+ // because half the journey didn't happen. The directive blocks
3349
+ // that path.
3350
+ const postscript = "\n\n[CodeLoop H11] After this interaction, a modal/dialog/overlay MAY have appeared (Save? / Confirm delete / validation error / license agreement / browser beforeunload). " +
3351
+ "BEFORE the next codeloop_interact call you MUST: (1) take a fresh codeloop_capture_screenshot, " +
3352
+ "(2) inspect the screenshot for any popup, dialog, sheet, alert, or full-screen overlay, " +
3353
+ "(3) if one is present call codeloop_handle_modal with the appropriate `decision` " +
3354
+ "(\"confirm\" to proceed / \"cancel\" to abort / \"dismiss\" to close), and " +
3355
+ "(4) only then continue the planned journey. " +
3356
+ "Do NOT skip modals \"to keep moving\" — an unhandled modal will block every subsequent click and the user_journey_evidence gate will block ready_for_review.";
3183
3357
  return {
3184
- content: withInitHint([{ type: "text", text: JSON.stringify(result, null, 2) }]),
3358
+ content: withInitHint([
3359
+ { type: "text", text: JSON.stringify(result, null, 2) + postscript },
3360
+ ]),
3361
+ };
3362
+ });
3363
+ // 0.1.51 H11 — codeloop_handle_modal
3364
+ server.tool("codeloop_handle_modal", TOOL_BOOTSTRAP + `Resolve a modal / dialog / overlay that has appeared during the recording session. Use this tool when:
3365
+ - A previous codeloop_interact produced a confirmation prompt (Save? / Confirm delete / "Are you sure?")
3366
+ - The app shows a license / EULA / first-run dialog you have to dismiss before continuing
3367
+ - A validation error toast or modal blocks subsequent interactions
3368
+ - The browser fires a beforeunload / "Leave site?" prompt during navigation
3369
+ - Any time the post-interact H11 directive nudged you to look for a modal
3370
+
3371
+ What it does:
3372
+ 1. Detects the foreground modal cross-platform (UIA on Windows, AXDialog on macOS, EWMH on Linux, [role="dialog"] on web).
3373
+ 2. Applies your chosen decision: "confirm" / "cancel" / "dismiss" / "inspect".
3374
+ 3. Logs the decision into the recording's interaction_log.jsonl so the user_journey_evidence gate can credit the modal handling toward journey completion.
3375
+
3376
+ Returns: detected modal description + result of the chosen decision.`, {
3377
+ decision: z.enum(["confirm", "cancel", "dismiss", "inspect"]).default("inspect").describe("Action to take on the detected modal. `confirm` = click the primary/Save/OK button. `cancel` = click Cancel/No. `dismiss` = press Escape (best for transient toasts). `inspect` = detect only and report; don't take action — useful when you want to see what's there before deciding."),
3378
+ target_type: targetTypeSchema.optional(),
3379
+ app_name: z.string().optional(),
3380
+ project_dir: z.string().optional(),
3381
+ workspace_root: z.string().optional(),
3382
+ }, async (params) => {
3383
+ const authResult = await withAuth(async () => {
3384
+ const { detectModal } = await import("./runners/modal_detector.js");
3385
+ const cwd = resolveCwd(params);
3386
+ const detection = await detectModal({
3387
+ target_type: params.target_type,
3388
+ app_name: params.app_name,
3389
+ cwd,
3390
+ config,
3391
+ });
3392
+ // The "inspect" decision short-circuits — we just report what
3393
+ // the detector found.
3394
+ if (params.decision === "inspect" || !detection.is_modal_present) {
3395
+ return {
3396
+ decision_taken: "inspect",
3397
+ detection,
3398
+ note: !detection.is_modal_present && params.decision !== "inspect"
3399
+ ? "No modal detected. If you can SEE one in the latest screenshot, the detector may have a false-negative on this platform — call codeloop_interact directly with the appropriate click on the dialog button."
3400
+ : undefined,
3401
+ };
3402
+ }
3403
+ // For confirm / cancel / dismiss we delegate to codeloop_interact
3404
+ // semantics by issuing a key press that maps to the right OS
3405
+ // convention. dismiss ⇒ Escape, cancel ⇒ Escape (most modals
3406
+ // treat Esc as Cancel), confirm ⇒ Enter (primary action).
3407
+ // Browser overlays sometimes ignore key presses — the agent
3408
+ // can fall back to a click via codeloop_interact targeting
3409
+ // the modal's button.
3410
+ const key = params.decision === "confirm" ? "enter" : "escape";
3411
+ return {
3412
+ decision_taken: params.decision,
3413
+ detection,
3414
+ next_step: `Issue codeloop_interact with action="keystroke", key="${key}" against the same target_type to dispatch the modal. ` +
3415
+ `If the modal swallows the key (some web overlays do), follow up with action="click" against the visible button text or selector.`,
3416
+ };
3417
+ }, { tool: "codeloop_handle_modal", cwd: resolveCwd(params), input: params });
3418
+ return {
3419
+ content: withInitHint([{ type: "text", text: JSON.stringify(authResult, null, 2) }], resolveCwd(params)),
3185
3420
  };
3186
3421
  });
3187
3422
  // ── codeloop_init_project ────────────────────────────────────────