@phnx-labs/agents-cli 1.20.78 → 1.20.83

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 (183) hide show
  1. package/CHANGELOG.md +595 -0
  2. package/README.md +26 -26
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +9 -0
  5. package/dist/commands/activity.js +153 -24
  6. package/dist/commands/apply.js +11 -4
  7. package/dist/commands/cli.js +1 -1
  8. package/dist/commands/commands.d.ts +1 -1
  9. package/dist/commands/commands.js +7 -6
  10. package/dist/commands/defaults.js +3 -11
  11. package/dist/commands/doctor.d.ts +66 -0
  12. package/dist/commands/doctor.js +596 -33
  13. package/dist/commands/events.js +1 -1
  14. package/dist/commands/exec.js +24 -5
  15. package/dist/commands/feed.d.ts +4 -0
  16. package/dist/commands/feed.js +87 -11
  17. package/dist/commands/harness.js +2 -2
  18. package/dist/commands/hooks.js +1 -1
  19. package/dist/commands/import.js +8 -4
  20. package/dist/commands/logs.js +6 -0
  21. package/dist/commands/menubar.js +12 -0
  22. package/dist/commands/models.js +1 -1
  23. package/dist/commands/packages.js +6 -6
  24. package/dist/commands/permissions.js +1 -1
  25. package/dist/commands/profiles.js +1 -1
  26. package/dist/commands/pull.d.ts +1 -1
  27. package/dist/commands/pull.js +4 -4
  28. package/dist/commands/repo.d.ts +78 -0
  29. package/dist/commands/repo.js +187 -11
  30. package/dist/commands/routines.d.ts +8 -1
  31. package/dist/commands/routines.js +241 -77
  32. package/dist/commands/rules.js +13 -12
  33. package/dist/commands/secrets.d.ts +3 -1
  34. package/dist/commands/secrets.js +100 -32
  35. package/dist/commands/sessions-inject.d.ts +7 -4
  36. package/dist/commands/sessions-inject.js +18 -18
  37. package/dist/commands/sessions-picker.js +32 -3
  38. package/dist/commands/sessions-resume.d.ts +16 -0
  39. package/dist/commands/sessions-resume.js +14 -13
  40. package/dist/commands/sessions.d.ts +115 -0
  41. package/dist/commands/sessions.js +189 -35
  42. package/dist/commands/set.d.ts +15 -0
  43. package/dist/commands/set.js +79 -0
  44. package/dist/commands/setup-fleet.d.ts +21 -0
  45. package/dist/commands/setup-fleet.js +201 -0
  46. package/dist/commands/setup-secrets.d.ts +20 -0
  47. package/dist/commands/setup-secrets.js +223 -0
  48. package/dist/commands/setup.js +17 -3
  49. package/dist/commands/skills.js +1 -1
  50. package/dist/commands/sync.js +7 -2
  51. package/dist/commands/teams-picker.js +0 -1
  52. package/dist/commands/teams.d.ts +33 -1
  53. package/dist/commands/teams.js +208 -44
  54. package/dist/commands/versions.js +10 -5
  55. package/dist/commands/view.d.ts +1 -0
  56. package/dist/commands/view.js +11 -0
  57. package/dist/index.js +52 -8
  58. package/dist/lib/acp/harnesses.js +0 -7
  59. package/dist/lib/activity.d.ts +156 -0
  60. package/dist/lib/activity.js +282 -0
  61. package/dist/lib/agents.d.ts +9 -2
  62. package/dist/lib/agents.js +72 -63
  63. package/dist/lib/browser/service.js +3 -0
  64. package/dist/lib/browser/types.d.ts +7 -0
  65. package/dist/lib/capabilities.js +6 -2
  66. package/dist/lib/crabbox/lease.js +2 -2
  67. package/dist/lib/crabbox/setup-copy.d.ts +4 -4
  68. package/dist/lib/crabbox/setup-copy.js +4 -4
  69. package/dist/lib/daemon.d.ts +18 -0
  70. package/dist/lib/daemon.js +47 -6
  71. package/dist/lib/devices/fleet.d.ts +1 -1
  72. package/dist/lib/devices/fleet.js +1 -1
  73. package/dist/lib/doctor-diff.d.ts +20 -0
  74. package/dist/lib/doctor-diff.js +6 -1
  75. package/dist/lib/drift.d.ts +31 -11
  76. package/dist/lib/drift.js +58 -7
  77. package/dist/lib/events.d.ts +8 -1
  78. package/dist/lib/events.js +10 -1
  79. package/dist/lib/exec.d.ts +7 -0
  80. package/dist/lib/exec.js +69 -11
  81. package/dist/lib/feed-post.d.ts +28 -1
  82. package/dist/lib/feed-post.js +110 -2
  83. package/dist/lib/fleet/apply.js +8 -1
  84. package/dist/lib/fleet/auth-sync.d.ts +18 -1
  85. package/dist/lib/fleet/auth-sync.js +25 -11
  86. package/dist/lib/fleet/remote-login.js +5 -0
  87. package/dist/lib/git.d.ts +14 -14
  88. package/dist/lib/git.js +41 -44
  89. package/dist/lib/hooks.d.ts +71 -0
  90. package/dist/lib/hooks.js +245 -95
  91. package/dist/lib/hosts/dispatch.d.ts +28 -1
  92. package/dist/lib/hosts/dispatch.js +33 -5
  93. package/dist/lib/hosts/option.js +2 -2
  94. package/dist/lib/hosts/passthrough.d.ts +22 -1
  95. package/dist/lib/hosts/passthrough.js +238 -13
  96. package/dist/lib/hosts/run-target.d.ts +2 -0
  97. package/dist/lib/hosts/run-target.js +1 -0
  98. package/dist/lib/hq/floor.d.ts +1 -1
  99. package/dist/lib/hq/floor.js +6 -1
  100. package/dist/lib/mcp.js +3 -71
  101. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  102. package/dist/lib/menubar/install-menubar.d.ts +24 -0
  103. package/dist/lib/menubar/install-menubar.js +52 -2
  104. package/dist/lib/menubar/notify-desktop.d.ts +15 -0
  105. package/dist/lib/menubar/notify-desktop.js +41 -5
  106. package/dist/lib/merged-resources.d.ts +11 -0
  107. package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
  108. package/dist/lib/permissions.d.ts +0 -32
  109. package/dist/lib/permissions.js +4 -157
  110. package/dist/lib/picker.js +1 -1
  111. package/dist/lib/platform/process.d.ts +16 -5
  112. package/dist/lib/platform/process.js +54 -0
  113. package/dist/lib/plugins.d.ts +0 -8
  114. package/dist/lib/plugins.js +4 -110
  115. package/dist/lib/project-resources.js +4 -1
  116. package/dist/lib/refresh.d.ts +5 -3
  117. package/dist/lib/refresh.js +7 -5
  118. package/dist/lib/resources/commands.js +4 -1
  119. package/dist/lib/resources/mcp.js +0 -4
  120. package/dist/lib/resources/permissions.d.ts +1 -1
  121. package/dist/lib/resources/permissions.js +1 -5
  122. package/dist/lib/resources/rules.js +4 -1
  123. package/dist/lib/resources/skills.js +4 -1
  124. package/dist/lib/resources/subagents.js +4 -0
  125. package/dist/lib/resources/types.d.ts +1 -1
  126. package/dist/lib/routines.d.ts +33 -0
  127. package/dist/lib/routines.js +122 -0
  128. package/dist/lib/run-defaults.d.ts +1 -0
  129. package/dist/lib/run-defaults.js +9 -0
  130. package/dist/lib/runner.d.ts +0 -12
  131. package/dist/lib/runner.js +49 -10
  132. package/dist/lib/scheduler.js +16 -4
  133. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  134. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  135. package/dist/lib/secrets/agent.d.ts +16 -0
  136. package/dist/lib/secrets/agent.js +105 -3
  137. package/dist/lib/secrets/audit.d.ts +46 -0
  138. package/dist/lib/secrets/audit.js +56 -0
  139. package/dist/lib/secrets/bundles.d.ts +11 -8
  140. package/dist/lib/secrets/bundles.js +31 -23
  141. package/dist/lib/secrets/index.d.ts +2 -1
  142. package/dist/lib/secrets/index.js +8 -2
  143. package/dist/lib/secrets/remote.d.ts +75 -2
  144. package/dist/lib/secrets/remote.js +139 -5
  145. package/dist/lib/secrets/sync.js +3 -3
  146. package/dist/lib/session/active.d.ts +128 -26
  147. package/dist/lib/session/active.js +192 -53
  148. package/dist/lib/session/actor-sidecar.d.ts +23 -0
  149. package/dist/lib/session/actor-sidecar.js +101 -0
  150. package/dist/lib/session/db.d.ts +4 -1
  151. package/dist/lib/session/db.js +65 -8
  152. package/dist/lib/session/discover.d.ts +4 -0
  153. package/dist/lib/session/discover.js +13 -0
  154. package/dist/lib/session/hook-sessions.d.ts +9 -0
  155. package/dist/lib/session/hook-sessions.js +57 -8
  156. package/dist/lib/session/pid-registry.d.ts +13 -0
  157. package/dist/lib/session/render.d.ts +2 -0
  158. package/dist/lib/session/render.js +1 -1
  159. package/dist/lib/session/types.d.ts +16 -0
  160. package/dist/lib/staleness/detectors/commands.js +2 -2
  161. package/dist/lib/staleness/detectors/permissions.js +1 -45
  162. package/dist/lib/staleness/writers/commands.js +4 -5
  163. package/dist/lib/staleness/writers/hooks.js +1 -1
  164. package/dist/lib/startup/command-registry.d.ts +1 -2
  165. package/dist/lib/startup/command-registry.js +2 -4
  166. package/dist/lib/state.js +57 -1
  167. package/dist/lib/subagents-registry.js +5 -12
  168. package/dist/lib/subagents.d.ts +0 -10
  169. package/dist/lib/subagents.js +0 -12
  170. package/dist/lib/teams/agents.d.ts +0 -2
  171. package/dist/lib/teams/agents.js +10 -27
  172. package/dist/lib/teams/index.d.ts +1 -1
  173. package/dist/lib/teams/index.js +1 -1
  174. package/dist/lib/types.d.ts +17 -9
  175. package/dist/lib/usage.d.ts +28 -5
  176. package/dist/lib/usage.js +271 -8
  177. package/dist/lib/versions.js +7 -1
  178. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  179. package/dist/lib/watchdog/watchdog.js +25 -13
  180. package/package.json +1 -2
  181. package/dist/commands/check.d.ts +0 -15
  182. package/dist/commands/check.js +0 -180
  183. package/dist/commands/resources.d.ts +0 -5
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Core agent configuration and detection module.
3
3
  *
