@phnx-labs/agents-cli 1.22.74 → 1.22.76

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 (148) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/README.md +21 -9
  3. package/dist/bootstrap.js +7 -7
  4. package/dist/cli/command-registry.js +5 -0
  5. package/dist/commands/artifacts-setup.js +1 -1
  6. package/dist/commands/artifacts.js +1 -1
  7. package/dist/commands/auth.js +7 -1
  8. package/dist/commands/browser.js +104 -10
  9. package/dist/commands/commands.js +7 -6
  10. package/dist/commands/computer.d.ts +1 -0
  11. package/dist/commands/computer.js +26 -7
  12. package/dist/commands/config.js +27 -4
  13. package/dist/commands/cost.js +6 -4
  14. package/dist/commands/doctor.d.ts +6 -5
  15. package/dist/commands/doctor.js +32 -274
  16. package/dist/commands/exec.d.ts +2 -0
  17. package/dist/commands/exec.js +9 -2
  18. package/dist/commands/harness.d.ts +1 -0
  19. package/dist/commands/harness.js +11 -3
  20. package/dist/commands/hooks.js +7 -6
  21. package/dist/commands/mcp.js +7 -6
  22. package/dist/commands/memory.js +7 -7
  23. package/dist/commands/monitors.js +3 -2
  24. package/dist/commands/open.d.ts +25 -12
  25. package/dist/commands/open.js +24 -10
  26. package/dist/commands/permissions.js +7 -6
  27. package/dist/commands/plugins.js +21 -17
  28. package/dist/commands/route.js +33 -16
  29. package/dist/commands/rules.js +7 -12
  30. package/dist/commands/sessions-share.js +1 -1
  31. package/dist/commands/setup-watchdog.js +2 -2
  32. package/dist/commands/setup.js +22 -1
  33. package/dist/commands/share.js +26 -10
  34. package/dist/commands/skills.js +7 -6
  35. package/dist/commands/subagents.js +7 -6
  36. package/dist/commands/sync.js +81 -10
  37. package/dist/commands/view.js +4 -1
  38. package/dist/commands/watchdog.d.ts +1 -1
  39. package/dist/commands/watchdog.js +10 -10
  40. package/dist/commands/webhook.d.ts +4 -0
  41. package/dist/commands/webhook.js +22 -4
  42. package/dist/commands/workflows.js +7 -6
  43. package/dist/lib/account-registry.js +27 -6
  44. package/dist/lib/accounting/rotate.d.ts +3 -1
  45. package/dist/lib/accounting/rotate.js +8 -4
  46. package/dist/lib/auth-health.d.ts +2 -0
  47. package/dist/lib/auth-health.js +2 -0
  48. package/dist/lib/browser/chrome.d.ts +21 -0
  49. package/dist/lib/browser/chrome.js +60 -3
  50. package/dist/lib/browser/drivers/local.d.ts +21 -0
  51. package/dist/lib/browser/drivers/local.js +102 -9
  52. package/dist/lib/browser/profiles.d.ts +29 -1
  53. package/dist/lib/browser/profiles.js +50 -1
  54. package/dist/lib/browser/types.d.ts +18 -0
  55. package/dist/lib/computer/computer-rpc.d.ts +6 -1
  56. package/dist/lib/computer/computer-rpc.js +23 -3
  57. package/dist/lib/computer/des.d.ts +1 -0
  58. package/dist/lib/computer/des.js +114 -0
  59. package/dist/lib/computer/rfb-client.d.ts +53 -0
  60. package/dist/lib/computer/rfb-client.js +562 -0
  61. package/dist/lib/config-keys.d.ts +7 -2
  62. package/dist/lib/config-keys.js +17 -2
  63. package/dist/lib/daemon/auth-sync-service.js +3 -0
  64. package/dist/lib/daemon/daemon.js +17 -10
  65. package/dist/lib/daemon/session-summarizer-service.d.ts +24 -0
  66. package/dist/lib/daemon/session-summarizer-service.js +39 -0
  67. package/dist/lib/daemon/usage-sync-service.js +3 -0
  68. package/dist/lib/daemon-services.d.ts +1 -1
  69. package/dist/lib/daemon-services.js +5 -0
  70. package/dist/lib/daemon-ticks.d.ts +2 -2
  71. package/dist/lib/daemon-ticks.js +2 -1
  72. package/dist/lib/daemon-webhooks.js +15 -2
  73. package/dist/lib/deeplink/register.js +10 -9
  74. package/dist/lib/deeplink/url.d.ts +4 -4
  75. package/dist/lib/deeplink/url.js +4 -4
  76. package/dist/lib/device-config.js +25 -0
  77. package/dist/lib/devices/doctor-findings.d.ts +4 -4
  78. package/dist/lib/devices/doctor-findings.js +14 -8
  79. package/dist/lib/devices/registry.js +2 -0
  80. package/dist/lib/devices/stats-cache.d.ts +4 -0
  81. package/dist/lib/devices/stats-cache.js +19 -0
  82. package/dist/lib/drift-sync.d.ts +3 -1
  83. package/dist/lib/drift-sync.js +16 -5
  84. package/dist/lib/exec.d.ts +2 -0
  85. package/dist/lib/exec.js +16 -1
  86. package/dist/lib/fleet-shared-repo-sync.d.ts +12 -0
  87. package/dist/lib/fleet-shared-repo-sync.js +101 -4
  88. package/dist/lib/fleet-shared-state.d.ts +8 -0
  89. package/dist/lib/heal.d.ts +4 -3
  90. package/dist/lib/heal.js +5 -4
  91. package/dist/lib/hosts/ready.d.ts +1 -1
  92. package/dist/lib/hosts/ready.js +16 -4
  93. package/dist/lib/hosts/reconnect.js +4 -2
  94. package/dist/lib/identity/client.d.ts +6 -0
  95. package/dist/lib/identity/index.d.ts +16 -0
  96. package/dist/lib/identity/index.js +25 -1
  97. package/dist/lib/profiles.d.ts +2 -0
  98. package/dist/lib/profiles.js +28 -9
  99. package/dist/lib/reconcile-and-repair.d.ts +109 -0
  100. package/dist/lib/reconcile-and-repair.js +267 -0
  101. package/dist/lib/routers.d.ts +12 -1
  102. package/dist/lib/routers.js +30 -1
  103. package/dist/lib/scheduling/routines.js +8 -2
  104. package/dist/lib/session/active.d.ts +13 -0
  105. package/dist/lib/session/db.d.ts +47 -7
  106. package/dist/lib/session/db.js +114 -12
  107. package/dist/lib/session/mirror.js +58 -0
  108. package/dist/lib/session/remote/remote-list.d.ts +2 -0
  109. package/dist/lib/session/remote/remote-list.js +4 -0
  110. package/dist/lib/session/remote/watch.js +22 -2
  111. package/dist/lib/session/session-cache.d.ts +19 -0
  112. package/dist/lib/session/session-cache.js +46 -0
  113. package/dist/lib/session/types.d.ts +34 -0
  114. package/dist/lib/share/backend.d.ts +6 -4
  115. package/dist/lib/share/backend.js +10 -8
  116. package/dist/lib/share/config.d.ts +4 -3
  117. package/dist/lib/share/config.js +10 -1
  118. package/dist/lib/share/delete.d.ts +1 -1
  119. package/dist/lib/share/delete.js +1 -1
  120. package/dist/lib/share/html.d.ts +1 -1
  121. package/dist/lib/share/html.js +1 -1
  122. package/dist/lib/share/provision.d.ts +1 -1
  123. package/dist/lib/share/provision.js +2 -2
  124. package/dist/lib/share/publish.d.ts +23 -7
  125. package/dist/lib/share/publish.js +58 -12
  126. package/dist/lib/share/worker-template.js +221 -60
  127. package/dist/lib/startup/command-registry.js +2 -2
  128. package/dist/lib/state.d.ts +15 -0
  129. package/dist/lib/state.js +29 -7
  130. package/dist/lib/summarizer/config.d.ts +46 -0
  131. package/dist/lib/summarizer/config.js +83 -0
  132. package/dist/lib/summarizer/pass.d.ts +45 -0
  133. package/dist/lib/summarizer/pass.js +112 -0
  134. package/dist/lib/summarizer/summarize.d.ts +68 -0
  135. package/dist/lib/summarizer/summarize.js +120 -0
  136. package/dist/lib/teams/agents.d.ts +4 -3
  137. package/dist/lib/teams/agents.js +12 -4
  138. package/dist/lib/teams/scheduler.d.ts +4 -2
  139. package/dist/lib/teams/scheduler.js +6 -6
  140. package/dist/lib/tmux/session.d.ts +2 -0
  141. package/dist/lib/tmux/session.js +7 -1
  142. package/dist/lib/types.d.ts +20 -0
  143. package/dist/lib/verbs.d.ts +23 -0
  144. package/dist/lib/verbs.js +24 -0
  145. package/dist/lib/view-types.d.ts +4 -0
  146. package/dist/lib/watchdog/rotate.d.ts +1 -1
  147. package/dist/lib/watchdog/rotate.js +1 -1
  148. package/package.json +1 -1
