impel-cli 0.20.46-beta.0 → 0.20.46

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.
package/README.md CHANGED
@@ -384,7 +384,7 @@ impel tasks delete IMP-123 --yes
384
384
  Use `--json` when exact machine-readable fields matter. Gateway-only members
385
385
  cannot use task CRUD.
386
386
 
387
- ### Native Tasks MCP App
387
+ ### Native Tasks board and MCP tools
388
388
 
389
389
  Setup and update register a second tenant-bound MCP server named
390
390
  `impel-tasks` in Impel-managed Claude and Codex/ChatGPT profiles. It is separate
@@ -404,14 +404,20 @@ control-plane HTTP bearer credential, and fixes tenant selection in the
404
404
  transport header. Generated Claude/Codex MCP entries contain neither the PAT
405
405
  nor a tenant-derived credential.
406
406
 
407
- - The pinned Impel Claude Desktop renders the standard MCP Apps task board when
408
- its host supports that protocol.
409
- - The exact pinned Impel Codex desktop profile enables its reviewed
410
- `enable_mcp_apps` contract so `show_tasks` can render the same board. The
411
- vendor-pin test fails closed if the embedded Codex binary stops accepting it.
407
+ - Managed Claude and ChatGPT/Codex apps on macOS add one persistent Tasks
408
+ destination. Windows keeps the tenant-bound headless `impel-tasks` tools but
409
+ does not inject the board into the signed Store/MSIX apps: their verified
410
+ Electron archives cannot be patched without invalidating vendor signatures.
411
+ It loads the authenticated canonical Impel `/tasks` application directly in
412
+ a sandboxed, tenant-isolated web view; a fresh profile uses the existing
413
+ Board default.
414
+ - The trusted native process obtains the selected tenant credential through a
415
+ private one-shot pipe, exchanges the PAT for a bounded HttpOnly web session,
416
+ and then loads `/tasks` without an Authorization header. The web renderer has
417
+ no preload, Node integration, credential API, or IPC bridge.
412
418
  - Claude Code and Codex CLI profiles remain headless and receive complete task
413
- tools plus text results; the desktop-only renderer flag is never written to
414
- those profiles.
419
+ tools plus text results. Desktop Codex no longer enables the MCP Apps
420
+ renderer; the headless `impel-tasks` server remains for model/tool access.
415
421
  - Profiles preserve unrelated MCP servers. If a user-authored server already
416
422
  owns the `impel-tasks` name, setup/update fails with remediation instead of
417
423
  overwriting it.