4
- * Defines the canonical registry of all supported AI coding agents (Claude, Codex,
5
- * Gemini, Cursor, OpenCode, OpenClaw, Copilot, Amp, Kiro, Goose, Grok) with their
6
- * CLI commands, config paths, capability flags, and MCP integration points.
4
+ * Defines the canonical registry of current and legacy AI coding agents (Claude,
5
+ * Codex, Gemini, Cursor, OpenCode, OpenClaw, Copilot, Amp, Kiro, Goose, Grok)
6
+ * with their CLI commands, config paths, capability flags, and MCP integration
7
+ * points.
7
8
  *
8
9
  * Provides functions for detecting installed CLIs, resolving version-managed binaries,
9
10
  * reading account/auth info, and managing MCP server registrations across agents.
@@ -256,13 +257,15 @@ export const AGENTS = {
256
257
  nativeAgentsSkillsDir: true,
257
258
  // Google retired the Gemini CLI (announced at Google I/O 2026, May 19); the `gemini`
258
259
  // command stopped serving free/Pro/Ultra requests on June 18, 2026. Antigravity CLI
259
- // (`agy`) is the official successor. See warnAgentDeprecated() for the surfaced warning.
260
+ // (`agy`) is the official successor. Hard deprecation keeps legacy Gemini
261
+ // sessions/config parseable while blocking install/import/sync.
260
262
  deprecated: {
261
263
  by: 'Google',
262
264
  date: 'June 18, 2026',
263
265
  reason: 'The Gemini CLI was retired for free, Pro, and Ultra tiers and no longer serves requests (announced at Google I/O 2026 on May 19).',
264
266
  replacement: 'antigravity',
265
267
  url: 'https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/',
268
+ hard: true,
266
269
  },
267
270
  // gemini hooks: shipped in v0.26.0 (Jan 2026); older binaries silently ignore the `hooks` key.
268
271
  // extensions: gemini-extension.json bundles shipped in v0.8.0; custom subagents in v0.36.0.
@@ -697,58 +700,18 @@ export const AGENTS = {
697
700
  rulesImports: false,
698
701
  },
699
702
  },
