@phnx-labs/agents-cli 1.22.42 → 1.22.44

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 (256) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +15 -13
  3. package/dist/bootstrap.js +33 -13
  4. package/dist/cli/command-registry.d.ts +0 -8
  5. package/dist/cli/command-registry.js +1 -25
  6. package/dist/commands/alias.d.ts +7 -4
  7. package/dist/commands/alias.js +33 -8
  8. package/dist/commands/apply.d.ts +7 -10
  9. package/dist/commands/apply.js +15 -35
  10. package/dist/commands/artifacts.js +4 -5
  11. package/dist/commands/audit.d.ts +6 -6
  12. package/dist/commands/audit.js +12 -12
  13. package/dist/commands/auth.js +14 -6
  14. package/dist/commands/beta.d.ts +7 -1
  15. package/dist/commands/beta.js +16 -9
  16. package/dist/commands/commands.js +1 -1
  17. package/dist/commands/computer.js +20 -6
  18. package/dist/commands/daemon.js +1 -1
  19. package/dist/commands/doctor.d.ts +4 -2
  20. package/dist/commands/doctor.js +21 -9
  21. package/dist/commands/events.d.ts +2 -2
  22. package/dist/commands/events.js +6 -5
  23. package/dist/commands/exec.d.ts +1 -1
  24. package/dist/commands/exec.js +11 -6
  25. package/dist/commands/factory.d.ts +1 -1
  26. package/dist/commands/feed.js +1 -30
  27. package/dist/commands/fleet-capture.d.ts +1 -1
  28. package/dist/commands/fleet-capture.js +4 -5
  29. package/dist/commands/harness.js +4 -52
  30. package/dist/commands/hooks.js +1 -1
  31. package/dist/commands/insights.d.ts +1 -1
  32. package/dist/commands/insights.js +3 -2
  33. package/dist/commands/menubar.js +58 -1
  34. package/dist/commands/monitors.js +1 -1
  35. package/dist/commands/org.d.ts +5 -0
  36. package/dist/commands/org.js +83 -29
  37. package/dist/commands/output.js +2 -2
  38. package/dist/commands/profiles.js +2 -2
  39. package/dist/commands/prune.js +5 -4
  40. package/dist/commands/routines.js +43 -25
  41. package/dist/commands/routines.test-fixture.d.ts +86 -0
  42. package/dist/commands/routines.test-fixture.js +345 -0
  43. package/dist/commands/sessions-picker.js +124 -122
  44. package/dist/commands/sessions.test-fixture.d.ts +24 -0
  45. package/dist/commands/sessions.test-fixture.js +224 -0
  46. package/dist/commands/setup.js +9 -1
  47. package/dist/commands/share.d.ts +9 -10
  48. package/dist/commands/share.js +68 -50
  49. package/dist/commands/skills.js +1 -1
  50. package/dist/commands/snapshot.d.ts +1 -1
  51. package/dist/commands/snapshot.js +4 -4
  52. package/dist/commands/ssh.js +15 -62
  53. package/dist/commands/status.d.ts +4 -2
  54. package/dist/commands/status.js +10 -7
  55. package/dist/commands/subagents.js +1 -1
  56. package/dist/commands/sync.d.ts +1 -0
  57. package/dist/commands/sync.js +29 -2
  58. package/dist/commands/view.js +11 -2
  59. package/dist/commands/webhook.js +1 -1
  60. package/dist/lib/accounting/rotate.d.ts +5 -3
  61. package/dist/lib/accounting/rotate.js +36 -7
  62. package/dist/lib/accounting/usage.d.ts +18 -4
  63. package/dist/lib/accounting/usage.js +138 -27
  64. package/dist/lib/agent-spec/agents.d.ts +7 -7
  65. package/dist/lib/agent-spec/agents.js +15 -15
  66. package/dist/lib/analytics/mix-commands.d.ts +6 -28
  67. package/dist/lib/analytics/mix-commands.js +14 -40
  68. package/dist/lib/audit/log.d.ts +2 -2
  69. package/dist/lib/audit/log.js +2 -2
  70. package/dist/lib/beta.js +1 -1
  71. package/dist/lib/browser/domain-skills.d.ts +26 -6
  72. package/dist/lib/browser/domain-skills.js +81 -43
  73. package/dist/lib/cloud/rush.d.ts +0 -15
  74. package/dist/lib/cloud/rush.js +0 -35
  75. package/dist/lib/crabbox/runtimes.d.ts +6 -2
  76. package/dist/lib/crabbox/runtimes.js +21 -5
  77. package/dist/lib/daemon/daemon.js +97 -58
  78. package/dist/lib/daemon/daemon.test-fixture.d.ts +24 -0
  79. package/dist/lib/daemon/daemon.test-fixture.js +71 -0
  80. package/dist/lib/daemon/runner.d.ts +18 -1
  81. package/dist/lib/daemon/runner.js +105 -11
  82. package/dist/lib/daemon-ticks.d.ts +38 -1
  83. package/dist/lib/daemon-ticks.js +52 -5
  84. package/dist/lib/device-config.js +1 -2
  85. package/dist/lib/devices/discovery-policy.d.ts +3 -3
  86. package/dist/lib/devices/discovery-policy.js +3 -3
  87. package/dist/lib/devices/doctor-findings.js +1 -1
  88. package/dist/lib/devices/fleet.d.ts +4 -8
  89. package/dist/lib/devices/fleet.js +3 -15
  90. package/dist/lib/devices/health-report.js +4 -4
  91. package/dist/lib/devices/pool.d.ts +0 -6
  92. package/dist/lib/devices/pool.js +0 -6
  93. package/dist/lib/devices/registry.d.ts +0 -15
  94. package/dist/lib/devices/registry.js +0 -9
  95. package/dist/lib/devices/stats-cache.d.ts +12 -0
  96. package/dist/lib/devices/stats-cache.js +24 -5
  97. package/dist/lib/drift-sync.d.ts +1 -1
  98. package/dist/lib/drift-sync.js +2 -2
  99. package/dist/lib/drift.d.ts +1 -1
  100. package/dist/lib/exec.js +13 -3
  101. package/dist/lib/fleet/capture.d.ts +1 -1
  102. package/dist/lib/fleet/manifest.js +1 -1
  103. package/dist/lib/fleet/remote-login.d.ts +2 -2
  104. package/dist/lib/fleet/remote-login.js +3 -4
  105. package/dist/lib/format.d.ts +9 -0
  106. package/dist/lib/format.js +16 -0
  107. package/dist/lib/github/pr-mergeable.d.ts +49 -0
  108. package/dist/lib/github/pr-mergeable.js +157 -0
  109. package/dist/lib/github/pr-verdict.d.ts +67 -0
  110. package/dist/lib/github/pr-verdict.js +72 -0
  111. package/dist/lib/harness/adapter.d.ts +6 -5
  112. package/dist/lib/hosts/dispatch.d.ts +2 -0
  113. package/dist/lib/hosts/dispatch.js +8 -1
  114. package/dist/lib/hosts/passthrough.d.ts +10 -0
  115. package/dist/lib/hosts/passthrough.js +28 -7
  116. package/dist/lib/hosts/providers/devices.js +1 -13
  117. package/dist/lib/hosts/ready.d.ts +12 -3
  118. package/dist/lib/hosts/ready.js +27 -12
  119. package/dist/lib/hosts/registry.d.ts +4 -5
  120. package/dist/lib/hosts/registry.js +3 -7
  121. package/dist/lib/hosts/session-index.js +1 -1
  122. package/dist/lib/installations/shims.js +1 -1
  123. package/dist/lib/installations/versions.d.ts +40 -0
  124. package/dist/lib/installations/versions.js +78 -4
  125. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
  126. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  127. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
  128. package/dist/lib/menubar/install-menubar.d.ts +109 -0
  129. package/dist/lib/menubar/install-menubar.js +218 -12
  130. package/dist/lib/monitors/config.d.ts +2 -2
  131. package/dist/lib/monitors/config.js +3 -2
  132. package/dist/lib/origin-machine.d.ts +18 -0
  133. package/dist/lib/origin-machine.js +34 -0
  134. package/dist/lib/perf/db.js +1 -1
  135. package/dist/lib/perf/spool.js +1 -1
  136. package/dist/lib/prix-account.d.ts +2 -1
  137. package/dist/lib/prix-account.js +2 -1
  138. package/dist/lib/profiles.d.ts +7 -0
  139. package/dist/lib/profiles.js +26 -1
  140. package/dist/lib/remote-agents-json.js +1 -7
  141. package/dist/lib/routines-placement.d.ts +1 -1
  142. package/dist/lib/routines-placement.js +1 -1
  143. package/dist/lib/sandbox.d.ts +24 -0
  144. package/dist/lib/sandbox.js +101 -0
  145. package/dist/lib/scheduling/routines.d.ts +18 -9
  146. package/dist/lib/scheduling/routines.js +16 -8
  147. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  148. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
  149. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  150. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
  151. package/dist/lib/secrets/agent.js +9 -1
  152. package/dist/lib/secrets/index.js +1 -1
  153. package/dist/lib/secrets/list-filter.js +1 -1
  154. package/dist/lib/service-manifest.d.ts +20 -0
  155. package/dist/lib/service-manifest.js +34 -0
  156. package/dist/lib/session/active.d.ts +4 -16
  157. package/dist/lib/session/active.js +2 -6
  158. package/dist/lib/session/cloud.js +1 -1
  159. package/dist/lib/session/db.d.ts +13 -98
  160. package/dist/lib/session/db.js +5 -12
  161. package/dist/lib/session/discover.d.ts +5 -81
  162. package/dist/lib/session/discover.js +5 -5
  163. package/dist/lib/session/fork.js +1 -1
  164. package/dist/lib/session/live-metadata.js +1 -1
  165. package/dist/lib/session/origin-machine.d.ts +1 -18
  166. package/dist/lib/session/origin-machine.js +4 -34
  167. package/dist/lib/session/relative-time.d.ts +1 -44
  168. package/dist/lib/session/relative-time.js +4 -120
  169. package/dist/lib/session/remote/remote-bundle.d.ts +12 -0
  170. package/dist/lib/session/remote/remote-bundle.js +61 -0
  171. package/dist/lib/session/remote/remote-list.d.ts +148 -0
  172. package/dist/lib/session/remote/remote-list.js +607 -0
  173. package/dist/lib/session/remote/remote.d.ts +132 -0
  174. package/dist/lib/session/remote/remote.js +314 -0
  175. package/dist/lib/session/remote/watch.d.ts +101 -0
  176. package/dist/lib/session/remote/watch.js +241 -0
  177. package/dist/lib/session/remote-bundle.d.ts +1 -12
  178. package/dist/lib/session/remote-bundle.js +3 -61
  179. package/dist/lib/session/remote-list.d.ts +1 -148
  180. package/dist/lib/session/remote-list.js +3 -607
  181. package/dist/lib/session/remote.d.ts +1 -132
  182. package/dist/lib/session/remote.js +3 -314
  183. package/dist/lib/session/short-id.d.ts +1 -17
  184. package/dist/lib/session/short-id.js +4 -20
  185. package/dist/lib/session/watch.d.ts +1 -101
  186. package/dist/lib/session/watch.js +3 -242
  187. package/dist/lib/session/width.d.ts +1 -29
  188. package/dist/lib/session/width.js +4 -101
  189. package/dist/lib/share/config.d.ts +12 -2
  190. package/dist/lib/share/config.js +47 -11
  191. package/dist/lib/share/delete.js +1 -1
  192. package/dist/lib/share/publish.js +1 -1
  193. package/dist/lib/share/worker-template.js +2 -2
  194. package/dist/lib/smart-launch.d.ts +4 -4
  195. package/dist/lib/smart-launch.js +8 -18
  196. package/dist/lib/snapshot.d.ts +2 -2
  197. package/dist/lib/snapshot.js +1 -1
  198. package/dist/lib/staleness/index.d.ts +3 -1
  199. package/dist/lib/staleness/index.js +21 -1
  200. package/dist/lib/staleness/types.d.ts +11 -0
  201. package/dist/lib/staleness/writers/commands.js +13 -4
  202. package/dist/lib/staleness/writers/hooks.js +3 -1
  203. package/dist/lib/staleness/writers/mcp.d.ts +0 -7
  204. package/dist/lib/staleness/writers/mcp.js +16 -1
  205. package/dist/lib/staleness/writers/rules.js +1 -1
  206. package/dist/lib/staleness/writers/skills.js +3 -1
  207. package/dist/lib/staleness/writers/subagents.d.ts +0 -9
  208. package/dist/lib/staleness/writers/subagents.js +18 -1
  209. package/dist/lib/staleness/writers/types.d.ts +10 -0
  210. package/dist/lib/startup/command-registry.d.ts +10 -1
  211. package/dist/lib/startup/command-registry.js +26 -7
  212. package/dist/lib/startup/root-command.d.ts +18 -1
  213. package/dist/lib/startup/root-command.js +18 -1
  214. package/dist/lib/state.js +7 -0
  215. package/dist/lib/teams/placement-probe.js +28 -13
  216. package/dist/lib/teams/scheduler.d.ts +3 -1
  217. package/dist/lib/teams/scheduler.js +1 -1
  218. package/dist/lib/text/relative-time.d.ts +44 -0
  219. package/dist/lib/text/relative-time.js +120 -0
  220. package/dist/lib/text/short-id.d.ts +17 -0
  221. package/dist/lib/text/short-id.js +20 -0
  222. package/dist/lib/text/width.d.ts +29 -0
  223. package/dist/lib/text/width.js +101 -0
  224. package/dist/lib/triggers/handlers.d.ts +7 -0
  225. package/dist/lib/triggers/handlers.js +8 -0
  226. package/dist/lib/triggers/webhook.d.ts +1 -0
  227. package/dist/lib/triggers/webhook.js +2 -1
  228. package/dist/lib/types.d.ts +1 -1
  229. package/dist/lib/view-types.d.ts +7 -0
  230. package/package.json +2 -3
  231. package/dist/bin/agents +0 -0
  232. package/dist/commands/serve.d.ts +0 -10
  233. package/dist/commands/serve.js +0 -68
  234. package/dist/commands/tickets.d.ts +0 -2
  235. package/dist/commands/tickets.js +0 -43
  236. package/dist/commands/trends.d.ts +0 -10
  237. package/dist/commands/trends.js +0 -12
  238. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  239. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  240. package/dist/lib/observe-aliases.d.ts +0 -30
  241. package/dist/lib/observe-aliases.js +0 -38
  242. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  243. package/dist/lib/serve/control.d.ts +0 -95
  244. package/dist/lib/serve/control.js +0 -260
  245. package/dist/lib/serve/data.d.ts +0 -81
  246. package/dist/lib/serve/data.js +0 -91
  247. package/dist/lib/serve/page.d.ts +0 -7
  248. package/dist/lib/serve/page.js +0 -140
  249. package/dist/lib/serve/server.d.ts +0 -80
  250. package/dist/lib/serve/server.js +0 -145
  251. package/dist/lib/serve/stream.d.ts +0 -43
  252. package/dist/lib/serve/stream.js +0 -116
  253. package/dist/lib/serve/token.d.ts +0 -35
  254. package/dist/lib/serve/token.js +0 -85
  255. package/dist/lib/tickets/list.d.ts +0 -53
  256. package/dist/lib/tickets/list.js +0 -153