package/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,70 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.20.46-beta.2 — Reload reconciled apps safely
4
+
5
+ - Closes every running managed macOS app before rewriting its profile and
6
+ desktop Tasks preloads, then reopens only the managed launchers that the
7
+ reconcile run actually closed. Current bundles still avoid an unnecessary
8
+ rebuild.
9
+ - Allows the production Tasks board to load GitHub-hosted member avatars while
10
+ keeping desktop web-view egress restricted to exact reviewed origins.
11
+ - States the signed-platform boundary explicitly: persistent in-app Tasks
12
+ navigation is a macOS feature; Windows retains tenant-bound headless Tasks
13
+ tools because modifying Store/MSIX Electron archives would invalidate the
14
+ verified vendor signatures.
15
+ - Gives Claude doctor probes enough output budget for reasoning-capable models
16
+ and reports non-terminal provider stop reasons such as token exhaustion
17
+ directly instead of presenting them as an unexplained missing acknowledgement.
18
+ - Preserves the `0.20.46-beta.1` released fixture and captures this prerelease as
19
+ a separate upgrade corpus without advancing the managed config or bundle
20
+ fingerprints.
21
+
22
+ ## 0.20.46-beta.1 — Embed the canonical Tasks board
23
+
24
+ - Replaces the duplicate desktop Tasks renderer with the authenticated
25
+ `www.useimpel.com/tasks` application in a sandboxed, tenant-isolated web
26
+ view. A fresh profile opens the existing Board default.
27
+ - Exchanges the tenant-bound Tasks PAT for an eight-hour HttpOnly web session
28
+ through a one-shot, header-only bootstrap. The PAT never enters a URL,
29
+ renderer, IPC bridge, profile, launch argument, or log.
30
+ - Removes the desktop-only Codex MCP Apps renderer flag while retaining the
31
+ tenant-bound headless Tasks MCP tools for CLI/model compatibility.
32
+ - Keeps the v1 Tasks widget available on the control plane for already-installed
33
+ desktop clients; its source and resource are retired only after the beta and
34
+ stable rollout cutoff is explicitly approved.
35
+ - Retains the telemetry, remote flags, and failure-reporting capabilities from
36
+ `0.20.46-beta.0`; beta users do not lose that prerelease functionality.
37
+ - Keeps a selected board visible when its host loses focus, and confines the
38
+ native Tasks fallback to the primary vendor window so first-run auxiliary
39
+ windows cannot cover or stall the board.
40
+ - Targets npm's existing `next` dist-tag. Promote the same
41
+ reviewed tree to stable `0.20.46` only after the session endpoint is deployed
42
+ and the isolated pinned-app launch smoke passes.
43
+
44
+ ## 0.20.46 — Ship hardened managed apps and the embedded Tasks board
45
+
46
+ - Promotes the tested `0.20.46-beta.2` tree without changing its managed
47
+ profile or bundle fingerprints.
48
+ - Updates the coordinated reviewed ChatGPT pin to desktop `26.803.81509`,
49
+ embedded Codex `0.147.0-alpha.6.6`, and Windows Store package
50
+ `26.803.10989.0`, keeping artifact, signature, runtime, and version checks
51
+ exact and fail-closed.
52
+ - Embeds the authenticated canonical Impel Tasks board in managed macOS Claude
53
+ and ChatGPT apps through a sandboxed, tenant-isolated web view while retaining
54
+ tenant-bound headless Tasks tools on Windows and in both CLIs.
55
+ - Closes every running managed macOS app before rewriting its profile and Tasks
56
+ preloads, then reopens only the exact managed launchers the update closed;
57
+ current bundles still avoid unnecessary rebuilds.
58
+ - Uses a code-mode-compatible Codex model for explicit tenant-agent adapters
59
+ and refreshes existing generated adapters through the managed-agent manifest.
60
+ - Keeps Tasks cold starts and the native navigation fallback reliable, permits
61
+ only the reviewed GitHub avatar origin, and gives reasoning-capable Claude
62
+ doctor probes enough output budget with actionable non-terminal stop reasons.
63
+ - Retries gateway model-catalog body-read timeouts as readiness failures,
64
+ preserves original targeted-recovery diagnostics and healthy sibling apps,
65
+ prevents external-source additions on managed verbatim-relay surfaces, and
66
+ accepts the control plane's `cancelled` task progress value.
67
+
3
68
  ## 0.20.41 — Windows onboarding: converge over an already-installed Store ChatGPT
4
69
 
5
70
  - Accepts winget's UPDATE_NOT_APPLICABLE result during setup as well as
@@ -48,9 +48,9 @@ is treated as an eager host.
48
48
  and re-qualified rather than receiving unknown configuration.
49
49
  - `mcp__impel_agent` is the native tool namespace. The server id
50
50
  `impel_agent` is not equivalent and does not qualify as direct exposure.
51
- - Generated Codex fixed-binding adapters use the ultra-fast Codex Spark model
52
- at low reasoning for the thin tool-selection and terminal-relay turns, with
53
- unrelated local host, plugin, remote-plugin, and skill-catalog context
51
+ - Generated Codex fixed-binding adapters use the Code Mode-compatible Luna
52
+ model at low reasoning for the thin tool-selection and terminal-relay turns,
53
+ with unrelated local host, plugin, remote-plugin, and skill-catalog context
54
54
  disabled. The selected Eve agent, not this wrapper model, still owns the
55
55
  substantive task.
56
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.46-beta.0",
3
+ "version": "0.20.46",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
package/src/agents.js CHANGED
@@ -65,14 +65,14 @@ export const NATIVE_AGENT_RESUME_TOOL = "resume_native_agent_run";
65
65
  export const NATIVE_AGENT_RECOVER_TOOL = "recover_native_agent_runs";
66
66
  export const NATIVE_AGENT_CONTINUATION_SCHEMA = "impel.native-agent-continuation.v1";
67
67
  export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