700
- // ForgeCode (`forge`) from Tailcall. It reads AGENTS.md project rules,
701
- // SKILL.md directories, and MCP servers from `.mcp.json` files.
702
- forge: {
703
- id: 'forge',
704
- name: 'ForgeCode',
705
- color: 'greenBright',
706
- cliCommand: 'forge',
707
- npmPackage: '',
708
- installScript: 'curl -fsSL https://forgecode.dev/cli | sh',
709
- configDir: path.join(HOME, '.forge'),
710
- commandsDir: path.join(HOME, '.forge', 'commands'),
711
- commandsSubdir: 'commands',
712
- skillsDir: path.join(HOME, '.forge', 'skills'),
713
- hooksDir: 'hooks',
714
- instructionsFile: 'AGENTS.md',
715
- format: 'markdown',
716
- variableSyntax: '$ARGUMENTS',
717
- supportsHooks: false,
718
- // Commands: ForgeCode reads Markdown slash commands from `~/.forge/commands/<name>.md`
719
- // (also the shared `~/.agents/commands/`); the filename is the command name.
720
- // Subagents: named `.md` agent definitions with YAML frontmatter under
721
- // `~/.forge/agents/<name>.md` — same Markdown+frontmatter shape as Droid/Copilot
722
- // (no `color` field), so transformSubagentForForge aliases transformSubagentForDroid.
723
- // See https://forgecode.dev/docs/commands/ and /docs/agent-definition-guide/.
724
- capabilities: {
725
- hooks: false,
726
- mcp: true,
727
- mcpHttp: true,
728
- mcpHeaders: false,
729
- // Permissions: ~/.forge/permissions.yaml (or $FORGE_CONFIG/permissions.yaml)
730
- // carries ordered operation-family glob policies. It is active only when
731
- // `.forge.toml` sets `restricted = true`; MCP tools bypass this file.
732
- allowlist: true,
733
- skills: true,
734
- commands: true,
735
- plugins: false,
736
- subagents: true,
737
- rules: { file: 'AGENTS.md' },
738
- workflows: false,
739
- memory: false,
740
- modes: ['edit'],
741
- rulesImports: false,
742
- },
743
- },
744
703
  };
745
- /** All registered agent IDs derived from the AGENTS registry. */
704
+ /** All current and legacy agent IDs derived from the AGENTS registry. */
746
705
  export const ALL_AGENT_IDS = Object.keys(AGENTS);