@@ -24,7 +24,7 @@ import { normalizeHost } from '../machine-id.js';
24
24
  import { readMeta } from '../state.js';
25
25
  import { isSshConfigHost } from './ssh-config.js';
26
26
  import { resolveRemoteOsSync } from './remote-os.js';
27
- import { loadDevices, isControlDevice } from '../devices/registry.js';
27
+ import { loadDevices } from '../devices/registry.js';
28
28
  import { resolveDeviceProfile } from '../devices/resolve-profile.js';
29
29
  import { isDeviceAuto, resolveDeviceAffinity } from '../smart-launch.js';
30
30
  import { localMachineId } from '../session/origin-machine.js';
@@ -148,7 +148,7 @@ function literalHost(token, host, user) {
148
148
  * all resolve the same way regardless of which subcommand called.
149
149
  *
150
150
  * Non-throwing: returns null on an injection-guard failure or an unresolved bare
151
- * token. Device-only refusals (control device, password auth) are NOT applied
151
+ * token. The device-only refusal (password auth) is NOT applied
152
152
  * here — that is the dispatch wrapper's job ({@link resolveHost}), so the fan-out
153
153
  * and `agents ssh` paths, which handle those cases differently, aren't forced
154
154
  * into a dispatch verdict.
@@ -240,8 +240,7 @@ export async function listAllHosts() {
240
240
  * caller consumes (`run --device`, the generic passthrough, teams placement, the
241
241
  * cloud host provider, doctor, funnel, remote secrets). Grammar and merge come
242
242
  * from {@link matchHost}; on top, this layer applies the device-only dispatch
243
- * refusals so they hold even when an inline overlay shadows the device:
244
- * - a control device (a paired iPhone) can never be a dispatch target;
243
+ * refusal so it holds even when an inline overlay shadows the device:
245
244
  * - a password-auth device throws the typed {@link DeviceOffloadUnsupportedError}
246
245
  * (offload rides `BatchMode=yes` ssh, which can't answer a prompt);
247
246
  * - an addressless device has nothing to dial.
@@ -254,9 +253,6 @@ export async function resolveHost(name) {
254
253
  if (!host)
255
254
  return null;
256
255
  if (host.device) {
257
- if (isControlDevice(host.device)) {
258
- throw new Error(`Device "${host.device.name}" is a control device (a cockpit), not an executor — it can't run agents. Dispatch to a worker device instead.`);
259
- }
260
256
  if (host.device.auth.method === 'password') {
261
257
  throw new DeviceOffloadUnsupportedError(host.device.name);
262
258
  }
@@ -21,7 +21,7 @@ import { upsertSession } from '../session/db.js';
21
21
  import { isSessionTrackedAgent } from '../session/types.js';
22
22
  import { localLogPath, updateTask } from './tasks.js';
23
23
  import { parseSessionIdMarker } from './session-marker.js';
24
- import { deriveShortId } from '../session/short-id.js';
24
+ import { deriveShortId } from '../text/short-id.js';
25
25
  import { normalizeHost } from '../machine-id.js';
26
26
  /**
27
27
  * Build the SessionMeta for a host-dispatched run. Returns null when the run has
@@ -2015,7 +2015,7 @@ export function pruneOrphanedCommandShim(fileName) {
2015
2015
  return false;
2016
2016
  }
2017
2017
  if (content.includes('# Alias shim:'))
2018
- return false; // a user `agents alias` — leave it
2018
+ return false; // a user `agents setup alias` shim — leave it
2019
2019
  const bin = readAgentsBinFromShim(shimPath);
2020
2020
  if (!bin)
2021
2021
  return false; // not an AGENTS_BIN-baked shim
@@ -230,6 +230,46 @@ export declare function printTrashFooter(moved: Array<{
230
230
  * containing only `home/`) remain under the agent dir.
231
231
  */
232
232
  export declare function removeAllVersions(agent: AgentId): number;
233
+ export interface HealedVersionPointers {
234
+ /** Global default reassigned off a not-installed version (`to: null` = cleared). */
235
+ globalDefault?: {
236
+ from: string;
237
+ to: string | null;
238
+ };
239
+ /** Isolated default (bare `agents run <agent>`) reassigned off a not-installed version. */
240
+ isolatedDefault?: {
241
+ from: string;
242
+ to: string | null;
243
+ };
244
+ /** `~/.<agent>` config symlink repointed off a not-installed version. */
245
+ configSymlink?: {
246
+ from: string;
247
+ to: string;
248
+ };
249
+ }
250
+ /**
251
+ * Repoint an agent's version pointers — the global/isolated default and the
252
+ * `~/.<agent>` config symlink — off any version that is no longer installed, so
253
+ * neither `agents sync`/`agents run` resolution nor the conventional-path home
254
+ * can land on a dead version (RUSH-2471).
255
+ *
256
+ * How a pointer goes dangling: {@link removeVersion} reassigns the defaults and
257
+ * clears the config symlink only when IT removes the pointed-at version. A
258
+ * version-home whose launch binary disappears by any other route — grok
259
+ * self-updated its per-version binary out from under the old dir, a manual
260
+ * delete, a half-finished install that seeded the home but never landed the
261
+ * binary — leaves the default and/or the symlink pointing at something
262
+ * {@link isVersionInstalled} reports as gone.
263
+ *
264
+ * Called from the sync resolve path so the invariant self-heals. Mirrors
265
+ * {@link removeVersion}'s reassignment: the global default prefers the newest
266
+ * NON-isolated survivor else clears; the isolated default prefers the newest
267
+ * remaining isolated copy else clears. The symlink is repointed at the now-healed
268
+ * resolved default, else the newest installed non-isolated version. A symlink
269
+ * already on an installed version, a real (non-symlink) config dir, and an
270
+ * isolated-only agent are left untouched.
271
+ */
272
+ export declare function healDanglingVersionPointers(agent: AgentId, cwd: string): Promise<HealedVersionPointers>;
233
273
  /**
234
274
  * Normalize a user-supplied @version token across CLI subcommands.
235
275
  *
@@ -32,9 +32,9 @@ import { VERSION_RE, compareVersions } from '../agent-spec/primitives.js';
32
32
  import { AGENTS, agentConfigDirName, getAccountEmail, resolveAgentName, formatAgentError, findInPath, isSelfUpdatingAgent, isAgentHardDeprecated, hardDeprecationError } from '../agents.js';
33
33
  import { discoverPermissionGroups, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from '../permissions.js';
34
34
  import { parseMcpConfigForScan, isProjectMcpTrusted } from '../mcp.js';
35
- import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink, assertIsolationBoundary, } from './shims.js';
35
+ import { createVersionedAlias, removeVersionedAlias, switchConfigSymlink, getConfigSymlinkVersion, ensureClaudeInsideSymlink, assertIsolationBoundary, isIsolationProtected, } from './shims.js';
36
36
  import { importInstallScriptBinary } from '../import.js';
37
- import { createInstallation, getBinaryPath, getCliVersionFromPath, getGlobalDefault, getIsolatedDefault, getLiveVersion, getVersionDir, getVersionHomePath, invalidateInstalledVersionsCache, invalidateLiveVersionCache, isGlobalBinaryAgent, isVersionInstalled, isVersionIsolated, listInstalledVersions, pickCanonicalGlobalBinaryVersion, } from './store.js';
37
+ import { createInstallation, getBinaryPath, getCliVersionFromPath, getGlobalDefault, getIsolatedDefault, getLiveVersion, getVersionDir, getVersionHomePath, invalidateInstalledVersionsCache, invalidateLiveVersionCache, isGlobalBinaryAgent, isVersionInstalled, isVersionIsolated, listInstalledVersions, pickCanonicalGlobalBinaryVersion, resolveVersion, } from './store.js';
38
38
  export * from './store.js';
39
39
  import { INSTALLATION_RECORD_FILE } from './types.js';
40
40
  import { composeWin32CommandLine } from '../platform/index.js';
@@ -1745,6 +1745,61 @@ export function removeAllVersions(agent) {
1745
1745
  }
1746
1746
  return removed;
1747
1747
  }
1748
+ /**
1749
+ * Repoint an agent's version pointers — the global/isolated default and the
1750
+ * `~/.<agent>` config symlink — off any version that is no longer installed, so
1751
+ * neither `agents sync`/`agents run` resolution nor the conventional-path home
1752
+ * can land on a dead version (RUSH-2471).
1753
+ *
1754
+ * How a pointer goes dangling: {@link removeVersion} reassigns the defaults and
1755
+ * clears the config symlink only when IT removes the pointed-at version. A
1756
+ * version-home whose launch binary disappears by any other route — grok
1757
+ * self-updated its per-version binary out from under the old dir, a manual
1758
+ * delete, a half-finished install that seeded the home but never landed the
1759
+ * binary — leaves the default and/or the symlink pointing at something
1760
+ * {@link isVersionInstalled} reports as gone.
1761
+ *
1762
+ * Called from the sync resolve path so the invariant self-heals. Mirrors
1763
+ * {@link removeVersion}'s reassignment: the global default prefers the newest
1764
+ * NON-isolated survivor else clears; the isolated default prefers the newest
1765
+ * remaining isolated copy else clears. The symlink is repointed at the now-healed
1766
+ * resolved default, else the newest installed non-isolated version. A symlink
1767
+ * already on an installed version, a real (non-symlink) config dir, and an
1768
+ * isolated-only agent are left untouched.
1769
+ */
1770
+ export async function healDanglingVersionPointers(agent, cwd) {
1771
+ const healed = {};
1772
+ const installed = listInstalledVersions(agent);
1773
+ const globalDefault = getGlobalDefault(agent);
1774
+ if (globalDefault !== null && !isVersionInstalled(agent, globalDefault)) {
1775
+ const promotable = installed.filter((v) => !isVersionIsolated(agent, v));
1776
+ const to = promotable.length > 0 ? promotable[promotable.length - 1] : undefined;
1777
+ setGlobalDefault(agent, to);
1778
+ healed.globalDefault = { from: globalDefault, to: to ?? null };
1779
+ }
1780
+ const isolatedDefault = getIsolatedDefault(agent);
1781
+ if (isolatedDefault !== null && !isVersionInstalled(agent, isolatedDefault)) {
1782
+ const survivors = installed.filter((v) => isVersionIsolated(agent, v));
1783
+ const to = survivors.length > 0 ? survivors[survivors.length - 1] : undefined;
1784
+ setIsolatedDefault(agent, to);
1785
+ healed.isolatedDefault = { from: isolatedDefault, to: to ?? null };
1786
+ }
1787
+ const current = getConfigSymlinkVersion(agent);
1788
+ const nonIsolated = installed.filter((v) => !isVersionIsolated(agent, v));
1789
+ if (current !== null &&
1790
+ !isVersionInstalled(agent, current) &&
1791
+ !isIsolationProtected(agent) &&
1792
+ nonIsolated.length > 0) {
1793
+ const pinned = resolveVersion(agent, cwd);
1794
+ const target = pinned && isVersionInstalled(agent, pinned) && !isVersionIsolated(agent, pinned)
1795
+ ? pinned
1796
+ : nonIsolated[nonIsolated.length - 1];
1797
+ const result = await switchConfigSymlink(agent, target);
1798
+ if (result.success)
1799
+ healed.configSymlink = { from: current, to: target };
1800
+ }
1801
+ return healed;
1802
+ }
1748
1803
  /**
1749
1804
  * Normalize a user-supplied @version token across CLI subcommands.
1750
1805
  *
@@ -2436,6 +2491,10 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2436
2491
  const commandsAlsoAsSkills = shouldAlsoInstallCommandAsSkill(agent, version);
2437
2492
  const commandsInstallAsSkills = commandsAsSkills || commandsAlsoAsSkills;
2438
2493
  let writtenCommands = [];
2494
+ // Artifact paths the writers report (WriteResult.paths), persisted to the
2495
+ // manifest as `writtenTargets` after a full sync so isStale can flag a
2496
+ // deleted artifact as stale (#2398).
2497
+ const writtenTargets = [];
2439
2498
  if (commandsToSync.length > 0 && commandsWriter) {
2440
2499
  // Agents that replace native command files with skills (codex >= 0.117.0,
2441
2500
  // kimi) must have their legacy command dir removed, or files written by an
@@ -2448,6 +2507,8 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2448
2507
  }
2449
2508
  const r = commandsWriter.write({ version, versionHome, selection: commandsToSync, cwd });
2450
2509
  writtenCommands = r.synced;
2510
+ if (r.paths)
2511
+ writtenTargets.push(...r.paths);
2451
2512
  result.commands = r.synced.length > 0;
2452
2513
  }
2453
2514
  // Orphan-sweep stale top-level command files from previous syncs under a
@@ -2455,8 +2516,9 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2455
2516
  // not pass an explicit `selection`. Callers that pass explicit selections
2456
2517
  // are using the incremental/additive API (sync exactly these; leave others
2457
2518
  // alone), so the sweep would be a contract violation there. The
2458
- // cross-project leak always comes from the no-selection shim auto-sync at
2459
- // launch.
2519
+ // cross-project leak comes from no-selection full syncs run under different
2520
+ // cwds (`agents sync` / `agents refresh`; the shim's `agents sync --launch`
2521
+ // skips version-home reconciliation and never reaches this path).
2460
2522
  if (!userPassedSelection && commandsWriter && !shouldInstallCommandAsSkill(agent, version)) {
2461
2523
  const commandsTargetSweep = path.join(agentDir, agentConfig.commandsSubdir);
2462
2524
  if (fs.existsSync(commandsTargetSweep)) {
@@ -2515,6 +2577,8 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2515
2577
  else if (skillsWriter) {
2516
2578
  if (skillsToSync.length > 0) {
2517
2579
  const r = skillsWriter.write({ version, versionHome, selection: skillsToSync, cwd });
2580
+ if (r.paths)
2581
+ writtenTargets.push(...r.paths);
2518
2582
  result.skills = r.synced.length > 0;
2519
2583
  }
2520
2584
  // Orphan-sweep stale skill directories from previous syncs under a
@@ -2570,6 +2634,8 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2570
2634
  }
2571
2635
  }
2572
2636
  }
2637
+ if (r.paths)
2638
+ writtenTargets.push(...r.paths);
2573
2639
  result.hooks = r.synced.length > 0;
2574
2640
  hookManifest = selectHookManifest(parseHookManifest(), hooksToSync);
2575
2641
  }
@@ -2596,6 +2662,8 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2596
2662
  : null;
2597
2663
  const preset = overridePreset || activeRulesPreset() || getActiveRulesPreset(agent, version);
2598
2664
  const r = rulesWriter.write({ version, versionHome, selection: { preset }, cwd });
2665
+ if (r.paths)
2666
+ writtenTargets.push(...r.paths);
2599
2667
  result.memory.push(...r.synced);
2600
2668
  // rulesPreset is tracked separately via setActiveRulesPreset.
2601
2669
  }
@@ -2673,6 +2741,8 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2673
2741
  const mcpToSync = mcpToSyncAll.filter(n => !untrustedProjectMcpNames.has(n));
2674
2742
  if (mcpToSync.length > 0 && mcpWriter) {
2675
2743
  const r = mcpWriter.write({ version, versionHome, selection: mcpToSync, cwd });
2744
+ if (r.paths)
2745
+ writtenTargets.push(...r.paths);
2676
2746
  result.mcp = r.synced;
2677
2747
  if (r.errors?.length)
2678
2748
  result.declined.push(...r.errors.map((e) => `mcp: ${e}`));
@@ -2694,6 +2764,8 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2694
2764
  }
2695
2765
  if (subagentsToSync.length > 0 && subagentsWriter) {
2696
2766
  const r = subagentsWriter.write({ version, versionHome, selection: subagentsToSync, cwd });
2767
+ if (r.paths)
2768
+ writtenTargets.push(...r.paths);
2697
2769
  result.subagents.push(...r.synced);
2698
2770
  // Orphan-sweep for Claude only — see comment on commands/skills sweep
2699
2771
  // for the no-selection guard. OpenClaw stores subagents as siblings of
@@ -2800,6 +2872,8 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2800
2872
  const previous = loadManifest(agent, version);
2801
2873
  const manifest = buildSyncManifest(agent, version, cwd, previous);
2802
2874
  manifest.writtenCommands = writtenCommands;
2875
+ // Deduped + sorted so repeated full syncs write byte-identical manifests.
2876
+ manifest.writtenTargets = Array.from(new Set(writtenTargets)).sort();
2803
2877
  saveManifest(agent, version, manifest);
2804
2878
  }
2805
2879
  return result;
@@ -7,11 +7,7 @@
7
7
  <key>CFBundleIdentifier</key>
8
8
  <string>com.phnx-labs.agents-menubar</string>
9
9
  <key>CFBundleName</key>
10
- <string>AGI Menu</string>
11
- <key>CFBundleDisplayName</key>
12
- <string>AGI Menu</string>
13
- <key>CFBundleIconFile</key>
14
- <string>AppIcon</string>
10
+ <string>Agents Menu Bar</string>
15
11
  <key>CFBundlePackageType</key>
16
12
  <string>APPL</string>
17
13
  <key>CFBundleShortVersionString</key>
@@ -3,22 +3,9 @@
3
3
  <plist version="1.0">
4
4
  <dict>
5
5
  <key>files</key>
6
- <dict>
7
- <key>Resources/AppIcon.icns</key>
8
- <data>
9
- jOjZVimcFRHoP2VPzgn8uM8mjkA=
10
- </data>
11
- </dict>
6
+ <dict/>
12
7
  <key>files2</key>
13
- <dict>
14
- <key>Resources/AppIcon.icns</key>
15
- <dict>
16
- <key>hash2</key>
17
- <data>
18
- GFvSLeNYJ3ASxW3OzcuiB4aID0gZUBkpozmWZkbTFNw=
19
- </data>
20
- </dict>
21
- </dict>
8
+ <dict/>
22
9
  <key>rules</key>
23
10
  <dict>
24
11
  <key>^Resources/</key>
@@ -75,6 +75,33 @@ export declare function generateServicePlist(execPath: string): string;
75
75
  export declare function restartMenubarLaunchAgent(uid: number, plist: string, exec?: (cmd: string, args: readonly string[], opts: {
76
76
  stdio: ['ignore', 'ignore', 'ignore'];
77
77
  }) => Buffer): void;
78
+ /**
79
+ * Force a running helper off the binary that was just swapped underneath it.
80
+ *
81
+ * `installMenubarLaunchAgentOnUpgrade()` calls this after a heal actually
82
+ * replaces the on-disk bundle (a version bump or the ad-hoc -> Developer ID
83
+ * transition — see `menubarHealReplacedBundle`). `restartMenubarLaunchAgent`
84
+ * (run moments earlier via `installAndStartService`) already attempts
85
+ * bootout+bootstrap+kickstart, but that targets the GUI launchd domain and
86
+ * fails silently — `launchctl kickstart -k gui/<uid>/<label>` prints "Could not
87
+ * find service ... in domain for user gui" — from a shell with no Aqua session,
88
+ * which is the ordinary case for `agents` invoked from a terminal/tmux/ssh
89
+ * session rather than literally the login item (verified live). When that
90
+ * happens the plist and on-disk bundle are current but the LIVE process is
91
+ * still the old binary, so it requests Accessibility under the OLD code
92
+ * identity and the grant never sticks.
93
+ *
94
+ * `kickstart -k` (force-restart, unlike the bare kickstart already tried) is
95
+ * attempted first since it is the clean launchd-native path when it works. If
96
+ * launchd has no record of the service in this context either, fall back to
97
+ * ending the specific pid(s) already confirmed to be running the installed
98
+ * bundle (`ownProcesses`, resolved by the caller via `liveMenubarProcesses`) —
99
+ * never a broad pkill. The launchd plist has `KeepAlive=true`, so the ended
100
+ * process is relaunched from the swapped binary within seconds.
101
+ */
102
+ export declare function restartMenubarHelperAfterSwap(uid: number, ownProcesses: readonly MenubarProcess[], exec?: (cmd: string, args: readonly string[], opts: {
103
+ stdio: ['ignore', 'ignore', 'ignore'];
104
+ }) => Buffer, kill?: (pid: number) => void): void;
78
105
  /**
79
106
  * Install + start the menu-bar helper as a launchd user service (idempotent).
80
107
  * Clears the sticky opt-out, installs the .app, writes the plist, and
@@ -97,6 +124,23 @@ export declare function isMenubarStale(opts: {
97
124
  currentVersion: string;
98
125
  execExists: boolean;
99
126
  }): boolean;
127
+ /**
128
+ * Pure decision (no I/O): did THIS heal actually replace bundle content a live
129
+ * process could be running stale, as opposed to a plist-only repoint?
130
+ *
131
+ * `stale` (version bump or missing exec) and `needsDevIdHeal` (ad-hoc ->
132
+ * Developer ID) both mean the shipped `.app` differs from what is already
133
+ * installed — the exact condition under which a running helper can be left on
134
+ * an old code identity after `enableMenubarService` swaps the bundle. A
135
+ * repoint-only heal (`menubarSetupNeedsRepoint()` alone, same version, same
136
+ * signing identity) does NOT count: RUSH-3005 already owns the churn from
137
+ * mixed-Node-interpreter repoints, and restarting the helper on every one of
138
+ * those would double that churn rather than fix this bug.
139
+ */
140
+ export declare function menubarHealReplacedBundle(opts: {
141
+ stale: boolean;
142
+ needsDevIdHeal: boolean;
143
+ }): boolean;
100
144
  /**
101
145
  * Pure re-point decision (no I/O): the plist's baked interpreter/entry no longer
102
146
  * match the install that is now running `agents`. This catches DUAL-INSTALL skew
@@ -110,6 +154,7 @@ export declare function isMenubarStale(opts: {
110
154
  export declare function menubarPlistNeedsRepoint(opts: {
111
155
  plistEntry: string | null;
112
156
  plistNode: string | null;
157
+ plistNodeExists: boolean;
113
158
  activeEntry: string | null;
114
159
  activeNode: string | null;
115
160
  }): boolean;
@@ -185,6 +230,31 @@ export declare function mayInstallMenubarHelper(opts: {
185
230
  * throws into startup.
186
231
  */
187
232
  export declare function installMenubarLaunchAgentOnUpgrade(): void;
233
+ /**
234
+ * Pure decision (no I/O): run the one-time `tccutil reset Accessibility` only
235
+ * on a real ad-hoc -> Developer ID transition, and only once ever. A machine
236
+ * that was always Developer ID never accumulated a dead TCC row under an
237
+ * ad-hoc identity, so resetting there would just force a needless re-prompt;
238
+ * a machine that already migrated must not be reset again on every heal.
239
+ */
240
+ export declare function shouldMigrateMenubarTcc(opts: {
241
+ needsDevIdHeal: boolean;
242
+ alreadyMigrated: boolean;
243
+ }): boolean;
244
+ /**
245
+ * Reset the stale Accessibility grant TCC recorded against the pre-migration
246
+ * ad-hoc identity, then stamp the marker so this never runs again on this
247
+ * machine. Targets `SERVICE_LABEL_BASE` — the bundle's actual
248
+ * `CFBundleIdentifier`/codesign `--identifier` (menubar/scripts/build.sh) that
249
+ * TCC keys grants to — not the namespaced `serviceLabel()`, which exists only
250
+ * to keep launchd job labels apart under a redirected HOME (RUSH-2639) and is
251
+ * never the app's real identity. Best-effort: a missing/failing `tccutil`
252
+ * (older macOS, a sandboxed test HOME) must never block the heal that just
253
+ * fixed the signing identity itself.
254
+ */
255
+ export declare function resetMenubarAccessibilityTcc(exec?: (cmd: string, args: readonly string[], opts: {
256
+ stdio: ['ignore', 'ignore', 'ignore'];
257
+ }) => Buffer): void;
188
258
  /** One step of `agents menubar setup`, and how it came out. */
189
259
  export interface SetupStep {
190
260
  /** What was configured. */
@@ -272,3 +342,42 @@ export interface MenubarStatus {
272
342
  disabledByUser: boolean;
273
343
  }
274
344
  export declare function getMenubarStatus(): MenubarStatus;
345
+ /** Read-only diagnostic for `agents menubar doctor` — probes, never mutates. */
346
+ export interface MenubarDoctorReport {
347
+ platform: string;
348
+ installPath: string | null;
349
+ installedVersion: string | null;
350
+ currentVersion: string;
351
+ versionMatches: boolean;
352
+ /** `unknown` on non-darwin or when nothing is installed to inspect. */
353
+ signingIdentity: 'developer-id' | 'ad-hoc' | 'unknown';
354
+ running: boolean;
355
+ /** A live helper pid started before the installed bundle's on-disk mtime —
356
+ * it is running the binary that has since been swapped underneath it. */
357
+ staleRunningProcess: PidStaleness[];
358
+ /** Grant likely needs to be re-made: an ad-hoc identity breaks on every
359
+ * update, or a live process is confirmed to predate the current bundle. */
360
+ accessibilityHintNeeded: boolean;
361
+ }
362
+ /** One live pid checked against the bundle's on-disk mtime. */
363
+ export interface PidStaleness {
364
+ pid: number;
365
+ stale: boolean;
366
+ }
367
+ /**
368
+ * Pure comparison (no I/O) behind the stale-process check: a helper pid that
369
+ * started before the bundle on disk was last written is still running the
370
+ * PREVIOUS binary — the exact condition `restartMenubarHelperAfterSwap` exists
371
+ * to fix. Millisecond epoch timestamps in, so the truth table is unit-testable
372
+ * without a live process or filesystem.
373
+ */
374
+ export declare function isMenubarProcessStaleAgainstBundle(pidStartedAtMs: number, bundleMtimeMs: number): boolean;
375
+ /**
376
+ * `agents menubar doctor` — everything a human needs to tell "why did
377
+ * Accessibility ask again" apart from "the helper is genuinely broken":
378
+ * install path, version skew, whether the signing identity is update-stable,
379
+ * and whether a live process predates the bundle it's supposed to be running.
380
+ * Read-only: no install, no restart, no TCC reset — `agents menubar setup`
381
+ * remains the command that fixes what this reports.
382
+ */
383
+ export declare function buildMenubarDoctorReport(): MenubarDoctorReport;