68
- export const MANAGED_AGENT_MANIFEST_VERSION = 24;
68
+ export const MANAGED_AGENT_MANIFEST_VERSION = 25;
69
69
  export const IMPEL_NATIVE_PARENT_DIRECT_ENV = "IMPEL_NATIVE_PARENT_DIRECT";
70
70
  export const IMPEL_NATIVE_SLASH_COMMANDS_ENV = "IMPEL_NATIVE_SLASH_COMMANDS";
71
71
 
72
72
  // The host model only selects the fixed MCP tool and faithfully returns its
73
- // result. Spark minimizes those transport-only turns while the selected Eve
74
- // agent remains responsible for the substantive work.
75
- const CODEX_AGENT_ADAPTER_MODEL = "gpt-5.3-codex-spark";
73
+ // result. Luna advertises the code-mode contract required by the direct-only
74
+ // adapter while the selected Eve agent remains responsible for substantive work.
75
+ const CODEX_AGENT_ADAPTER_MODEL = "gpt-5.6-luna";
76
76
 
77
77
  const NATIVE_AGENT_TOOL_NAMES = [
78
78
  NATIVE_AGENT_RUN_TOOL,
package/src/apps.js CHANGED
@@ -6,7 +6,6 @@ import { spawnSync } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import {
8
8
  enableManagedCodexDesktopCodeModeHost,
9
- enableManagedCodexDesktopMcpApps,
10
9
  hardenManagedCodexToml,
11
10
  secureAllManagedCodexHomes,
12
11
  secureManagedCodexHome,
@@ -29,7 +28,6 @@ import { CURRENT_CONFIG_VERSION } from "./managedProfileVersion.js";
29
28
  import { IMPEL_CODEX_PARENT_DELEGATION_INSTRUCTIONS } from "./commands/launch.js";
30
29
  import {
31
30
  desktopTasksAssetPaths,
32
- desktopTasksHostHtml,
33
31
  desktopTasksMainPreload,
34
32
  desktopTasksViewPreload,
35
33
  } from "./desktopTasks.js";
@@ -511,6 +509,38 @@ export function blockingAppNames(targets, { tenantId = null, tenantName = null }
511
509
  return [...new Set(names)];
512
510
  }
513
511
 
512
+ function blockingAppEntries(targets, {
513
+ tenantId = null,
514
+ tenantName = null,
515
+ homeDir = os.homedir(),
516
+ bundleTargets = targets,
517
+ } = {}) {
518
+ const bundleTargetSet = new Set(bundleTargets);
519
+ const paths = appPaths(homeDir, tenantId, { tenantName });
520
+ const legacyPaths = tenantId ? appPaths(homeDir) : null;
521
+ const entries = [];
522
+ for (const target of targets) {
523
+ if (bundleTargetSet.has(target)) {
524
+ for (const root of vendorSearchRoots(homeDir, target)) {
525
+ for (const bundleName of APP_DEFINITIONS[target].names) {
526
+ entries.push({
527
+ name: bundleName.replace(/\.app$/u, ""),
528
+ appPath: path.join(root, bundleName),
529
+ });
530
+ }
531
+ }
532
+ if (legacyPaths) {
533
+ entries.push({ name: APP_DEFINITIONS[target].label, appPath: legacyPaths[target].launcher });
534
+ }
535
+ }
536
+ entries.push({
537
+ name: managedLauncherName(target, { tenantId, tenantName }),
538
+ appPath: paths[target].launcher,
539
+ });
540
+ }
541
+ return [...new Map(entries.map((entry) => [entry.appPath, entry])).values()];
542
+ }
543
+
514
544
  /**
515
545
  * pgrep/pkill -f pattern for one app's processes. Anchored on
516
546
  * "/<Name>.app/.../Contents/MacOS/" so "Claude" never matches the
@@ -522,6 +552,14 @@ export function appProcessPattern(name) {
522
552
  return `/${escaped}\\.app/(.*/)?Contents/MacOS/`;
523
553
  }
524
554
 
555
+ /** Anchored process pattern for one exact outer app bundle. */
556
+ export function appBundleProcessPattern(appPath) {
557
+ // App bundles and process argv paths are macOS/POSIX paths even when this
558
+ // pure helper is exercised by the cross-platform test matrix.
559
+ const escaped = path.posix.resolve(appPath).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
560
+ return `^${escaped}/(.*/)?Contents/MacOS/`;
561
+ }
562
+
525
563
  const QUIT_POLL_MS = 500;
526
564
 
527
565
  /**
@@ -535,39 +573,46 @@ const QUIT_POLL_MS = 500;
535
573
  export async function quitBlockingApps(targets, {
536
574
  tenantId = null,
537
575
  tenantName = null,
576
+ homeDir = os.homedir(),
577
+ bundleTargets = targets,
538
578
  log = console.log,
539
- isRunning = (name) =>
540
- spawnSync("/usr/bin/pgrep", ["-f", appProcessPattern(name)], { stdio: "ignore" }).status === 0,
541
- requestQuit = (name) =>
579
+ isRunning = (_name, appPath) =>
580
+ spawnSync("/usr/bin/pgrep", ["-f", appBundleProcessPattern(appPath)], { stdio: "ignore" }).status === 0,
581
+ requestQuit = (_name, appPath) =>
542
582
  // osascript `tell` would LAUNCH a non-running app, so callers must gate on
543
583
  // isRunning first; the timeout covers apps stuck on a save/confirm dialog.
544
- spawnSync("/usr/bin/osascript", ["-e", `tell application ${JSON.stringify(name)} to quit`], {
584
+ spawnSync("/usr/bin/osascript", ["-e", `tell application ${JSON.stringify(appPath)} to quit`], {
545
585
  stdio: "ignore",
546
586
  timeout: 8_000,
547
587
  }),
548
- forceQuit = (name) =>
549
- spawnSync("/usr/bin/pkill", ["-f", appProcessPattern(name)], { stdio: "ignore" }),
588
+ forceQuit = (_name, appPath) =>
589
+ spawnSync("/usr/bin/pkill", ["-f", appBundleProcessPattern(appPath)], { stdio: "ignore" }),
550
590
  wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
551
591
  quitWaitMs = 10_000,
552
592
  killWaitMs = 5_000,
553
593
  } = {}) {
554
- async function goneWithin(name, budgetMs) {
594
+ async function goneWithin(name, appPath, budgetMs) {
555
595
  for (let waited = 0; waited <= budgetMs; waited += QUIT_POLL_MS) {
556
- if (!isRunning(name)) return true;
596
+ if (!isRunning(name, appPath)) return true;
557
597
  await wait(QUIT_POLL_MS);
558
598
  }
559
- return !isRunning(name);
599
+ return !isRunning(name, appPath);
560
600
  }
561
601
 
562
602
  const closed = [];
563
- for (const name of blockingAppNames(targets, { tenantId, tenantName })) {
564
- if (!isRunning(name)) continue;
603
+ for (const { name, appPath } of blockingAppEntries(targets, {
604
+ tenantId,
605
+ tenantName,
606
+ homeDir,
607
+ bundleTargets,
608
+ })) {
609
+ if (!isRunning(name, appPath)) continue;
565
610
  log(`${name} is running; closing it before touching app bundles…`);
566
- requestQuit(name);
567
- if (await goneWithin(name, quitWaitMs)) { closed.push(name); continue; }
611
+ requestQuit(name, appPath);
612
+ if (await goneWithin(name, appPath, quitWaitMs)) { closed.push(name); continue; }
568
613
  log(`${name} did not quit gracefully; terminating it…`);
569
- forceQuit(name);
570
- if (await goneWithin(name, killWaitMs)) { closed.push(name); continue; }
614
+ forceQuit(name, appPath);
615
+ if (await goneWithin(name, appPath, killWaitMs)) { closed.push(name); continue; }
571
616
  throw new Error(`${name} is still running; close it manually, then re-run`);
572
617
  }
573
618
  return closed;
@@ -615,11 +660,14 @@ export function appPaths(homeDir = os.homedir(), tenantId = null, {
615
660
  };
616
661
  }
617
662
 
618
- function writeDesktopTasksProfileAssets(root) {
663
+ function writeDesktopTasksProfileAssets(root, appUrl) {
619
664
  const assets = desktopTasksAssetPaths(root);
620
- writeAtomic(assets.mainPreload, desktopTasksMainPreload(), 0o600);
665
+ writeAtomic(assets.mainPreload, desktopTasksMainPreload(appUrl), 0o600);
621
666
  writeAtomic(assets.viewPreload, desktopTasksViewPreload(), 0o600);
622
- writeAtomic(assets.hostHtml, desktopTasksHostHtml(), 0o600);
667
+ // This exact path is Impel-owned. Remove the former local MCP Apps host when
668
+ // a profile is regenerated; old released clients keep the immutable server
669
+ // resource during the rollout window.
670
+ fs.rmSync(assets.hostHtml, { force: true });
623
671
  return assets;
624
672
  }
625
673
 
@@ -828,7 +876,10 @@ export function installManagedAppFiles({
828
876
 
829
877
  const installed = [];
830
878
  for (const target of targets) {
831
- writeDesktopTasksProfileAssets(paths[target].root);
879
+ writeDesktopTasksProfileAssets(
880
+ paths[target].root,
881
+ config.appUrl || RUNTIME_BRAND.controlPlane.defaultOrigin,
882
+ );
832
883
  // Non-bundle targets are the background-refresh / fast-open path: configs,
833
884
  // token helper, catalog, and manifest only. Bundle swaps require the app
834
885
  // to be closed (see quitBlockingApps), so those paths never attempt one.
@@ -1444,7 +1495,7 @@ function writeChatGPTConfig(
1444
1495
  writeAtomic(
1445
1496
  configPath,
1446
1497
  enableManagedCodexDesktopCodeModeHost(
1447
- enableManagedCodexDesktopMcpApps(hardenedToml, configPath),
1498
+ hardenedToml,
1448
1499
  configPath,
1449
1500
  ),
1450
1501
  0o600,
@@ -1603,11 +1654,9 @@ export function managedChatGPTConfigDrifted(paths) {
1603
1654
  // surface the vendor settings writer rewrites. code_mode_host is
1604
1655
  // load-bearing: without it pinned Codex fails closed on the code_mode_only
1605
1656
  // default model, so a dropped key must self-heal as drift rather than wait
1606
- // out the refresh TTL. enable_mcp_apps only degrades rendering but rides
1607
- // the same check. Configs from generations before the keys existed are
1657
+ // out the refresh TTL. Configs from generations before the key existed are
1608
1658
  // merely stale (the manifest version bump rewrites them), never drift —
1609
- // require the keys only once this install's manifest proves they were
1610
- // written.
1659
+ // require it only once this install's manifest proves it was written.
1611
1660
  let manifestConfigVersion = 0;
1612
1661
  try {
1613
1662
  manifestConfigVersion = Number(JSON.parse(
@@ -1618,8 +1667,7 @@ export function managedChatGPTConfigDrifted(paths) {
1618
1667
  }
1619
1668
  const featuresTable = current.match(/^\[features\]\n(?:[^[\n][^\n]*\n|\n)*/mu)?.[0] || "";
1620
1669
  const featureKeysDropped = manifestConfigVersion >= 35
1621
- && (!/^code_mode_host = true$/mu.test(featuresTable)
1622
- || !/^enable_mcp_apps = true$/mu.test(featuresTable));
1670
+ && !/^code_mode_host = true$/mu.test(featuresTable);
1623
1671
  return !current.includes(CHATGPT_CONFIG_START)
1624
1672
  || readTopLevelTomlString(current, "model_provider") !== RUNTIME_BRAND.cli.providerId
1625
1673
  || !readTopLevelTomlString(current, "chatgpt_base_url")
@@ -148,24 +148,6 @@ export function hardenManagedCodexToml(toml, configPath = "managed Codex config"
148
148
  return applyManagedCodexSandboxPolicy(withoutCredentialSnapshots, configPath);
149
149
  }
150
150
 
151
- /**
152
- * Enable the standard MCP Apps renderer only in the exact pinned desktop
153
- * profile. CLI profiles deliberately stay headless, and vendor-pin contract
154
- * tests must prove that the embedded Codex binary still recognizes this flag.
155
- */
156
- export function enableManagedCodexDesktopMcpApps(
157
- toml,
158
- configPath = "managed Codex desktop config",
159
- ) {
160
- return upsertManagedScalar(
161
- toml,
162
- "features",
163
- "enable_mcp_apps",
164
- "true",
165
- configPath,
166
- );
167
- }
168
-
169
151
  /**
170
152
  * Enable the Code Mode execution host in the exact pinned desktop profile.
171
153
  * Pinned Codex fails closed when a code_mode_only model is selected while
@@ -592,11 +592,11 @@ export async function reconcileMacTenantApps({
592
592
  .filter((status) => completedTargets.includes(status.target) && !io.bundleCurrent(status))
593
593
  .map((status) => status.target);
594
594
  let closedApps = [];
595
- if (staleTargets.length) {
596
- const result = await io.quitApps(staleTargets, { tenantId: config.tenantId, tenantName: config.tenantName });
595
+ if (completedTargets.length) {
596
+ const result = await io.quitApps(completedTargets, { tenantId: config.tenantId, tenantName: config.tenantName });
597
597
  if (Array.isArray(result)) closedApps = result;
598
598
  }
599
- const reopenTargets = new Set(staleTargets.filter((target) => closedApps.includes(
599
+ const reopenTargets = new Set(completedTargets.filter((target) => closedApps.includes(
600
600
  managedLauncherName(target, { tenantId: config.tenantId, tenantName: config.tenantName }),
601
601
  )));
602
602
  const installed = completedTargets.length ? await io.installFiles({
@@ -1054,19 +1054,34 @@ export async function cmdApps(argv, overrides = {}) {
1054
1054
  ? [...actionTargets]
1055
1055
  : statuses.filter((status) => !io.bundleCurrent(status)).map((status) => status.target);
1056
1056
 
1057
- // A running vendor or Impel app only needs to close when its bundle will be
1058
- // swapped. Remember which managed launchers we closed so the update can
1059
- // reopen them afterward and feel in-place (the fast path never quits at all).
1057
+ if (flags["skip-vendor"]) {
1058
+ const missingVendorTargets = statuses
1059
+ .filter((status) => staleBundleTargets.includes(status.target) && !status.vendorPath)
1060
+ .map((status) => status.target);
1061
+ if (missingVendorTargets.length > 0) {
1062
+ throw new Error(
1063
+ `${missingVendorTargets.join(", ")} verified vendor app is unavailable; rerun without --skip-vendor`,
1064
+ );
1065
+ }
1066
+ }
1067
+
1068
+ // Every explicit install/update rewrites the isolated model profile and may
1069
+ // refresh skills or native agents. Close a running managed app even when its
1070
+ // bundle is current so its long-lived client process cannot keep serving the
1071
+ // pre-update catalog. Remember only the managed launchers we actually closed
1072
+ // and reopen those after every profile sync finishes.
1060
1073
  let closedApps = [];
1061
- if (staleBundleTargets.length > 0) {
1062
- const quitResult = await io.quitApps(staleBundleTargets, {
1074
+ if (actionTargets.length > 0) {
1075
+ const quitResult = await io.quitApps(actionTargets, {
1063
1076
  tenantId: config.tenantId,
1064
1077
  tenantName: config.tenantName,
1078
+ homeDir: io.homeDir,
1079
+ bundleTargets: staleBundleTargets,
1065
1080
  });
1066
1081
  if (Array.isArray(quitResult)) closedApps = quitResult;
1067
1082
  }
1068
1083
  const reopenTargets = new Set(
1069
- staleBundleTargets.filter((target) => closedApps.includes(
1084
+ actionTargets.filter((target) => closedApps.includes(
1070
1085
  managedLauncherName(target, { tenantId: config.tenantId, tenantName: config.tenantName }),
1071
1086
  )),
1072
1087
  );
@@ -1118,8 +1133,7 @@ export async function cmdApps(argv, overrides = {}) {
1118
1133
  }));
1119
1134
  }
1120
1135
 
1121
- // Reopen the managed apps we had to close for the bundle swap, so the update
1122
- // lands in place from the user's point of view. Apps that were already
1136
+ // Reopen the managed apps we closed for the update. Apps that were already
1123
1137
  // closed (not in reopenTargets) stay closed.
1124
1138
  for (const item of installed) {
1125
1139
  if (!reopenTargets.has(item.target)) continue;