706
+ /** Agents retained only for legacy reads, not install/import/sync targets. */
707
+ export const HARD_DEPRECATED_AGENT_IDS = ALL_AGENT_IDS.filter((id) => AGENTS[id].deprecated?.hard);
708
+ /** Agents that can receive managed installs, imports, and resource sync writes. */
709
+ export const MANAGED_AGENT_IDS = ALL_AGENT_IDS.filter((id) => !AGENTS[id].deprecated?.hard);
747
710
  /**
748
711
  * A self-updating agent is a single global binary installed by an official
749
712
  * `curl … | sh` / `brew install` script that carries NO version token — the
750
713
  * installer can only ever fetch the *current* release, and the binary then keeps
751
- * itself up to date in place (droid, grok, antigravity, cursor, hermes, forge,
714
+ * itself up to date in place (droid, grok, antigravity, cursor, hermes,
752
715
  * kiro, goose). There is no semver to pin, so agents-cli must not model these as
753
716
  * having multiple installable version-homes the way it does for npm-packaged
754
717
  * agents (claude, codex, kimi, …).
@@ -768,6 +731,9 @@ export function isSelfUpdatingAgent(agent) {
768
731
  const cfg = AGENTS[agent];
769
732
  return !cfg.npmPackage && !!cfg.installScript && !cfg.installScript.includes('VERSION');
770
733
  }
734
+ export function isAgentHardDeprecated(agent) {
735
+ return AGENTS[agent].deprecated?.hard === true;
736
+ }
771
737
  // Capability-filtered agent lists used to live here as `*_CAPABLE_AGENTS`
772
738
  // constants. They were a frequent source of silent-skip bugs (e.g. grok
773
739
  // rules sync gated on `COMMANDS_CAPABLE_AGENTS`). Use `capableAgents(cap)`
@@ -1552,6 +1518,38 @@ export async function getAccountInfo(agentId, home) {
1552
1518
  const email = data.active || null;
1553
1519
  return { ...empty, email, signedIn: !!email, lastActive };
1554
1520
  }
1521
+ case 'cursor': {
1522
+ // Cursor CLI keeps account metadata in ~/.cursor/cli-config.json
1523
+ // (authInfo: { email, userId, authId }) and its OAuth tokens SEPARATELY
1524
+ // in ~/.config/cursor/auth.json ({ accessToken, refreshToken }). Presence
1525
+ // of an access token is the signed-in signal; email/ids come from
1526
+ // cli-config. authId is the OAuth subject (e.g. "google-oauth2|<n>") — the
1527
+ // same value the usage endpoint keys on (see getCursorUsageInfo).
1528
+ const cfgPath = resolveAccountCredentialPath(base, '.cursor', 'cli-config.json');
1529
+ if (!cfgPath)
1530
+ return { ...empty, lastActive };
1531
+ try {
1532
+ const cfg = JSON.parse(await fs.promises.readFile(cfgPath, 'utf-8'));
1533
+ const authInfo = cfg?.authInfo;
1534
+ const email = typeof authInfo?.email === 'string' ? authInfo.email : null;
1535
+ const accountId = normalizeIdentityPart(authInfo?.authId ?? authInfo?.userId);
1536
+ if (!email && !accountId)
1537
+ return { ...empty, lastActive };
1538
+ const authPath = resolveAccountCredentialPath(base, '.config', 'cursor', 'auth.json');
1539
+ let hasToken = false;
1540
+ if (authPath) {
1541
+ try {
1542
+ const tok = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
1543
+ hasToken = typeof tok?.accessToken === 'string' && tok.accessToken.length > 0;
1544
+ }
1545
+ catch { /* unreadable token file */ }
1546
+ }
1547
+ const accountKey = buildIdentityKey(agentId, [['user', accountId]]);
1548
+ return { ...empty, email, accountId, accountKey, signedIn: hasToken || !!email, lastActive };
1549
+ }
1550
+ catch { }
1551
+ return { ...empty, lastActive };
1552
+ }
1555
1553
  case 'grok': {
1556
1554
  // Grok stores auth in ~/.grok/auth.json as a map keyed by
1557
1555
  // "<oidc_issuer>::<client_id>" -> { email, user_id, refresh_token,
@@ -1867,7 +1865,7 @@ function buildIdentityKey(agentId, parts) {
1867
1865
  /** Register an MCP server with an agent's CLI via `mcp add`. */
1868
1866
  export async function registerMcp(agentId, name, command, scope = 'user', transport = 'stdio', options) {
1869
1867
  const agent = AGENTS[agentId];
1870
- if (!agent.capabilities.mcp) {
1868
+ if (!supports(agentId, 'mcp').ok) {
1871
1869
  return { success: false, error: 'Agent does not support MCP' };
1872
1870
  }
1873
1871
  if (transport === 'http' && !supports(agentId, 'mcpHttp').ok) {
@@ -1876,7 +1874,7 @@ export async function registerMcp(agentId, name, command, scope = 'user', transp
1876
1874
  if (transport === 'http' && options?.headers && Object.keys(options.headers).length > 0 && !supports(agentId, 'mcpHeaders').ok) {
1877
1875
  return { success: false, error: 'skipped: HTTP MCP headers are only supported for Claude registration' };
1878
1876
  }
1879
- if (agentId === 'hermes' || agentId === 'forge') {
1877
+ if (agentId === 'hermes') {
1880
1878
  try {
1881
1879
  writeMcpToConfig(agentId, name, command, scope, transport, options?.home);
1882
1880
  return { success: true };
@@ -1927,10 +1925,10 @@ export async function registerMcp(agentId, name, command, scope = 'user', transp
1927
1925
  /** Unregister (remove) a named MCP server from an agent's CLI config. */
1928
1926
  export async function unregisterMcp(agentId, name, options) {
1929
1927
  const agent = AGENTS[agentId];
1930
- if (!agent.capabilities.mcp) {
1928
+ if (!supports(agentId, 'mcp').ok) {
1931
1929
  return { success: false, error: 'Agent does not support MCP' };
1932
1930
  }
1933
- if (agentId === 'hermes' || agentId === 'forge') {
1931
+ if (agentId === 'hermes') {
1934
1932
  try {
1935
1933
  removeMcpFromConfig(agentId, name, options?.home);
1936
1934
  return { success: true };
@@ -2282,8 +2280,6 @@ export function getUserMcpConfigPath(agentId) {
2282
2280
  return path.join(agent.configDir, 'mcp.json');
2283
2281
  case 'hermes':
2284
2282
  return path.join(agent.configDir, 'config.yaml');
2285
- case 'forge':
2286
- return path.join(agent.configDir, '.mcp.json');
2287
2283
  default:
2288
2284
  // Gemini and others use settings.json
2289
2285
  return path.join(agent.configDir, 'settings.json');
@@ -2320,8 +2316,6 @@ export function getMcpConfigPathForHome(agentId, home) {
2320
2316
  return path.join(home, '.factory', 'mcp.json');
2321
2317
  case 'hermes':
2322
2318
  return path.join(home, '.hermes', 'config.yaml');
2323
- case 'forge':
2324
- return path.join(home, '.forge', '.mcp.json');
2325
2319
  default:
2326
2320
  return path.join(home, agentConfigDirName(agentId), 'settings.json');
2327
2321
  }
@@ -2361,8 +2355,6 @@ export function getProjectMcpConfigPath(agentId, cwd = process.cwd()) {
2361
2355
  return path.join(cwd, '.factory', 'mcp.json');
2362
2356
  case 'hermes':
2363
2357
  return path.join(cwd, '.hermes', 'config.yaml');
2364
- case 'forge':
2365
- return path.join(cwd, '.mcp.json');
2366
2358
  default:
2367
2359
  return path.join(cwd, `.${agentId}`, 'settings.json');
2368
2360
  }
@@ -2506,9 +2498,6 @@ export const AGENT_NAME_ALIASES = {
2506
2498
  droid: 'droid',
2507
2499
  hermes: 'hermes',
2508
2500
  'hermes-agent': 'hermes',
2509
- forge: 'forge',
2510
- forgecode: 'forge',
2511
- 'forge-code': 'forge',
2512
2501
  };
2513
2502
  /**
2514
2503
  * Resolve a user-provided agent name (alias, shorthand, or canonical) to its AgentId.
@@ -2559,6 +2548,26 @@ export function deprecationNotice(agent) {
2559
2548
  lines.push(` ${dep.url}`);
2560
2549
  return lines;
2561
2550
  }
2551
+ export function hardDeprecationNotice(agent) {
2552
+ const dep = AGENTS[agent].deprecated;
2553
+ if (!dep?.hard)
2554
+ return null;
2555
+ const name = AGENTS[agent].name;
2556
+ const lines = [
2557
+ `${name} is no longer supported by agents-cli because ${dep.by} retired it (${dep.date}).`,
2558
+ ` ${dep.reason}`,
2559
+ ];
2560
+ if (dep.replacement) {
2561
+ const rep = AGENTS[dep.replacement];
2562
+ lines.push(` Use ${rep.name} instead: agents add ${rep.id}`);
2563
+ }
2564
+ if (dep.url)
2565
+ lines.push(` ${dep.url}`);
2566
+ return lines;
2567
+ }
2568
+ export function hardDeprecationError(agent) {
2569
+ return hardDeprecationNotice(agent)?.join('\n') ?? `${AGENTS[agent].name} is no longer supported.`;
2570
+ }
2562
2571
  /**
2563
2572
  * Print a deprecation warning (yellow) if the agent's registry entry carries a
2564
2573
  * `deprecated` marker; no-op otherwise. Call from any user entry point that
@@ -14,6 +14,7 @@ import { clickAtCoords, hoverAtCoords, scrollAtCoords, typeText, pressKey, focus
14
14
  import { typeEditorText } from './editor.js';
15
15
  import { detectUploadPattern, stageUploadFile, uploadToDropTarget, uploadToFileInput, uploadViaFileChooser, } from './upload.js';
16
16
  import { emit } from '../events.js';
17
+ import { resolveActor } from '../actor.js';
17
18
  import { sshExecAsync } from '../ssh-exec.js';
18
19
  function isPathInside(candidate, dir) {
19
20
  const rel = path.relative(dir, candidate);
@@ -331,6 +332,7 @@ export class BrowserService {
331
332
  currentTabId: undefined,
332
333
  createdAt: Date.now(),
333
334
  pid: conn.pid,
335
+ owner: resolveActor().id, // who launched this browser task (RUSH-2020)
334
336
  };
335
337
  // For Electron, get the existing window as the tab
336
338
  if (conn.electron) {
@@ -2012,6 +2014,7 @@ export class BrowserService {
2012
2014
  endedAt: Date.now(),
2013
2015
  domains,
2014
2016
  tabCount: Object.keys(task.tabs).length,
2017
+ ...(task.owner ? { owner: task.owner } : {}), // carry who launched it (RUSH-2020)
2015
2018
  });
2016
2019
  // Keep only last 50 entries
2017
2020
  history = history.slice(0, 50);
@@ -78,6 +78,11 @@ export interface Task {
78
78
  currentTabId?: string;
79
79
  createdAt: number;
80
80
  pid: number;
81
+ /**
82
+ * Resolved actor id (`resolveActor().id`) stamped at task start — who launched
83
+ * this browser task. Optional: tasks persisted before RUSH-2020 carry none.
84
+ */
85
+ owner?: string;
81
86
  /**
82
87
  * Per-tab snapshot of the last ref listing captured for that tab
83
88
  * (shortId -> {descriptors, opts}). Persisted to tasks.json so a later
@@ -126,6 +131,8 @@ export interface HistoricalTask {
126
131
  endedAt: number;
127
132
  domains: string[];
128
133
  tabCount: number;
134
+ /** Actor id who launched the task (RUSH-2020); absent for pre-RUSH-2020 history. */
135
+ owner?: string;
129
136
  }
130
137
  export type IPCAction = 'start' | 'record-start' | 'record-stop' | 'done' | 'stop' | 'status' | 'history' | 'navigate' | 'tab-add' | 'tab-focus' | 'tab-close' | 'tab-list' | 'evaluate' | 'screenshot' | 'pdf' | 'refs' | 'click' | 'type' | 'press' | 'hover' | 'scroll' | 'set-viewport' | 'set-device' | 'console' | 'errors' | 'requests' | 'response-body' | 'wait' | 'set-download-path' | 'wait-download' | 'upload' | 'getAppLogs' | 'version';
131
138
  export interface IPCRequest {
@@ -7,7 +7,7 @@
7
7
  * is below `since`, the install path skips the write and surfaces a clear
8
8
  * reason instead of silently corrupting an older binary's settings file.
9
9
  */
10
- import { AGENTS } from './agents.js';
10
+ import { AGENTS, MANAGED_AGENT_IDS } from './agents.js';
11
11
  /**
12
12
  * Compare semver-like versions ("0.116.0" vs "0.115.9"). Local copy to avoid
13
13
  * importing versions.ts (which imports agents.ts, which imports this file).
@@ -37,6 +37,8 @@ function getCapability(agent, cap) {
37
37
  * Useful for filtering UI lists; does not check installed version.
38
38
  */
39
39
  export function isCapable(agent, cap) {
40
+ if (AGENTS[agent]?.deprecated?.hard)
41
+ return false;
40
42
  const c = getCapability(agent, cap);
41
43
  return c !== false;
42
44
  }
@@ -47,6 +49,8 @@ export function isCapable(agent, cap) {
47
49
  * capable" filters but NOT for install-time gating.
48
50
  */
49
51
  export function supports(agent, cap, version) {
52
+ if (AGENTS[agent]?.deprecated?.hard)
53
+ return { ok: false, reason: 'unsupported' };
50
54
  const c = getCapability(agent, cap);
51
55
  if (c === false)
52
56
  return { ok: false, reason: 'unsupported' };
@@ -78,5 +82,5 @@ export function explainSkip(agent, cap, result, version) {
78
82
  }
79
83
  /** All agents whose `capabilities[cap]` is anything other than `false`. */
80
84
  export function capableAgents(cap) {
81
- return Object.keys(AGENTS).filter((id) => isCapable(id, cap));
85
+ return MANAGED_AGENT_IDS.filter((id) => isCapable(id, cap));
82
86
  }
@@ -137,7 +137,7 @@ export function buildBootstrapScript(opts) {
137
137
  // (leaseAndRun, before this script). Here — after ENSURE_AGENTS_CLI installed
138
138
  // the CLI — we materialize that config into the runtime home. Gated by
139
139
  // copySetup (cleared for --bare). Best-effort; a refresh failure never aborts.
140
- copySetup ? [step('copy-setup'), 'agents repo refresh >/dev/null 2>&1 || true'].join('\n') : '',
140
+ copySetup ? [step('copy-setup'), 'agents sync --local -y >/dev/null 2>&1 || true'].join('\n') : '',
141
141
  // Marker on its own line: the command layer shows everything before this as
142
142
  // setup progress and everything after (the agent's output) verbatim.
143
143
  `echo ${q(LEASE_AGENT_MARKER)}`,
@@ -176,7 +176,7 @@ export async function leaseAndRun(opts) {
176
176
  // Setup-copy (F1, RUSH-1920): push the git-tracked ~/.agents config onto the
177
177
  // box from the host, over crabbox's own per-lease ssh (a raw ssh fails
178
178
  // publickey). rsync only here — the box has no agents-cli yet, so the matching
179
- // `agents repo refresh` runs inside the bootstrap script, after the install
179
+ // `agents sync --local` runs inside the bootstrap script, after the install
180
180
  // step. Best-effort: a copy failure never aborts the run (the agent just runs
181
181
  // without the config).
182
182
  if (opts.copySetup !== false) {
@@ -12,7 +12,7 @@
12
12
  * 2. `rsync` that exact file set to `~/.agents` on the box over crabbox's OWN
13
13
  * ssh invocation (`crabboxSshArgv`) — crabbox provisions a per-lease identity
14
14
  * key, so a raw `ssh crabbox@ip` fails publickey; only crabbox's key works.
15
- * 3. `agents repo refresh` on the box so the copied config takes effect.
15
+ * 3. `agents sync --local` on the box so the copied config takes effect.
16
16
  *
17
17
  * NEVER copies `~/.claude` / `~/.claude.json` — they live in `$HOME`, not
18
18
  * `~/.agents`, and are rebuilt on the box by `agents add` + refresh; an explicit
@@ -34,7 +34,7 @@ export interface CopySetupOptions {
34
34
  /** Receives combined stdout/stderr of the rsync + refresh, if set. */
35
35
  onData?: (chunk: string) => void;
36
36
  /**
37
- * Run `agents repo refresh` on the box after the push (default `true`). Set
37
+ * Run `agents sync --local` on the box after the push (default `true`). Set
38
38
  * `false` when the caller runs the refresh itself in the box bootstrap — the
39
39
  * lease path does this so the refresh runs AFTER the box installs agents-cli
40
40
  * (this host-side push happens before the box boots agents-cli, so a host-side
@@ -47,7 +47,7 @@ export interface CopySetupResult {
47
47
  files: string[];
48
48
  /** rsync exit code, or null when the process failed to spawn. */
49
49
  pushExitCode: number | null;
50
- /** `agents repo refresh` exit code, or null when it was skipped/failed to spawn. */
50
+ /** `agents sync --local` exit code, or null when it was skipped/failed to spawn. */
51
51
  refreshExitCode: number | null;
52
52
  }
53
53
  /**
@@ -80,7 +80,7 @@ export declare function buildSetupRsyncArgs(opts: {
80
80
  /**
81
81
  * Replicate the git-tracked subset of the local `~/.agents` onto the crabbox box
82
82
  * and refresh it. Enumerates tracked files, rsyncs them over ssh, then runs
83
- * `agents repo refresh` on the box. Refresh is skipped when the push fails or the
83
+ * `agents sync --local` on the box. Refresh is skipped when the push fails or the
84
84
  * file set is empty (nothing to refresh). Never throws — surfaces failure through
85
85
  * the returned exit codes.
86
86
  */
@@ -12,7 +12,7 @@
12
12
  * 2. `rsync` that exact file set to `~/.agents` on the box over crabbox's OWN
13
13
  * ssh invocation (`crabboxSshArgv`) — crabbox provisions a per-lease identity
14
14
  * key, so a raw `ssh crabbox@ip` fails publickey; only crabbox's key works.
15
- * 3. `agents repo refresh` on the box so the copied config takes effect.
15
+ * 3. `agents sync --local` on the box so the copied config takes effect.
16
16
  *
17
17
  * NEVER copies `~/.claude` / `~/.claude.json` — they live in `$HOME`, not
18
18
  * `~/.agents`, and are rebuilt on the box by `agents add` + refresh; an explicit
@@ -88,7 +88,7 @@ function runStreaming(cmd, args, env, onData) {
88
88
  /**
89
89
  * Replicate the git-tracked subset of the local `~/.agents` onto the crabbox box
90
90
  * and refresh it. Enumerates tracked files, rsyncs them over ssh, then runs
91
- * `agents repo refresh` on the box. Refresh is skipped when the push fails or the
91
+ * `agents sync --local` on the box. Refresh is skipped when the push fails or the
92
92
  * file set is empty (nothing to refresh). Never throws — surfaces failure through
93
93
  * the returned exit codes.
94
94
  */
@@ -115,8 +115,8 @@ export async function copySetupToBox(opts) {
115
115
  const pushExitCode = await runStreaming('rsync', rsyncArgs, env, opts.onData);
116
116
  let refreshExitCode = null;
117
117
  if (pushExitCode === 0 && opts.refresh !== false) {
118
- // ssh <opts> crabbox@host bash -lc 'agents repo refresh'
119
- const refreshArgs = [...sshArgv.slice(1), 'bash', '-lc', 'agents repo refresh'];
118
+ // ssh <opts> crabbox@host bash -lc 'agents sync --local -y'
119
+ const refreshArgs = [...sshArgv.slice(1), 'bash', '-lc', 'agents sync --local -y'];
120
120
  refreshExitCode = await runStreaming('ssh', refreshArgs, env, opts.onData);
121
121
  }
122
122
  return { files, pushExitCode, refreshExitCode };
@@ -68,6 +68,24 @@ export declare function reapStrayDaemons(keepPid?: number): {
68
68
  /** Append a JSONL log entry to the daemon log file (owner-only permissions). */
69
69
  export declare function log(level: string, message: string): void;
70
70
  /** Main daemon loop: load jobs, schedule crons, monitor runs, and handle signals. */
71
+ /**
72
+ * Anchor the daemon's working directory to a stable, always-present path.
73
+ *
74
+ * The daemon is long-lived and inherits whatever cwd it was launched from — often
75
+ * a git worktree (e.g. a `.agents/worktrees/<slug>/` a session happened to be in).
76
+ * When that directory is later removed (`git worktree remove`, `rm -rf`), the
77
+ * daemon keeps the deleted inode as its cwd — a process cannot chdir out of a
78
+ * deleted directory on its own — and every job it spawns inherits the dead cwd
79
+ * (`spawnJobAttempt` and command runs pass no explicit `cwd`, so the child uses
80
+ * the parent's). Bun then fails `getcwd()` during startup and every routine crashes
81
+ * at 0 seconds with `ENOENT: Bun could not find a file` before the agent even runs.
82
+ *
83
+ * Re-anchoring to the home directory once, at daemon startup, makes the daemon
84
+ * immune regardless of how it was launched (systemd unit, launchd, or a manual
85
+ * `agents __daemon-run` from any directory). Returns the resolved cwd, or null if
86
+ * anchoring failed (logged, non-fatal).
87
+ */
88
+ export declare function anchorDaemonCwd(): string | null;
71
89
  export declare function runDaemon(): Promise<void>;
72
90
  /**
73
91
  * Write a launchd plist or systemd unit with owner-only permissions atomically.
@@ -11,7 +11,7 @@ import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import * as os from 'os';
13
13
  import { getDaemonDir as getDaemonDirRoot } from './state.js';
14
- import { isAlive, killTree, backgroundSpawnOptions } from './platform/index.js';
14
+ import { isAlive, killTree, backgroundSpawnOptions, waitForExit } from './platform/index.js';
15
15
  import { listJobs as listAllJobs } from './routines.js';
16
16
  import { syncAllProjectRoutines } from './routines-project.js';
17
17
  import { JobScheduler } from './scheduler.js';
@@ -171,6 +171,10 @@ export function isDaemonWedged() {
171
171
  const elapsed = Date.now() - Date.parse(hb.lastTick);
172
172
  return elapsed > WEDGE_THRESHOLD_TICKS * MONITOR_TICK_MS;
173
173
  }
174
+ /** How long stopDaemon waits for a SIGTERMed daemon to exit before escalating. */
175
+ const STOP_GRACE_MS = 5000;
176
+ /** How long it waits after the hard tree-kill before giving up. */
177
+ const STOP_KILL_GRACE_MS = 2000;
174
178
  /** Check if the daemon process is alive by sending signal 0 to the stored PID. */
175
179
  export function isDaemonRunning() {
176
180
  const pid = readDaemonPid();
@@ -289,6 +293,34 @@ export function log(level, message) {
289
293
  catch { /* best effort */ }
290
294
  }
291
295
  /** Main daemon loop: load jobs, schedule crons, monitor runs, and handle signals. */
296
+ /**
297
+ * Anchor the daemon's working directory to a stable, always-present path.
298
+ *
299
+ * The daemon is long-lived and inherits whatever cwd it was launched from — often
300
+ * a git worktree (e.g. a `.agents/worktrees/<slug>/` a session happened to be in).
301
+ * When that directory is later removed (`git worktree remove`, `rm -rf`), the
302
+ * daemon keeps the deleted inode as its cwd — a process cannot chdir out of a
303
+ * deleted directory on its own — and every job it spawns inherits the dead cwd
304
+ * (`spawnJobAttempt` and command runs pass no explicit `cwd`, so the child uses
305
+ * the parent's). Bun then fails `getcwd()` during startup and every routine crashes
306
+ * at 0 seconds with `ENOENT: Bun could not find a file` before the agent even runs.
307
+ *
308
+ * Re-anchoring to the home directory once, at daemon startup, makes the daemon
309
+ * immune regardless of how it was launched (systemd unit, launchd, or a manual
310
+ * `agents __daemon-run` from any directory). Returns the resolved cwd, or null if
311
+ * anchoring failed (logged, non-fatal).
312
+ */
313
+ export function anchorDaemonCwd() {
314
+ const home = os.homedir();
315
+ try {
316
+ process.chdir(home);
317
+ return home;
318
+ }
319
+ catch (err) {
320
+ log('WARN', `Could not anchor daemon cwd to ${home}: ${err.message}`);
321
+ return null;
322
+ }
323
+ }
292
324
  export async function runDaemon() {
293
325
  // Single-instance guard: a direct `agents __daemon-run` (manual, or a
294
326
  // service-manager restart racing a live predecessor) must not clobber a
@@ -301,6 +333,7 @@ export async function runDaemon() {
301
333
  process.exit(0);
302
334
  }
303
335
  log('INFO', `Daemon started (PID: ${process.pid})`);
336
+ anchorDaemonCwd();
304
337
  // The daemon holds NO Claude credential of its own. Routine runs authenticate
305
338
  // exactly like an interactive `agents run`: through the per-account
306
339
  // CLAUDE_CONFIG_DIR login on this device (its own auto-refreshing
@@ -1161,11 +1194,19 @@ export function stopDaemon() {
1161
1194
  process.kill(pid, 'SIGTERM');
1162
1195
  }
1163
1196
  catch { /* process already exited */ }
1164
- // Escalate to a hard tree-kill if it ignored SIGTERM after the grace period.
1165
- setTimeout(() => {
1166
- if (isAlive(pid))
1167
- killTree(pid);
1168
- }, 5000);
1197
+ // Wait for it to actually go. This used to be a setTimeout escalation plus
1198
+ // an immediate removeDaemonPid(), which had two failure modes: in a
1199
+ // short-lived process (the npm postinstall) the timer never fired at all,
1200
+ // and clearing the pid file while the old daemon still ran made
1201
+ // isDaemonRunning() report false, so startDaemon() launched a SECOND
1202
+ // daemon. Its hosted broker then unlinked the live socket and rebound,
1203
+ // orphaning the first broker with every unlocked bundle still in its RAM
1204
+ // and unreachable — two brokers on one socket path, seen on a real machine
1205
+ // after an install into a second prefix.
1206
+ if (!waitForExit(pid, STOP_GRACE_MS)) {
1207
+ killTree(pid);
1208
+ waitForExit(pid, STOP_KILL_GRACE_MS);
1209
+ }
1169
1210
  }
1170
1211
  }
1171
1212
  removeDaemonPid();
@@ -48,7 +48,7 @@ export interface FanOutDeviceResult<T> {
48
48
  export declare function planFleetTargets(reg: DeviceRegistry): FleetTarget[];
49
49
  /**
50
50
  * Remote fan-out targets for the fleet health/drift gates (`fleet status`,
51
- * `check --devices`): every planned device except this machine and control-only
51
+ * `doctor --check --devices`): every planned device except this machine and control-only
52
52
  * cockpits. A control device never runs agents (mirrors doctor's fan-out, which
53
53
  * drops it via `isControlDevice`), so counting it as unreachable/drift would make
54
54
  * the CI gate fail on every run for a fleet that merely has a cockpit registered.
@@ -44,7 +44,7 @@ export function planFleetTargets(reg) {
44
44
  }
45
45
  /**
46
46
  * Remote fan-out targets for the fleet health/drift gates (`fleet status`,
47
- * `check --devices`): every planned device except this machine and control-only
47
+ * `doctor --check --devices`): every planned device except this machine and control-only
48
48
  * cockpits. A control device never runs agents (mirrors doctor's fan-out, which
49
49
  * drops it via `isControlDevice`), so counting it as unreachable/drift would make
50
50
  * the CI gate fail on every run for a fleet that merely has a cockpit registered.
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import type { AgentId } from './types.js';
23
23
  import type { DiscoveredPlugin } from './types.js';
24
+ import { type HookWiringReport } from './hooks.js';
24
25
  export type DoctorKind = 'commands' | 'skills' | 'hooks' | 'rules' | 'mcp' | 'permissions' | 'subagents' | 'plugins' | 'promptcuts';
25
26
  export type DiffStatus = 'ok' | 'diff' | 'missing' | 'extra';
26
27
  export type SourceLayer = 'project' | 'user' | 'system' | 'extra';
@@ -37,6 +38,19 @@ export interface ResourceDiff {
37
38
  * "0.6.1→0.7.0, missing skills: ship, learn". Currently set for plugins. */
38
39
  detail?: string;
39
40
  }
41
+ /** A source layer (user / system / an extra repo) that is behind its upstream —
42
+ * reconciled against stale truth. Attached to the report by the doctor command
43
+ * (needs a git probe, kept out of the pure per-version diff). */
44
+ export interface SourceLayerBehind {
45
+ layer: 'user' | 'system' | 'extra';
46
+ /** Human label for the layer's on-disk root (e.g. `~/.agents`). */
47
+ label: string;
48
+ /** `agents repo pull` alias for the remediation hint. */
49
+ alias: string;
50
+ behind: number;
51
+ /** Upstream ref the layer trails (e.g. `origin/main`). */
52
+ branch: string;
53
+ }
40
54
  export interface VersionResourceReport {
41
55
  agent: AgentId;
42
56
  version: string;
@@ -58,6 +72,12 @@ export interface VersionResourceReport {
58
72
  missing: number;
59
73
  extra: number;
60
74
  };
75
+ /** Wiring inspection for the version's native settings.json (claude/droid).
76
+ * Populated only when the `hooks` kind is in scope. */
77
+ hookWiring?: HookWiringReport;
78
+ /** Source layers behind their upstream. Filled by the doctor command (a git
79
+ * probe), not the pure diff — undefined when uncomputed. */
80
+ sourceBehind?: SourceLayerBehind[];
61
81
  }
62
82
  /**
63
83
  * Describe how a version's marketplace MIRROR of a plugin diverges from its
@@ -33,7 +33,7 @@ import { shouldInstallCommandAsSkill, commandSkillMatches, commandSkillName } fr
33
33
  import { gooseCommandMatches, gooseCommandsDir } from './goose-commands.js';
34
34
  import { supports } from './capabilities.js';
35
35
  import { listSkillsInVersionHome, getVersionSkillsDir } from './skills.js';
36
- import { listHooksInVersionHome, listHookEntriesFromDir } from './hooks.js';
36
+ import { listHooksInVersionHome, listHookEntriesFromDir, checkVersionHookWiring } from './hooks.js';
37
37
  const RULES_DOC_FILENAME = 'README.md';
38
38
  const ALL_KINDS = [
39
39
  'commands',
@@ -611,6 +611,10 @@ export function diffVersionResources(agent, version, options = {}) {
611
611
  empty.skills = diffSkills(agent, version, cwd, excludeProject);
612
612
  if (requested.has('hooks'))
613
613
  empty.hooks = diffHooks(agent, version, cwd);
614
+ // Wiring check: a hook FILE can be present and byte-identical to source (ok
615
+ // above) yet never referenced in settings.json, so it never fires. Only
616
+ // meaningful when hooks are in scope.
617
+ const hookWiring = requested.has('hooks') ? checkVersionHookWiring(agent, version) : undefined;
614
618
  if (requested.has('rules'))
615
619
  empty.rules = diffRules(agent, version, cwd, excludeProject);
616
620
  if (requested.has('mcp'))
@@ -649,6 +653,7 @@ export function diffVersionResources(agent, version, options = {}) {
649
653
  },
650
654
  kinds: empty,
651
655
  summary: { ok, diff, missing, extra },
656
+ ...(hookWiring ? { hookWiring } : {}),
652
657
  };
653
658
  }
654
659
  export const DOCTOR_ALL_KINDS = ALL_KINDS;