@@ -10,7 +10,7 @@ export async function runWatchdogSetupWizard() {
10
10
  const devices = [...new Set([...Object.keys(registry), machineId()].map(normalizeHost))].sort();
11
11
  const enabled = new Set(devices.filter((device) => getConfigValue('watchdog.enabled', { device }).value === true));
12
12
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
13
- requireInteractiveSelection('watchdog devices', ['agents watchdog on', 'agents watchdog on --device <device>']);
13
+ requireInteractiveSelection('watchdog devices', ['agents watchdog enable', 'agents watchdog enable --device <device>']);
14
14
  }
15
15
  try {
16
16
  const { checkbox } = await import('@inquirer/prompts');
@@ -25,7 +25,7 @@ export async function runWatchdogSetupWizard() {
25
25
  setConfigValue('watchdog.enabled', on);
26
26
  continue;
27
27
  }
28
- const launch = getCliLaunch(['watchdog', on ? 'on' : 'off', '--device', device]);
28
+ const launch = getCliLaunch(['watchdog', on ? 'enable' : 'disable', '--device', device]);
29
29
  const result = spawnSync(launch.command, launch.args, { stdio: 'inherit', env: process.env });
30
30
  if ((result.status ?? 1) !== 0)
31
31
  throw new Error(`Could not configure watchdog on ${device}`);
@@ -29,6 +29,7 @@ import { registerSetupWatchdogCommand, runWatchdogSetupWizard } from './setup-wa
29
29
  import { hasMintedSetupToken } from '../lib/auth-mint.js';
30
30
  import { registerAliasCommand } from './alias.js';
31
31
  import { registerBetaCommands } from './beta.js';
32
+ import { addUrlSchemeSubcommands } from './open.js';
32
33
  import { runPreferencesStep } from './setup-preferences.js';
33
34
  import { getConfiguredDefaultProfileName, getProfile, getAutoDetectedProfile, isProfileLaunchableHere } from '../lib/browser/profiles.js';
34
35
  import { listInstalledBrowsers } from '../lib/browser/chrome.js';
@@ -228,7 +229,7 @@ export async function runSetup(program, options = {}) {
228
229
  console.log(chalk.gray('Registered the agents:// URL scheme (artifact session deep links).'));
229
230
  }
230
231
  catch {
231
- // non-fatal — the user can run `agents open register` later.
232
+ // non-fatal — the user can run `agents setup url-scheme register` later.
232
233
  }
233
234
  if (options.suppressFooter)
234
235
  return;
@@ -382,6 +383,26 @@ export function registerSetupCommand(program) {
382
383
  registerSetupWatchdogCommand(setupCmd);
383
384
  registerAliasCommand(setupCmd);
384
385
  registerBetaCommands(setupCmd);
386
+ // `agents setup url-scheme register|unregister|status` — the canonical, visible
387
+ // home for the agents:// OS handler that routes artifact deep links to the
388
+ // machine-only `agents _callback` verb. Reuses the same builder the hidden
389
+ // `agents open` back-compat subcommands mount.
390
+ const urlScheme = setupCmd
391
+ .command('url-scheme')
392
+ .description('Register/unregister/status the agents:// OS URL-scheme handler for artifact session deep links.');
393
+ addUrlSchemeSubcommands(urlScheme, { hidden: false });
394
+ setHelpSections(urlScheme, {
395
+ examples: `
396
+ # Register the handler so agents:// links in rendered artifacts resume sessions
397
+ agents setup url-scheme register
398
+
399
+ # Check whether the handler is registered on this machine
400
+ agents setup url-scheme status
401
+
402
+ # Remove the handler
403
+ agents setup url-scheme unregister
404
+ `,
405
+ });
385
406
  setupCmd.command('status')
386
407
  .description('Show setup readiness for core, browser, computer, secrets, accounts, fleet, share, watchdog, and preferences.')
387
408
  .option('--json', 'print machine-readable JSON')
@@ -1,5 +1,5 @@
1
1
  // `agents artifacts share` — publish an HTML file to a shareable link. Signed-in
2
- // users hit the managed endpoint (share.agents-cli.sh) with the Phoenix bearer;
2
+ // users hit the managed endpoint (share.getrush.ai) with the Phoenix bearer;
3
3
  // otherwise the existing BYO Cloudflare R2 + Worker path. See cli/docs/observability.md.
4
4
  //
5
5
  // Registered under the `artifacts` group by commands/artifacts.ts; the
@@ -19,6 +19,7 @@ import { extractShareHttpError, formatShareHttpErrorDetail } from '../lib/share/
19
19
  import { phoenixIdBaseForDeploy, resolveShareBackend, shouldUseManaged, } from '../lib/share/backend.js';
20
20
  import { publishVisibility } from '../lib/storage/visibility.js';
21
21
  import { resolveGitHubUsername } from '../lib/git.js';
22
+ import { refreshSessionAvatar } from '../lib/identity/index.js';
22
23
  import { setHelpSections } from '../lib/help.js';
23
24
  import { showUrl } from '../lib/open-url.js';
24
25
  export function formatSharePublishResult(result, json = false) {
@@ -548,7 +549,7 @@ function registerShareDeleteOptions(cmd) {
548
549
  }
549
550
  const SHARE_DELETE_EXAMPLES = `
550
551
  # Delete by full URL — also takes down the sibling OG cover and any retained revisions
551
- agents artifacts share delete https://share.agents-cli.sh/octocat/my-plan-a1b2
552
+ agents artifacts share delete https://share.getrush.ai/octocat/my-plan-a1b2
552
553
 
553
554
  # Delete by <user>/<slug>, or a bare slug in your own namespace
554
555
  agents artifacts share delete octocat/my-plan-a1b2
@@ -581,7 +582,7 @@ const SHARE_DELETE_NOTES = `
581
582
  `;
582
583
  const SHARE_VISIBILITY_EXAMPLES = `
583
584
  # Take a public page private to just you (Phoenix session required for me/org)
584
- agents artifacts share visibility https://share.agents-cli.sh/octocat/q3-plan me
585
+ agents artifacts share visibility https://share.getrush.ai/octocat/q3-plan me
585
586
 
586
587
  # Make it public again — by <user>/<slug> or a bare slug in your namespace
587
588
  agents artifacts share visibility octocat/q3-plan public
@@ -627,6 +628,7 @@ export function registerShareCommands(artifactsCmd) {
627
628
  .argument('[file]', 'file to publish (HTML or any static asset)')
628
629
  .option('--slug <slug>', 'URL slug override (default: stable slug of the artifact title, then filename)')
629
630
  .option('--github-user <user>', 'GitHub username for the share namespace (default: resolved from gh/git config; ignored on the managed endpoint)')
631
+ .option('--handle <name>', 'managed only: publish under an alternate public handle instead of the email local-part (default: your email local-part). Escape hatch when your derived handle is taken, or for a vanity namespace — the Worker binds it with the same first-writer claim')
630
632
  .option('--expire <spec>', "auto-expire (default 30d). e.g. 12h, 30d, 2026-08-01, or 'never'")
631
633
  .addOption(new Option('--visibility <level>', 'public | unlisted | private | me | org. Default when signed in: me (owner-only, Phoenix-gated); use org for your whole email domain, public to opt into the gallery. unlisted is an UNAUTHENTICATED capability URL; private is token-gated (404 without the key). All but public are hidden from the gallery')
632
634
  .choices([...PUBLISH_VISIBILITY_LEVELS]))
@@ -669,9 +671,16 @@ export function registerShareCommands(artifactsCmd) {
669
671
  if (visibility === 'unlisted' && !opts.json) {
670
672
  console.error(chalk.yellow(`⚠ ${unlistedNotPrivateWarning()}`));
671
673
  }
674
+ // Managed publish: fill the session's hosted avatar (one /auth/me call,
675
+ // no-op when already known) so the attribution bar can show the OAuth
676
+ // profile image — sessions written before the CLI tracked avatars only
677
+ // carry token/email/userId (PHNX-3547).
678
+ if (shouldUseManaged({}))
679
+ await refreshSessionAvatar();
672
680
  const result = await publishFile(file, {
673
681
  slug: opts.slug,
674
682
  githubUser: opts.githubUser,
683
+ handle: opts.handle,
675
684
  expire: opts.expire,
676
685
  visibility,
677
686
  unlisted: visibility === 'unlisted',
@@ -720,18 +729,25 @@ export function registerShareCommands(artifactsCmd) {
720
729
 
721
730
  # Republish the same title to the same URL without retaining a revision
722
731
  agents artifacts share ./out/plan.html --no-revision
732
+
733
+ # Your derived handle is taken (or you want a vanity namespace)
734
+ agents artifacts share ./out/plan.html --handle acme-muqsit
723
735
  ${SHARE_DELETE_EXAMPLES}
724
736
  # One-time setup (or join an existing endpoint)
725
737
  agents artifacts setup
726
- agents artifacts share join https://share.agents-cli.sh
738
+ agents artifacts share join https://share.getrush.ai
727
739
 
728
740
  # Push a worker-template.ts change out to an already-provisioned endpoint
729
741
  agents artifacts share update
730
742
  `,
731
743
  notes: `
732
- Signed-in users publish to the managed endpoint (share.agents-cli.sh/<handle>/…)
744
+ Signed-in users publish to the managed endpoint (share.getrush.ai/<handle>/…)
733
745
  with the Phoenix session — no Cloudflare account, bucket, or write token. The
734
- handle is the local-part of the signed-in email. The default slug is derived
746
+ handle is the local-part of the signed-in email; --handle publishes under an
747
+ alternate public namespace instead (first-writer-claimed like the derived one —
748
+ the recovery path when your derived handle belongs to another account; a
749
+ same-email re-login re-binds your old handle automatically). The default slug
750
+ is derived
735
751
  deterministically from the HTML <title> or Markdown frontmatter title, then
736
752
  the filename, so republishing the same title updates the same URL. --slug is
737
753
  an optional override. Without a session, the existing BYO Cloudflare path still
@@ -846,7 +862,7 @@ ${SHARE_DELETE_NOTES}
846
862
  setHelpSections(shareOpenCmd, {
847
863
  examples: `agents artifacts share open q3-plan # open your page, signed in, ready to re-scope
848
864
  agents artifacts share open octocat/q3-plan
849
- agents artifacts share open https://share.agents-cli.sh/octocat/q3-plan --no-open`,
865
+ agents artifacts share open https://share.getrush.ai/octocat/q3-plan --no-open`,
850
866
  notes: `The shared page's visibility chip is an interactive control only for the signed-in owner; a plain link has no login, so the chip renders as a static cue. This mints a short-lived, single-use login ticket (self-signed by your share Worker) and opens the page with it, so the chip becomes a dropdown you can switch in place.
851
867
  Managed (Phoenix) endpoints only — run 'agents auth login' first. If the mint 501s/404s, your Worker predates the feature: run 'agents artifacts share update' to redeploy it.
852
868
  Prefer to change visibility without a browser? Use 'agents artifacts share visibility <target> <level>'.`,
@@ -854,7 +870,7 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
854
870
  shareCmd
855
871
  .command('join')
856
872
  .description('Use an existing synced share endpoint and write token (no provisioning).')
857
- .argument('[baseUrl]', 'base URL of the endpoint, e.g. https://share.agents-cli.sh')
873
+ .argument('[baseUrl]', 'base URL of the endpoint, e.g. https://share.getrush.ai')
858
874
  .option('--token <token>', 'write token (else SHARE_WRITE_TOKEN env, existing bundle, or prompt)')
859
875
  .action(async (baseUrl, opts) => {
860
876
  try {
@@ -1039,7 +1055,7 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
1039
1055
  Use --scope unlisted|me|org or --all to include your hidden pages; those scopes
1040
1056
  send the owner's bearer and a 'scope=mine' hint to the Worker's JSON listing route,
1041
1057
  which returns hidden pages ONLY after verifying the caller owns the namespace.
1042
- Signed-in users list the managed endpoint (share.agents-cli.sh/<handle>); otherwise
1058
+ Signed-in users list the managed endpoint (share.getrush.ai/<handle>); otherwise
1043
1059
  BYO. It reads the endpoint's JSON listing route, which ships with the current
1044
1060
  Worker template. If a BYO Worker predates this feature the command says so and
1045
1061
  points you at 'agents artifacts share update' (RUSH-2449) rather than returning a
@@ -1138,7 +1154,7 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
1138
1154
 
1139
1155
  # By <user>/<slug> or full URL, same target forms as 'agents artifacts unshare'
1140
1156
  agents artifacts share revisions octocat/q3-report
1141
- agents artifacts share revisions https://share.agents-cli.sh/octocat/q3-report
1157
+ agents artifacts share revisions https://share.getrush.ai/octocat/q3-report
1142
1158
 
1143
1159
  # Machine-readable, and a bare slug resolved against another namespace
1144
1160
  agents artifacts share revisions q3-report --revisions-json
@@ -1,3 +1,4 @@
1
+ import { withAliases } from '../lib/verbs.js';
1
2
  import chalk from 'chalk';
2
3
  import ora from 'ora';
3
4
  import * as fs from 'fs';
@@ -41,8 +42,8 @@ When to use:
41
42
  - Specialization: install domain skills (rush-product-knowledge, rdev) per project
42
43
  - Version isolation: install different skills to different versions for experimentation
43
44
  `);
44
- skillsCmd
45
- .command('list [agent]')
45
+ withAliases(skillsCmd
46
+ .command('list [agent]'), 'list')
46
47
  .description('Show which skills are installed and which agent versions they are synced to')
47
48
  .option('-a, --agent <agent>', 'Filter to a specific agent (alternative to positional arg)')
48
49
  .option('--json', 'Emit machine-readable JSON instead of the table/picker')
@@ -302,8 +303,8 @@ Examples:
302
303
  process.exit(1);
303
304
  }
304
305
  });
305
- skillsCmd
306
- .command('remove [name]')
306
+ withAliases(skillsCmd
307
+ .command('remove [name]'), 'remove')
307
308
  .description('Delete a skill from central storage (interactive picker if no name given)')
308
309
  .addHelpText('after', `
309
310
  Examples:
@@ -416,8 +417,8 @@ Examples:
416
417
  console.error(chalk.gray('Use: agents prune cleanup skills (or `agents prune cleanup` for everything)'));
417
418
  process.exit(1);
418
419
  });
419
- skillsCmd
420
- .command('view [name]')
420
+ withAliases(skillsCmd
421
+ .command('view [name]'), 'view')
421
422
  .description('Read skill metadata (name, description, rules count)')
422
423
  .addHelpText('after', `
423
424
  Examples:
@@ -5,6 +5,7 @@
5
5
  * installing, and removing lightweight agent definitions (AGENT.md files)
6
6
  * that parent agents can spawn for focused subtasks.
7
7
  */
8
+ import { withAliases } from '../lib/verbs.js';
8
9
  import chalk from 'chalk';
9
10
  import ora from 'ora';
10
11
  import * as fs from 'fs';
@@ -66,8 +67,8 @@ When to use:
66
67
  });
67
68
  };
68
69
  // agents subagents list
69
- subagentsCmd
70
- .command('list')
70
+ withAliases(subagentsCmd
71
+ .command('list'), 'list')
71
72
  .description('Show subagents in a table with sync status across agent versions')
72
73
  .option('--json', 'Emit machine-readable JSON instead of the table/picker')
73
74
  .addHelpText('after', `
@@ -77,8 +78,8 @@ Examples:
77
78
  `)
78
79
  .action(runList);
79
80
  // agents subagents view <name>
80
- subagentsCmd
81
- .command('view [name]')
81
+ withAliases(subagentsCmd
82
+ .command('view [name]'), 'view')
82
83
  .description('Show details for a specific subagent (use "list" to see all)')
83
84
  .addHelpText('after', `
84
85
  Examples:
@@ -236,8 +237,8 @@ Examples:
236
237
  console.log();
237
238
  });
238
239
  // agents subagents remove [name]
239
- subagentsCmd
240
- .command('remove [name]')
240
+ withAliases(subagentsCmd
241
+ .command('remove [name]'), 'remove')
241
242
  .description('Delete a subagent from central storage and unsync from all agent versions')
242
243
  .option('-y, --yes', 'Skip confirmation prompt')
243
244
  .addHelpText('after', `
@@ -40,6 +40,7 @@ import { shouldAutoClaimCentralProfile } from '../lib/browser/profiles.js';
40
40
  import { isVersionInstalled, syncResourcesToVersion, parseAgentSpec, resolveVersion, resolveVersionAlias, listInstalledVersions, healDanglingVersionPointers, getAvailableResources, getActuallySyncedResources, getProjectOnlyResources, getNewResources, hasNewResources, promptResourceSelection, promptNewResourceSelection, buildSelection, mergeRepoScopedSelections, listRepoNames, getVersionHomePath, } from '../lib/installations/versions.js';
41
41
  import { capableAgents } from '../lib/capabilities.js';
42
42
  import { parseHookManifest, registerHooksToSettings } from '../lib/hooks/install.js';
43
+ import { repairAfterSync, renderRepairAfterSync, repairHadFailures, repairChangedAnything, repairAfterSyncJson } from '../lib/reconcile-and-repair.js';
43
44
  import { compileRulesForProject } from '../lib/rules/compile.js';
44
45
  import { runLaunchSync } from '../lib/project-launch.js';
45
46
  import { formatKeptProjectResources } from '../lib/project-resources.js';
@@ -197,6 +198,7 @@ export function registerSyncCommand(program) {
197
198
  .option('--secrets', 'Umbrella: pull encrypted secret bundles from the remote', false)
198
199
  .option('--cloud', 'Umbrella: fetch all remote state but skip the local reconcile', false)
199
200
  .option('--local', "Umbrella: reconcile resources into installed agents only (no fetch)", false)
201
+ .option('--prune-clis', 'Umbrella: also purge stale/legacy agents-cli installs (npx-cache, pre-1.22.30, unsafe helper) when a fixed peer exists. DESTRUCTIVE and off by default — the purge never runs on a routine sync.', false)
200
202
  .action(async (agentSpec, repo, opts) => {
201
203
  await runSync(agentSpec, repo, opts);
202
204
  });
@@ -412,6 +414,7 @@ async function runInteractiveReconcile(opts, outLog, errLog) {
412
414
  // register hooks so synced hook scripts fire.
413
415
  const hookManifest = parseHookManifest();
414
416
  const hookCapable = new Set(capableAgents('hooks'));
417
+ const touched = [];
415
418
  for (const agentId of agents) {
416
419
  const version = resolveVersion(agentId, cwd) || listInstalledVersions(agentId).slice(-1)[0];
417
420
  if (!version)
@@ -421,7 +424,24 @@ async function runInteractiveReconcile(opts, outLog, errLog) {
421
424
  if (result.hooks && hookCapable.has(agentId) && Object.keys(hookManifest).length > 0) {
422
425
  registerHooksToSettings(agentId, getVersionHomePath(agentId, version), hookManifest);
423
426
  }
427
+ touched.push({ agent: agentId, version });
424
428
  }
429
+ // Post-reconcile repair (the superset of the old `doctor --fix`): heal live-home
430
+ // gaps, re-wire hooks the diff left behind, and repair managed hook runtime
431
+ // shims for exactly the versions this reconcile touched. Always interactive
432
+ // here, so render its detail freely.
433
+ for (const t of touched) {
434
+ const repair = await repairAfterSync({ agent: t.agent, versions: [t.version], cwd });
435
+ renderRepairAfterSync(repair, outLog);
436
+ }
437
+ // Bare `agents sync` at a TTY is the umbrella verb — after per-agent repair,
438
+ // run one no-agent pass. This purges stale/legacy agents-cli copies ONLY when
439
+ // the user passed `--prune-clis` (the purge is never automatic); otherwise it is
440
+ // a cheap re-diff no-op over the just-reconciled homes.
441
+ const umbrellaRepair = await repairAfterSync({ cwd, pruneClis: !!opts.pruneClis });
442
+ renderRepairAfterSync(umbrellaRepair, outLog);
443
+ if (repairHadFailures(umbrellaRepair))
444
+ process.exitCode = 1;
425
445
  }
426
446
  /**
427
447
  * Drain the legacy central `browser:` tombstone during `agents sync` (PHNX-3315).
@@ -508,11 +528,21 @@ async function runUmbrella(opts, quiet, outLog, errLog, json = false) {
508
528
  const residual = result.reconciled
509
529
  ? verifyReconciled(result.reconciledVersions.map((r) => ({ agent: r.agent, version: r.version })), cwd)
510
530
  : [];
531
+ // Post-reconcile repair, machine-wide: no agent scope → heal every installed
532
+ // agent, re-wire hooks the diff left behind, and repair managed hook runtime
533
+ // shims. The stale-CLI purge runs ONLY with `--prune-clis` (never automatic).
534
+ // Skipped under --cloud (fetch-only: nothing was reconciled to repair).
535
+ const repair = opts.cloud ? null : await repairAfterSync({ cwd, pruneClis: !!opts.pruneClis });
536
+ // A repair that left something for a human (an unresolvable shim, a failed
537
+ // rewire/purge) is a non-zero outcome, same as the deleted `doctor --fix`.
538
+ const repairFailed = repair !== null && repairHadFailures(repair);
539
+ if (repairFailed)
540
+ process.exitCode = 1;
511
541
  if (json) {
512
542
  emitJson({
513
- // A refused resource OR residual drift is not a clean sync (RUSH-2700 +
514
- // PHNX-3186).
515
- ok: result.declined.length === 0 && residual.length === 0,
543
+ // A refused resource, residual drift, OR a repair failure is not a clean
544
+ // sync (RUSH-2700 + PHNX-3186).
545
+ ok: result.declined.length === 0 && residual.length === 0 && !repairFailed,
516
546
  mode: 'umbrella',
517
547
  plan: result.plan,
518
548
  repos: result.repos,
@@ -521,6 +551,7 @@ async function runUmbrella(opts, quiet, outLog, errLog, json = false) {
521
551
  reconciled: result.reconciled,
522
552
  declined: result.declined,
523
553
  residualDrift: residual,
554
+ repair: repair ? repairAfterSyncJson(repair) : null,
524
555
  });
525
556
  return;
526
557
  }
@@ -545,6 +576,8 @@ async function runUmbrella(opts, quiet, outLog, errLog, json = false) {
545
576
  for (const e of errs)
546
577
  errLog(chalk.yellow(` ! ${e}`));
547
578
  printResidual(residual, errLog);
579
+ if (repair)
580
+ renderRepairAfterSync(repair, outLog);
548
581
  }
549
582
  }
550
583
  catch (err) {
@@ -765,15 +798,25 @@ async function runSync(agentSpec, repoArg, opts) {
765
798
  : verifyReconciled(versions.map(({ version: v }) => ({ agent: agentId, version: v })), cwd);
766
799
  if (!quiet && !json)
767
800
  printResidual(residual, errLog);
801
+ // Post-reconcile repair over exactly the versions just reconciled (the
802
+ // superset of the old `doctor --fix`). Runs under --json too (fleet fan-out);
803
+ // only its human detail is gated on !quiet && !json.
804
+ const allRepair = await repairAfterSync({ agent: agentId, versions: installed, cwd });
805
+ if (!quiet && !json)
806
+ renderRepairAfterSync(allRepair, outLog);
807
+ const allRepairFailed = repairHadFailures(allRepair);
808
+ if (allRepairFailed)
809
+ process.exitCode = 1;
768
810
  if (json) {
769
811
  emitJson({
770
- // Any version that refused a write, or any residual drift after the
771
- // reconcile, makes the whole run not-ok (RUSH-2700 + PHNX-3186).
772
- ok: versions.every(({ result }) => result.declined.length === 0) && residual.length === 0,
812
+ // Any version that refused a write, any residual drift, or a repair
813
+ // failure makes the whole run not-ok (RUSH-2700 + PHNX-3186).
814
+ ok: versions.every(({ result }) => result.declined.length === 0) && residual.length === 0 && !allRepairFailed,
773
815
  mode: 'agent-all',
774
816
  agent: agentId,
775
817
  repo: repoScope,
776
818
  residualDrift: residual,
819
+ repair: repairAfterSyncJson(allRepair),
777
820
  ...healedPointers,
778
821
  versions: versions.map(({ version: v, result }) => ({
779
822
  version: v,
@@ -896,11 +939,17 @@ async function runSync(agentSpec, repoArg, opts) {
896
939
  return;
897
940
  }
898
941
  const result = syncResourcesToVersion(agentId, version, scoped, { projectDir, cwd, force, prune: !!repoScope, allowExecSurfaces: !!opts.allowExecSurfaces });
942
+ const scopedRepair = await repairAfterSync({ agent: agentId, versions: [version], cwd });
943
+ const scopedRepairFailed = repairHadFailures(scopedRepair);
944
+ if (scopedRepairFailed)
945
+ process.exitCode = 1;
899
946
  if (json) {
900
- emitJson(agentSyncJson(agentId, version, result, repoScope));
947
+ const base = agentSyncJson(agentId, version, result, repoScope);
948
+ emitJson({ ...base, ok: base.ok === true && !scopedRepairFailed, repair: repairAfterSyncJson(scopedRepair) });
901
949
  }
902
950
  else if (!quiet) {
903
951
  printSyncDetail(result, agentId, version, cwd);
952
+ renderRepairAfterSync(scopedRepair, outLog);
904
953
  }
905
954
  return;
906
955
  }
@@ -934,8 +983,21 @@ async function runSync(agentSpec, repoArg, opts) {
934
983
  selection = userSelection;
935
984
  }
936
985
  else if (!force) {
937
- outLog(chalk.gray(`${agentLabel(agentId)}@${version} is already in sync.`));
938
- outLog(chalk.gray('Run with --force to re-sync, or --yes to bypass this check.'));
986
+ // Tracked resources match source — but a generated hook shim can still be
987
+ // broken on an otherwise in-sync version (the yosemite-s1 class), and
988
+ // syncResourcesToVersion never generates it. Run the repair pass BEFORE
989
+ // the early return so a broken shim is still fixed on bare
990
+ // `agents sync <agent>`, then report what (if anything) it touched.
991
+ const repair = await repairAfterSync({ agent: agentId, versions: [version], cwd });
992
+ if (repairChangedAnything(repair)) {
993
+ renderRepairAfterSync(repair, outLog);
994
+ }
995
+ else {
996
+ outLog(chalk.gray(`${agentLabel(agentId)}@${version} is already in sync.`));
997
+ outLog(chalk.gray('Run with --force to re-sync, or --yes to bypass this check.'));
998
+ }
999
+ if (repairHadFailures(repair))
1000
+ process.exitCode = 1;
939
1001
  return;
940
1002
  }
941
1003
  // else: --force on a fully-synced version → selection stays undefined,
@@ -973,6 +1035,13 @@ async function runSync(agentSpec, repoArg, opts) {
973
1035
  // picked kinds, so the rest legitimately still differs and is not this run's
974
1036
  // failure. This is the exact path `agents sync <agent>[@version]` takes.
975
1037
  const residual = selection ? [] : verifyReconciled([{ agent: agentId, version }], cwd);
1038
+ // Post-reconcile repair over the single version just reconciled (the superset
1039
+ // of the old `doctor --fix`). Runs under --json too; only its human detail is
1040
+ // gated on the non-quiet path below.
1041
+ const singleRepair = await repairAfterSync({ agent: agentId, versions: [version], cwd });
1042
+ const singleRepairFailed = repairHadFailures(singleRepair);
1043
+ if (singleRepairFailed)
1044
+ process.exitCode = 1;
976
1045
  // Compile project-scope rules into the workspace itself so each agent's
977
1046
  // native loader picks up cwd/<INSTRUCTIONS_FILE>. projectDir is the
978
1047
  // .agents/ directory; the workspace root is its parent.
@@ -985,8 +1054,9 @@ async function runSync(agentSpec, repoArg, opts) {
985
1054
  const base = agentSyncJson(agentId, version, result);
986
1055
  emitJson({
987
1056
  ...base,
988
- ok: base.ok === true && residual.length === 0,
1057
+ ok: base.ok === true && residual.length === 0 && !singleRepairFailed,
989
1058
  residualDrift: residual,
1059
+ repair: repairAfterSyncJson(singleRepair),
990
1060
  ...healedPointers,
991
1061
  projectCompile: projectCompile
992
1062
  ? {
@@ -1004,6 +1074,7 @@ async function runSync(agentSpec, repoArg, opts) {
1004
1074
  // ---------- 6. Detailed output ----------
1005
1075
  printSyncDetail(result, agentId, version, cwd);
1006
1076
  printResidual(residual, errLog);
1077
+ renderRepairAfterSync(singleRepair, outLog);
1007
1078
  if (projectCompile?.compiled) {
1008
1079
  const linkInfo = projectCompile.symlinks.length > 0
1009
1080
  ? ` (+ ${projectCompile.symlinks.join(', ')})`
@@ -1287,6 +1287,7 @@ export async function collectAgentsJson(filterAgentId, resourceSections, opts) {
1287
1287
  const usageKey = getUsageLookupKey(info);
1288
1288
  const usageInfo = usageKey ? usageByKey.get(usageKey) : undefined;
1289
1289
  const snapshot = usageInfo?.snapshot ?? null;
1290
+ const authHealth = authCache[authCacheKey(host, agentId, version)];
1290
1291
  const entry = {
1291
1292
  version,
1292
1293
  isDefault: version === globalDefault,
@@ -1298,13 +1299,15 @@ export async function collectAgentsJson(filterAgentId, resourceSections, opts) {
1298
1299
  // `collectRunCandidates` uses locally, so remote `--device auto` placement
1299
1300
  // is gated on identical launchability (PHNX-3466).
1300
1301
  launchable: isLaunchableSignedIn(info.signedIn, credentialPresence(agentId, home)),
1301
- authVerdict: authCache[authCacheKey(host, agentId, version)]?.verdict ?? null,
1302
+ authVerdict: authHealth?.verdict ?? null,
1303
+ authCheckedAt: authHealth?.checkedAt ?? null,
1302
1304
  email: info.email,
1303
1305
  accountId: info.accountId,
1304
1306
  organizationType: info.organizationType ?? null,
1305
1307
  organizationName: info.organizationName ?? null,
1306
1308
  plan: info.plan,
1307
1309
  usageStatus: info.usageStatus,
1310
+ usageCapturedAt: snapshot?.capturedAt?.toISOString() ?? null,
1308
1311
  overageCredits: info.overageCredits,
1309
1312
  usageError: usageErrorForDisplay(usageInfo?.error),
1310
1313
  windows: snapshot
@@ -10,7 +10,7 @@
10
10
  * agents watchdog --nudge one tick, actually injects (explicit opt-in)
11
11
  * agents watchdog --watch manual poll loop (dry unless --nudge)
12
12
  * agents watchdog --json machine-readable tick output (for the menu-bar)
13
- * agents watchdog on|off turn the device-local daemon pass on/off
13
+ * agents watchdog enable|disable turn the device-local daemon pass on/off
14
14
  * agents watchdog policy <id> <p> per-session policy: off | keep | handsoff
15
15
  *
16
16
  * The agents daemon is the sole automatic watchdog scheduler. The menu bar reads
@@ -10,7 +10,7 @@
10
10
  * agents watchdog --nudge one tick, actually injects (explicit opt-in)
11
11
  * agents watchdog --watch manual poll loop (dry unless --nudge)
12
12
  * agents watchdog --json machine-readable tick output (for the menu-bar)
13
- * agents watchdog on|off turn the device-local daemon pass on/off
13
+ * agents watchdog enable|disable turn the device-local daemon pass on/off
14
14
  * agents watchdog policy <id> <p> per-session policy: off | keep | handsoff
15
15
  *
16
16
  * The agents daemon is the sole automatic watchdog scheduler. The menu bar reads
@@ -138,7 +138,7 @@ export function registerWatchdogCommand(program) {
138
138
  .command('watchdog')
139
139
  .description('Auto-nudge stalled agent terminals: detect stalls, resolve the exact split, inject "Continue." — no menu-bar needed.')
140
140
  .option('--nudge', 'Actually inject (default is a dry run that only reports what it would do)')
141
- .option('--watch', 'Manual poll loop: run a tick every --interval (dry unless --nudge; the always-on path is `watchdog on`)')
141
+ .option('--watch', 'Manual poll loop: run a tick every --interval (dry unless --nudge; the always-on path is `watchdog enable`)')
142
142
  .option('--interval <dur>', 'Poll interval in --watch mode (e.g. 30s, 1m)', '30s')
143
143
  .option('--stall <dur>', 'Idle time before a session counts as stalled', humanMs(DEFAULT_THRESHOLDS.stallMs))
144
144
  .option('--cooldown <dur>', 'Minimum time between nudges to the same session', humanMs(DEFAULT_THRESHOLDS.cooldownMs))
@@ -183,7 +183,7 @@ export function registerWatchdogCommand(program) {
183
183
  const intervalMs = durationMsOr(opts.interval, 30_000);
184
184
  if (!computeWillInject() && !opts.json) {
185
185
  console.log(chalk.yellow(`watchdog --watch is DETECT-ONLY. Pass --nudge to inject, ` +
186
- `or run 'agents watchdog on' for the daemon-owned pass.`));
186
+ `or run 'agents watchdog enable' for the daemon-owned pass.`));
187
187
  }
188
188
  // eslint-disable-next-line no-constant-condition
189
189
  while (true) {
@@ -213,7 +213,7 @@ export function registerWatchdogCommand(program) {
213
213
  agents watchdog --json
214
214
 
215
215
  # Turn on the daemon-owned watchdog pass (every three minutes)
216
- agents watchdog on
216
+ agents watchdog enable
217
217
 
218
218
  # Show device enablement, rotate config, and in-flight rotates
219
219
  agents watchdog status
@@ -261,7 +261,7 @@ export function registerWatchdogCommand(program) {
261
261
  'watchdog.rotate: off' to ~/.agents/agents.yaml; nudging stays on).
262
262
  State machine: ~/.agents/.cache/state/watchdog/rotate/<sessionId>.json.
263
263
 
264
- Always-on: 'agents watchdog on' enables one daemon-owned pass every three
264
+ Always-on: 'agents watchdog enable' enables one daemon-owned pass every three
265
265
  minutes on this device and reloads the daemon; 'off' disables it here.
266
266
  Defaults OFF. Per-session policy: off (ignore), keep (default), handsoff
267
267
  (detect + flag).
@@ -280,21 +280,21 @@ export function registerWatchdogCommand(program) {
280
280
  await reloadDaemonForRoutine(false);
281
281
  console.log(chalk.yellow('watchdog: OFF on this device'));
282
282
  };
283
- cmd.command('on')
283
+ cmd.command('enable')
284
+ .alias('on')
284
285
  .description('Enable the daemon watchdog pass on this device.')
285
286
  .action(async () => {
286
287
  await turnOn();
287
288
  });
288
- cmd.command('off')
289
+ cmd.command('disable')
290
+ .alias('off')
289
291
  .description('Disable the daemon watchdog pass on this device.')
290
292
  .action(async () => {
291
293
  await turnOff();
292
294
  });
293
- cmd.command('enable', { hidden: true }).action(turnOn);
294
- cmd.command('disable', { hidden: true }).action(turnOff);
295
295
  cmd.command('rotate <state>')
296
296
  .description('Turn in-place rotate of rate-limited sessions on|off (watchdog.rotate in agents.yaml). ' +
297
- 'Rotate-only: nudging stays on — unlike `watchdog off`, which disables the whole watchdog on this device.')
297
+ 'Rotate-only: nudging stays on — unlike `watchdog disable`, which disables the whole watchdog on this device.')
298
298
  .action((state) => {
299
299
  const s = state.toLowerCase();
300
300
  if (s !== 'on' && s !== 'off') {
@@ -6,4 +6,8 @@
6
6
  * rotated without changing the Tailscale Funnel config.
7
7
  */
8
8
  import type { Command } from 'commander';
9
+ import type { Server } from 'http';
10
+ import type { Socket } from 'net';
11
+ /** Stop accepting requests and destroy persistent connections so shutdown is bounded. */
12
+ export declare function closeWebhookServer(server: Server, sockets: Set<Socket>): Promise<void>;
9
13
  export declare function registerWebhooksCommand(program: Command): void;
@@ -1,6 +1,7 @@
1
1
  import * as path from 'path';
2
2
  import chalk from 'chalk';
3
3
  import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
4
+ import { closeServerBounded } from '../lib/secrets/agent.js';
4
5
  import { createFileDeliveryStore, startWebhookServer, waitForListening } from '../lib/triggers/webhook.js';
5
6
  import { getRuntimeStateDir } from '../lib/state.js';
6
7
  const DEFAULT_HOST = '127.0.0.1';
@@ -32,6 +33,13 @@ function readWebhookSecrets(bundleName) {
32
33
  }
33
34
  return secrets;
34
35
  }
36
+ /** Stop accepting requests and destroy persistent connections so shutdown is bounded. */
37
+ export async function closeWebhookServer(server, sockets) {
38
+ const closing = closeServerBounded(server);
39
+ for (const socket of sockets)
40
+ socket.destroy();
41
+ await closing;
42
+ }
35
43
  export function registerWebhooksCommand(program) {
36
44
  const webhooks = program
37
45
  .command('webhooks')
@@ -81,17 +89,27 @@ export function registerWebhooksCommand(program) {
81
89
  console.error(chalk.red(`${new Date().toISOString()} ${webhook.source}:${webhook.event} dispatch failed after ack: ${err.message}`));
82
90
  },
83
91
  });
92
+ const sockets = new Set();
93
+ server.on('connection', (socket) => {
94
+ sockets.add(socket);
95
+ socket.once('close', () => sockets.delete(socket));
96
+ });
84
97
  await waitForListening(server);
85
98
  const address = server.address();
86
99
  const bound = typeof address === 'object' && address ? address.port : port;
87
100
  console.log(`${chalk.green('agents webhooks')} ${chalk.dim('→')} ${chalk.cyan(`http://${opts.bind ?? DEFAULT_HOST}:${bound}`)}`);
88
101
  console.log(chalk.dim('signed · localhost by default · endpoints: /hooks/github, /hooks/linear, /hooks/slack · acks 202 then dispatches · Ctrl-C to stop'));
89
102
  console.log(chalk.dim('for a supervised receiver that survives reboot: agents daemon webhooks add --secrets-bundle <name>'));
90
- const shutdown = () => {
91
- server.close(() => process.exit(0));
103
+ let shuttingDown = false;
104
+ const shutdown = async () => {
105
+ if (shuttingDown)
106
+ return;
107
+ shuttingDown = true;
108
+ await closeWebhookServer(server, sockets);
109
+ process.exit(0);
92
110
  };
93
- process.on('SIGINT', shutdown);
94
- process.on('SIGTERM', shutdown);
111
+ process.on('SIGINT', () => { void shutdown(); });
112
+ process.on('SIGTERM', () => { void shutdown(); });
95
113
  }
96
114
  catch (err) {
97
115
  console.error(chalk.red(`Could not start webhook receiver: ${err.message}`));