@phnx-labs/agents-cli 1.22.66 → 1.22.69

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 (161) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +21 -8
  3. package/dist/bootstrap.js +6 -3
  4. package/dist/commands/browser.js +46 -21
  5. package/dist/commands/daemon-test-harness.d.ts +1 -1
  6. package/dist/commands/daemon-test-harness.js +2 -2
  7. package/dist/commands/daemon.js +21 -22
  8. package/dist/commands/exec.js +50 -0
  9. package/dist/commands/feed.js +20 -7
  10. package/dist/commands/monitors.js +5 -2
  11. package/dist/commands/projects.d.ts +26 -6
  12. package/dist/commands/projects.js +55 -22
  13. package/dist/commands/repo.js +57 -19
  14. package/dist/commands/resume.d.ts +16 -0
  15. package/dist/commands/resume.js +41 -8
  16. package/dist/commands/routines.js +42 -21
  17. package/dist/commands/send.js +29 -2
  18. package/dist/commands/sessions-inject.d.ts +58 -0
  19. package/dist/commands/sessions-inject.js +143 -7
  20. package/dist/commands/sessions-optimize.js +1 -1
  21. package/dist/commands/sessions-picker.js +1 -0
  22. package/dist/commands/sessions.js +4 -11
  23. package/dist/commands/share.d.ts +5 -3
  24. package/dist/commands/share.js +73 -20
  25. package/dist/commands/ssh.js +205 -2
  26. package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
  27. package/dist/lib/accounting/account-pool-collect.js +6 -4
  28. package/dist/lib/accounting/usage-ingest.js +4 -2
  29. package/dist/lib/accounting/usage-sync.d.ts +38 -93
  30. package/dist/lib/accounting/usage-sync.js +66 -210
  31. package/dist/lib/accounting/usage.d.ts +18 -5
  32. package/dist/lib/accounting/usage.js +165 -20
  33. package/dist/lib/auth-health.d.ts +8 -0
  34. package/dist/lib/auth-health.js +4 -4
  35. package/dist/lib/boot-profile.d.ts +14 -0
  36. package/dist/lib/boot-profile.js +66 -0
  37. package/dist/lib/browser/caller-identity.d.ts +12 -0
  38. package/dist/lib/browser/caller-identity.js +19 -0
  39. package/dist/lib/browser/ipc.d.ts +37 -32
  40. package/dist/lib/browser/ipc.js +146 -94
  41. package/dist/lib/browser/task-index.d.ts +10 -2
  42. package/dist/lib/browser/task-index.js +22 -3
  43. package/dist/lib/channels/providers/desktop.d.ts +5 -4
  44. package/dist/lib/channels/providers/desktop.js +5 -4
  45. package/dist/lib/claude-account-token.js +108 -4
  46. package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
  47. package/dist/lib/daemon/account-state-daemon-service.js +81 -18
  48. package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
  49. package/dist/lib/daemon/auth-sync-service.js +17 -6
  50. package/dist/lib/daemon/catchup-service.d.ts +51 -0
  51. package/dist/lib/daemon/catchup-service.js +51 -0
  52. package/dist/lib/daemon/daemon.d.ts +12 -22
  53. package/dist/lib/daemon/daemon.js +463 -176
  54. package/dist/lib/daemon/runner.js +2 -0
  55. package/dist/lib/daemon/service.d.ts +22 -4
  56. package/dist/lib/daemon/service.js +2 -2
  57. package/dist/lib/daemon/supervisor.d.ts +55 -15
  58. package/dist/lib/daemon/supervisor.js +119 -29
  59. package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
  60. package/dist/lib/daemon/usage-sync-service.js +22 -18
  61. package/dist/lib/daemon-health.js +36 -31
  62. package/dist/lib/daemon-services.d.ts +1 -1
  63. package/dist/lib/daemon-services.js +12 -2
  64. package/dist/lib/daemon-ticks.d.ts +9 -6
  65. package/dist/lib/daemon-ticks.js +14 -8
  66. package/dist/lib/devices/health.d.ts +38 -2
  67. package/dist/lib/devices/health.js +43 -5
  68. package/dist/lib/devices/registry.js +2 -0
  69. package/dist/lib/devices/worker-pick.d.ts +1 -1
  70. package/dist/lib/devices/worker-pick.js +4 -1
  71. package/dist/lib/exec.js +15 -0
  72. package/dist/lib/feed/watch.d.ts +3 -0
  73. package/dist/lib/feed/watch.js +13 -3
  74. package/dist/lib/feed-broadcast.d.ts +64 -5
  75. package/dist/lib/feed-broadcast.js +124 -22
  76. package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
  77. package/dist/lib/fleet-shared-repo-sync.js +333 -0
  78. package/dist/lib/fleet-shared-state.d.ts +38 -0
  79. package/dist/lib/fleet-shared-state.js +105 -0
  80. package/dist/lib/hosts/remote-cmd.d.ts +2 -0
  81. package/dist/lib/hosts/remote-cmd.js +12 -3
  82. package/dist/lib/lock-compromise.d.ts +8 -0
  83. package/dist/lib/lock-compromise.js +12 -0
  84. package/dist/lib/monitors/engine.d.ts +2 -1
  85. package/dist/lib/monitors/engine.js +27 -2
  86. package/dist/lib/monitors/sources/command.js +13 -3
  87. package/dist/lib/monitors/sources/failure.d.ts +32 -0
  88. package/dist/lib/monitors/sources/failure.js +52 -0
  89. package/dist/lib/monitors/sources/types.d.ts +9 -0
  90. package/dist/lib/owner-message.d.ts +12 -0
  91. package/dist/lib/owner-message.js +44 -0
  92. package/dist/lib/refresh-coordinator.js +2 -0
  93. package/dist/lib/run-trace-sync.d.ts +28 -0
  94. package/dist/lib/run-trace-sync.js +99 -0
  95. package/dist/lib/secrets/filestore.d.ts +4 -0
  96. package/dist/lib/secrets/filestore.js +164 -3
  97. package/dist/lib/secrets/push.d.ts +10 -0
  98. package/dist/lib/secrets/push.js +86 -7
  99. package/dist/lib/secrets/remote.d.ts +18 -6
  100. package/dist/lib/secrets/remote.js +29 -4
  101. package/dist/lib/secrets/reserved-sync.d.ts +28 -27
  102. package/dist/lib/secrets/reserved-sync.js +119 -101
  103. package/dist/lib/session/active.d.ts +13 -1
  104. package/dist/lib/session/active.js +5 -0
  105. package/dist/lib/session/actor-sidecar.d.ts +12 -0
  106. package/dist/lib/session/actor-sidecar.js +2 -0
  107. package/dist/lib/session/db.d.ts +16 -2
  108. package/dist/lib/session/db.js +73 -22
  109. package/dist/lib/session/discover.d.ts +12 -3
  110. package/dist/lib/session/discover.js +187 -26
  111. package/dist/lib/session/linear.d.ts +13 -0
  112. package/dist/lib/session/linear.js +44 -0
  113. package/dist/lib/session/live-metadata.js +1 -0
  114. package/dist/lib/session/parse.js +2 -3
  115. package/dist/lib/session/prompt.d.ts +17 -0
  116. package/dist/lib/session/prompt.js +35 -0
  117. package/dist/lib/session/recovery.d.ts +43 -6
  118. package/dist/lib/session/recovery.js +80 -10
  119. package/dist/lib/session/remote/remote-list.d.ts +17 -1
  120. package/dist/lib/session/remote/remote-list.js +29 -4
  121. package/dist/lib/session/remote/watch.d.ts +25 -2
  122. package/dist/lib/session/remote/watch.js +188 -11
  123. package/dist/lib/session/session-cache.d.ts +2 -1
  124. package/dist/lib/session/session-cache.js +1 -0
  125. package/dist/lib/session/state.js +11 -13
  126. package/dist/lib/session/types.d.ts +2 -0
  127. package/dist/lib/share/backend.d.ts +2 -2
  128. package/dist/lib/share/backend.js +20 -9
  129. package/dist/lib/share/delete.d.ts +5 -1
  130. package/dist/lib/share/delete.js +7 -2
  131. package/dist/lib/share/http-error.d.ts +52 -0
  132. package/dist/lib/share/http-error.js +65 -0
  133. package/dist/lib/share/publish.d.ts +67 -11
  134. package/dist/lib/share/publish.js +98 -16
  135. package/dist/lib/share/worker-template.js +105 -9
  136. package/dist/lib/smart-launch.js +27 -4
  137. package/dist/lib/ssh-exec.d.ts +2 -0
  138. package/dist/lib/ssh-exec.js +20 -4
  139. package/dist/lib/storage/index.d.ts +14 -0
  140. package/dist/lib/storage/index.js +14 -0
  141. package/dist/lib/storage/selection.d.ts +48 -0
  142. package/dist/lib/storage/selection.js +39 -0
  143. package/dist/lib/storage/visibility.d.ts +82 -0
  144. package/dist/lib/storage/visibility.js +99 -0
  145. package/dist/lib/teams/agents.js +3 -1
  146. package/dist/lib/teams/placement-probe.js +1 -0
  147. package/dist/lib/teams/registry.js +2 -0
  148. package/dist/lib/teams/scheduler.d.ts +8 -1
  149. package/dist/lib/teams/scheduler.js +4 -1
  150. package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
  151. package/dist/lib/testdata/daemon-health-writer.js +8 -0
  152. package/dist/lib/traces/backend.js +13 -2
  153. package/dist/lib/traces/sync.d.ts +7 -0
  154. package/dist/lib/traces/sync.js +9 -0
  155. package/dist/lib/usage-refresh.d.ts +8 -2
  156. package/dist/lib/usage-refresh.js +3 -3
  157. package/dist/lib/worktree/held.d.ts +166 -0
  158. package/dist/lib/worktree/held.js +368 -0
  159. package/package.json +2 -2
  160. package/dist/lib/account-state-service.d.ts +0 -21
  161. package/dist/lib/account-state-service.js +0 -60
@@ -11,11 +11,13 @@ import { Argument, Option } from 'commander';
11
11
  import chalk from 'chalk';
12
12
  import { DEFAULT_BUCKET_NAME, DEFAULT_CF_BUNDLE, DEFAULT_SHARE_DOMAIN, DEFAULT_WORKER_NAME, generateWriteToken, readCloudflareCreds, readShareConfig, readWriteToken, readWriteTokenEnv, readWriteTokenFromBundle, storeWriteToken, writeShareConfig, } from '../lib/share/config.js';
13
13
  import { addCustomDomain, configureBucketLifecycle, createBucket, deployWorker, enableWorkersDev, findZoneId, hashWorkerScript, putWorkerSecret, updateWorker, WORKER_PHOENIX_ID_BASE_SECRET, setWorkerSecret, } from '../lib/share/provision.js';
14
- import { publishFile, resolveShareUsername, parseMetaEntries, sanitizeLabel, resolveShareVisibility, scanShareContent, formatSensitiveContentError, SHARE_VISIBILITY_LEVELS, } from '../lib/share/publish.js';
14
+ import { publishFile, resolveShareUsername, parseMetaEntries, sanitizeLabel, scanShareContent, formatSensitiveContentError, unlistedNotPrivateWarning, SHARE_VISIBILITY_LEVELS, PUBLISH_VISIBILITY_LEVELS, } from '../lib/share/publish.js';
15
15
  import { deleteShare, resolveDeleteTarget } from '../lib/share/delete.js';
16
16
  import { renderWorkerBundle } from '../lib/share/worker-template.js';
17
17
  import { analyticsEnabled } from '../lib/share/analytics.js';
18
- import { phoenixIdBaseForDeploy, resolveShareBackend, } from '../lib/share/backend.js';
18
+ import { extractShareHttpError, formatShareHttpErrorDetail } from '../lib/share/http-error.js';
19
+ import { phoenixIdBaseForDeploy, resolveShareBackend, shouldUseManaged, } from '../lib/share/backend.js';
20
+ import { publishVisibility } from '../lib/storage/visibility.js';
19
21
  import { resolveGitHubUsername } from '../lib/git.js';
20
22
  import { setHelpSections } from '../lib/help.js';
21
23
  import { showUrl } from '../lib/open-url.js';
@@ -37,7 +39,11 @@ export function formatSharePublishResult(result, json = false) {
37
39
  lines.push(chalk.dim(' expires never'));
38
40
  const visibility = result.visibility ?? (result.unlisted ? 'unlisted' : 'public');
39
41
  if (visibility === 'unlisted') {
40
- lines.push(chalk.dim(' visibility: unlisted (noindex, hidden from gallery)'));
42
+ lines.push(chalk.dim(' visibility: unlisted (noindex, hidden from gallery — NOT authenticated)'));
43
+ }
44
+ else if (visibility === 'private') {
45
+ lines.push(chalk.dim(' visibility: private (token-gated — 404 without the key; hidden from gallery)'));
46
+ lines.push(chalk.yellow(' the key is in the URL above (?k=…) — treat the whole link as a secret'));
41
47
  }
42
48
  else if (visibility === 'me' || visibility === 'org') {
43
49
  lines.push(chalk.dim(` visibility: ${visibility} (login required, hidden from gallery)`));
@@ -57,6 +63,14 @@ export function shareTemplateStatus(cfg) {
57
63
  return cfg.templateHash === hashWorkerScript(renderWorkerBundle().script) ? 'current' : 'outdated';
58
64
  }
59
65
  export async function runShareEdit(target, opts) {
66
+ // Token-gated `private` can't be set in place: the metadata-edit route carries
67
+ // no viewer token, so re-stamping visibility=private alone would leave the page
68
+ // gated by a hash that doesn't exist — inaccessible to everyone (PHNX-3654).
69
+ // Fail loud pointing at the publish path, which mints the key.
70
+ if (opts.visibility === 'private') {
71
+ throw new Error("Can't change a page to 'private' in place — a token-gated link needs a fresh viewer key. " +
72
+ "Re-publish it with 'agents artifacts share <file> --protected' instead.");
73
+ }
60
74
  // Same public-listing gate as publish: label + every metadata value are
61
75
  // world-readable in the gallery and `share list --list-json`.
62
76
  if (opts.force !== true) {
@@ -158,7 +172,10 @@ export function parseShareListing(user, body) {
158
172
  host: item.host == null ? null : String(item.host),
159
173
  repo: item.repo == null ? null : String(item.repo),
160
174
  revisionCount: typeof item.revisionCount === 'number' ? item.revisionCount : 0,
161
- visibility: item.visibility === 'unlisted' || item.visibility === 'me' || item.visibility === 'org'
175
+ visibility: item.visibility === 'unlisted' ||
176
+ item.visibility === 'private' ||
177
+ item.visibility === 'me' ||
178
+ item.visibility === 'org'
162
179
  ? item.visibility
163
180
  : 'public',
164
181
  meta: parseMetaField(item.meta),
@@ -230,7 +247,8 @@ export async function runShareList(opts = {}) {
230
247
  throw new Error(OUTDATED_TEMPLATE_HINT);
231
248
  }
232
249
  if (res.status !== 200) {
233
- throw new Error(`Listing failed (${res.status}) for ${listUrl}. Check the endpoint is reachable, or that 'agents artifacts setup' / 'agents auth login' completed.`);
250
+ const detail = formatShareHttpErrorDetail(extractShareHttpError({ status: res.status, body: res.body }));
251
+ throw new Error(`Listing failed (${res.status}) for ${listUrl}${detail}. Check the endpoint is reachable, or that 'agents artifacts setup' / 'agents auth login' completed.`);
234
252
  }
235
253
  if (!/application\/json/i.test(res.contentType)) {
236
254
  // A 200 that isn't JSON means the old Worker ignored ?format=json and served
@@ -374,7 +392,8 @@ export async function runShareRevisions(target, opts = {}) {
374
392
  const fetchListing = opts.fetchListing ?? defaultListingFetch;
375
393
  const res = await fetchListing(revUrl);
376
394
  if (res.status !== 200) {
377
- throw new Error(`Revisions lookup failed (${res.status}) for ${revUrl}. Check the endpoint is reachable, or that 'agents artifacts setup' / 'agents auth login' completed.`);
395
+ const detail = formatShareHttpErrorDetail(extractShareHttpError({ status: res.status, body: res.body }));
396
+ throw new Error(`Revisions lookup failed (${res.status}) for ${revUrl}${detail}. Check the endpoint is reachable, or that 'agents artifacts setup' / 'agents auth login' completed.`);
378
397
  }
379
398
  if (!/application\/json/i.test(res.contentType)) {
380
399
  if (backend.kind === 'managed') {
@@ -609,11 +628,11 @@ export function registerShareCommands(artifactsCmd) {
609
628
  .option('--slug <slug>', 'URL slug override (default: stable slug of the artifact title, then filename)')
610
629
  .option('--github-user <user>', 'GitHub username for the share namespace (default: resolved from gh/git config; ignored on the managed endpoint)')
611
630
  .option('--expire <spec>', "auto-expire (default 30d). e.g. 12h, 30d, 2026-08-01, or 'never'")
612
- .addOption(new Option('--visibility <level>', 'public | unlisted | me | org (default public). unlisted is a capability URL; me/org require a Phoenix session and are hidden from the gallery')
613
- .choices(['public', 'unlisted', 'me', 'org'])
614
- .default('public'))
615
- .addOption(new Option('--unlisted', 'hidden alias of --visibility unlisted').hideHelp())
616
- .addOption(new Option('--private', 'hidden alias of --visibility unlisted').hideHelp())
631
+ .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
+ .choices([...PUBLISH_VISIBILITY_LEVELS]))
633
+ .addOption(new Option('--protected', 'token-gated link (= --visibility private): the URL carries a secret key and returns 404 without it — the authenticated alternative to --unlisted'))
634
+ .addOption(new Option('--unlisted', 'hidden alias of --visibility unlisted (obscurity, NOT authentication)').hideHelp())
635
+ .addOption(new Option('--private', 'hidden alias of --visibility unlisted (obscurity, NOT authentication — for real read-auth use --protected)').hideHelp())
617
636
  .option('--force', 'publish even when the file contains emails or credential-shaped strings')
618
637
  .option('--no-cover', 'skip the OG preview image (HTML pages get one by default)')
619
638
  .option('--no-analytics', 'skip injecting the Cloudflare Web Analytics beacon')
@@ -634,16 +653,29 @@ export function registerShareCommands(artifactsCmd) {
634
653
  }
635
654
  try {
636
655
  const meta = parseMetaEntries(opts.meta);
637
- const visibility = resolveShareVisibility({
656
+ // Private by default: an explicit flag wins; otherwise the backend-aware
657
+ // product default — `me` (owner-only) when signed in to Phoenix, `public`
658
+ // for a BYO bucket (the Worker refuses `me`/`org` without a Phoenix owner).
659
+ // `shouldUseManaged` reads the same session/BYO signals `publishFile` will
660
+ // resolve, so the default matches the backend the publish actually uses.
661
+ const visibility = publishVisibility({
638
662
  visibility: opts.visibility,
639
663
  unlisted: opts.unlisted === true || opts.private === true,
640
- });
664
+ protected: opts.protected === true,
665
+ }, shouldUseManaged({}) ? 'managed' : 'byo');
666
+ // unlisted (incl. its --private alias) is obscurity, not read-auth — warn
667
+ // loudly before the publish so a "private" link is never mistaken for a
668
+ // gated one (PHNX-3654). Skipped for --json so machine output stays clean.
669
+ if (visibility === 'unlisted' && !opts.json) {
670
+ console.error(chalk.yellow(`⚠ ${unlistedNotPrivateWarning()}`));
671
+ }
641
672
  const result = await publishFile(file, {
642
673
  slug: opts.slug,
643
674
  githubUser: opts.githubUser,
644
675
  expire: opts.expire,
645
676
  visibility,
646
677
  unlisted: visibility === 'unlisted',
678
+ protected: visibility === 'private',
647
679
  force: opts.force,
648
680
  cover: opts.cover,
649
681
  analytics: opts.analytics,
@@ -660,15 +692,25 @@ export function registerShareCommands(artifactsCmd) {
660
692
  });
661
693
  setHelpSections(shareCmd, {
662
694
  examples: `
663
- # Signed in? Just publish — no Cloudflare setup (managed endpoint)
695
+ # Signed in? Just publish — no Cloudflare setup (managed endpoint).
696
+ # Default is private: an owner-only 'me' link, Phoenix-gated.
664
697
  agents auth login
665
698
  agents artifacts share ./out/plan.html
666
699
 
700
+ # Share with everyone at your email domain (no org setup — the domain is it)
701
+ agents artifacts share ./out/plan.html --visibility org
702
+
703
+ # Opt into the public gallery with an OG preview card
704
+ agents artifacts share ./out/landing.html --visibility public
705
+
667
706
  # Hide from the public gallery (direct URL still works, noindex) and expire sooner
668
707
  agents artifacts share ./out/report.html --visibility unlisted --expire 12h
669
708
 
709
+ # Token-gated link — GET returns 404 without the secret ?k= key
710
+ agents artifacts share ./out/secret.html --protected
711
+
670
712
  # Permanent public page (the slug is derived from its title)
671
- agents artifacts share ./out/landing.html --expire never
713
+ agents artifacts share ./out/landing.html --visibility public --expire never
672
714
 
673
715
  # Optional slug override
674
716
  agents artifacts share ./out/report.html --slug q3-report --expire 7d
@@ -696,11 +738,21 @@ ${SHARE_DELETE_EXAMPLES}
696
738
  applies (setup / join). HTML is stored as one object: local images are inlined
697
739
  and Chrome-saved file:// TOC links become in-page hashes.
698
740
 
741
+ Private by default: signed in, a publish with no --visibility is 'me' —
742
+ owner-only, Phoenix-gated, not in the gallery. Pass --visibility org to share
743
+ with everyone at your email domain (no org to create — the domain is the whole
744
+ mechanism), or --visibility public to opt into the gallery with an OG card. A
745
+ BYO publish (no Phoenix session) still defaults to public, since me/org need a
746
+ Phoenix owner to gate on.
747
+
699
748
  Default expiry is 30d so an accidental publish decays. Pass --expire never for
700
749
  a permanent link. --visibility unlisted (hidden aliases: --unlisted / --private)
701
750
  hides the page from the public gallery and agents artifacts share list; the
702
751
  direct URL is still world-readable (unlisted, not secret) and GET sends
703
- X-Robots-Tag: noindex. --visibility me requires a Phoenix session and is only
752
+ X-Robots-Tag: noindex. --protected (= --visibility private) is token-gated
753
+ read-auth: the published URL carries a secret ?k= key (Authorization: Bearer
754
+ is accepted too) and GET returns 404 without it — treat the whole link as a
755
+ secret. --visibility me requires a Phoenix session and is only
704
756
  visible to the signed-in owner; --visibility org requires the same and is
705
757
  visible to members of the same Phoenix organization. A pre-publish scan
706
758
  refuses emails and credential-shaped strings unless --force is passed.
@@ -929,10 +981,10 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
929
981
  // so the listing filter is named `--scope` here; `--all` is the convenience
930
982
  // alias for `--scope all`.
931
983
  .option('--for-user <user>', 'GitHub username whose namespace to list (default: resolved from gh/git config)')
932
- .addOption(new Option('--scope <level>', 'visibility filter: public (default), unlisted, me, org, or all')
933
- .choices(['public', 'unlisted', 'me', 'org', 'all'])
984
+ .addOption(new Option('--scope <level>', 'visibility filter: public (default), unlisted, private, me, org, or all')
985
+ .choices(['public', 'unlisted', 'private', 'me', 'org', 'all'])
934
986
  .default('public'))
935
- .option('--all', "list every page including hidden unlisted/me/org shares (alias for --scope all)")
987
+ .option('--all', "list every page including hidden unlisted/private/me/org shares (alias for --scope all)")
936
988
  .option('--agent <name>', 'filter to shares published by this agent/harness (case-insensitive)')
937
989
  .option('--session <id>', 'filter to shares published from this session id')
938
990
  // Named --label-contains, not --label: `share <file>` (the parent) already owns
@@ -964,10 +1016,11 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
964
1016
  # Everything you've published, newest first
965
1017
  agents artifacts share list
966
1018
 
967
- # Include your hidden pages (unlisted / me / org) so you can see everything
1019
+ # Include your hidden pages (unlisted / private / me / org) so you can see everything
968
1020
  agents artifacts share list --all
969
1021
  agents artifacts share list --scope me
970
1022
  agents artifacts share list --scope unlisted
1023
+ agents artifacts share list --scope private
971
1024
 
972
1025
  # Machine-readable — e.g. pull every still-public URL with jq
973
1026
  agents artifacts share list --list-json | jq -r '.objects[].url'
@@ -37,7 +37,7 @@ import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
37
37
  import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, deviceIdentityArgs, fleetDialTarget, isAgentsBrowserDrive, markFleetRemote, writeAskpassShim, } from '../lib/devices/connect.js';
38
38
  import { ensureManagedKnownHostsDir, isHostPinned } from '../lib/devices/known-hosts.js';
39
39
  import { shouldSyncTerminfo, syncTerminfoToDevice, terminfoHostKey } from '../lib/devices/terminfo.js';
40
- import { fanOutDevices, fleetHealthSkip, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
40
+ import { fanOutDevices, fleetHealthSkip, planFleetTargets, remoteFleetTargets, runFleet, runOnDevice, runLocalCommand, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
41
41
  import { isRolloutSuccess, verifyFleetRollout, } from '../lib/devices/rollout-verify.js';
42
42
  import { fleetCapacity, fmtBytes, headroom, } from '../lib/devices/health.js';
43
43
  import { buildFleetHealthReport, renderFleetMatrix, renderFleetSummary, renderFleetWarnings, } from '../lib/devices/health-report.js';
@@ -61,6 +61,8 @@ import { runFleetLogin } from '../lib/fleet/remote-login.js';
61
61
  import { getConfigValue, listConfig, setConfigValue, unsetConfigValue, configKeySpec, autoPoolMode, configuredDeviceRole, listConfiguredDeviceRoles, setConfiguredDeviceRole, } from '../lib/device-config.js';
62
62
  import { filterAutoPool, listWorkerDevices } from '../lib/devices/pool.js';
63
63
  import { registerCommandGroups, setHelpSections } from '../lib/help.js';
64
+ import { isSelfHost } from '../lib/devices/self-host.js';
65
+ import { collectHeldWorktrees, collectHeldWorktreesUnder, summarizeHeld, aggregateHeld, pushStrandedBranch, } from '../lib/worktree/held.js';
64
66
  /** One-line summary of a device for `list`. `isSelf` marks the machine this
65
67
  * command is running on so it stands out from the rest of the tailnet.
66
68
  * `isInteractive` marks the configured interactive host (`devices config <name> interactive.host`). */
@@ -1130,7 +1132,7 @@ function registerDevicesCommands(program) {
1130
1132
  { title: 'Inspect', names: ['list', 'show', 'status', 'ping', 'harnesses', 'accounts', 'snapshot'] },
1131
1133
  { title: 'Disposable devices', names: ['lease'] },
1132
1134
  { title: 'Configure a device', names: ['config', 'describe', 'render'] },
1133
- { title: 'Fleet operations', names: ['update', 'run', 'login', 'capture', 'apply'] },
1135
+ { title: 'Fleet operations', names: ['update', 'run', 'login', 'capture', 'apply', 'worktrees'] },
1134
1136
  ]);
1135
1137
  devicesCmd
1136
1138
  .command('sync')
@@ -2363,6 +2365,207 @@ is reported \`failed\` (exit non-zero), never a stranded \`ok\`.
2363
2365
  .alias('kill')
2364
2366
  .description('Terminate a running dispatched task from this machine (SIGTERM the remote process group; marks it failed/143).')
2365
2367
  .action((id) => doDeviceTaskStop(id));
2368
+ const worktreesCmd = devicesCmd
2369
+ .command('worktrees')
2370
+ .description("Surface the held set of agent worktrees the sweep only counts, broken into buckets: unmerged-commits (real stranded work), uncommitted-changes, undeterminable. Read-only; --push publishes stranded branches.")
2371
+ .option('--json', 'emit the structured held set (device, repo, worktree, branch, reason, age, size) for machine callers / fleet aggregation')
2372
+ .option('--home <dir>', 'search root to discover repos under (default: $HOME)')
2373
+ .option('--repo <path>', 'scope to a single repo root instead of discovering')
2374
+ .option('--bucket <name>', 'show only one bucket: unmerged-commits | uncommitted-changes | undeterminable')
2375
+ .option('--fleet', 'fan out across every online device and aggregate the held set fleet-wide')
2376
+ .option('--push', 'PUBLISH each unmerged-commits branch that is on no remote (the safe recovery action); never deletes')
2377
+ .option('--yes', 'skip the confirmation prompt for --push')
2378
+ .action((opts) => runWorktreesHeld(opts));
2379
+ setHelpSections(worktreesCmd, {
2380
+ examples: `
2381
+ Inspect the residue:
2382
+ agents fleet worktrees # this box, grouped by bucket
2383
+ agents fleet worktrees --bucket unmerged-commits
2384
+ agents fleet worktrees --json # structured, for scripts
2385
+
2386
+ Fleet-wide:
2387
+ agents fleet worktrees --fleet # aggregate every online device
2388
+ agents fleet run 'agents fleet worktrees --json' # per-device composition
2389
+
2390
+ Recover stranded work (never deletes):
2391
+ agents fleet worktrees --push # publish on-no-remote branches
2392
+ `,
2393
+ notes: 'The nightly worktree-sweep (phnx-labs/.agents) reclaims merged worktrees and HOLDS the rest, reporting only a count. This surfaces that held set. unmerged-commits is the one that matters — a branch with commits on no remote is the PHNX-2951/PHNX-2732 stranded-work class; --push makes it visible. --push and --fleet are mutually exclusive (recovery is per-device on purpose).',
2394
+ });
2395
+ }
2396
+ const BUCKET_ORDER = ['unmerged-commits', 'uncommitted-changes', 'undeterminable'];
2397
+ const BUCKET_LABEL = {
2398
+ 'unmerged-commits': 'unmerged-commits (stranded work — push or open a PR)',
2399
+ 'uncommitted-changes': 'uncommitted-changes (dirty tree — needs review)',
2400
+ 'undeterminable': 'undeterminable (broken/locked — re-examine)',
2401
+ };
2402
+ function isHeldBucket(v) {
2403
+ return BUCKET_ORDER.includes(v);
2404
+ }
2405
+ function fmtWtSize(n) {
2406
+ if (n < 0)
2407
+ return '?';
2408
+ if (n < 1024)
2409
+ return `${n}B`;
2410
+ const units = ['K', 'M', 'G', 'T'];
2411
+ let v = n / 1024;
2412
+ let i = 0;
2413
+ while (v >= 1024 && i < units.length - 1) {
2414
+ v /= 1024;
2415
+ i++;
2416
+ }
2417
+ return `${v.toFixed(v < 10 ? 1 : 0)}${units[i]}`;
2418
+ }
2419
+ /**
2420
+ * `agents fleet worktrees` — surface the held set the sweep only counts. Never
2421
+ * removes anything; `--push` publishes on-no-remote stranded branches. This is
2422
+ * the CLI-native surfacing half of PHNX-3520 (the sweep itself lives in
2423
+ * phnx-labs/.agents; it still emits a bare count until a sibling change teaches
2424
+ * it to consume this).
2425
+ */
2426
+ async function runWorktreesHeld(opts) {
2427
+ if (opts.bucket && !isHeldBucket(opts.bucket)) {
2428
+ console.error(chalk.red(`Unknown bucket '${opts.bucket}'. Use one of: ${BUCKET_ORDER.join(', ')}`));
2429
+ process.exitCode = 1;
2430
+ return;
2431
+ }
2432
+ if (opts.fleet && opts.push) {
2433
+ console.error(chalk.red('--push and --fleet are mutually exclusive; run --push on the device that holds the work.'));
2434
+ process.exitCode = 1;
2435
+ return;
2436
+ }
2437
+ if (opts.fleet && opts.repo) {
2438
+ console.error(chalk.red('--repo scopes a single local repo and cannot combine with --fleet; drop one. To scope on each device, run `agents fleet run \'agents fleet worktrees --repo <path> --json\'`.'));
2439
+ process.exitCode = 1;
2440
+ return;
2441
+ }
2442
+ if (opts.fleet) {
2443
+ await runWorktreesHeldFleet(opts);
2444
+ return;
2445
+ }
2446
+ const searchHome = opts.home ?? os.homedir();
2447
+ const held = opts.repo
2448
+ ? await collectHeldWorktrees(path.resolve(opts.repo))
2449
+ : await collectHeldWorktreesUnder(searchHome);
2450
+ if (opts.push) {
2451
+ await runWorktreesPush(held, opts);
2452
+ return;
2453
+ }
2454
+ const shown = opts.bucket ? held.filter((w) => w.bucket === opts.bucket) : held;
2455
+ if (opts.json) {
2456
+ console.log(JSON.stringify(shown, null, 2));
2457
+ return;
2458
+ }
2459
+ const summary = summarizeHeld(shown);
2460
+ if (summary.total === 0) {
2461
+ console.log(chalk.green('No held worktrees — nothing stranded, dirty, or undeterminable.'));
2462
+ return;
2463
+ }
2464
+ for (const bucket of BUCKET_ORDER) {
2465
+ const items = summary.buckets[bucket];
2466
+ if (items.length === 0)
2467
+ continue;
2468
+ const color = bucket === 'unmerged-commits' ? chalk.yellow : chalk.gray;
2469
+ console.log('\n' + color(chalk.bold(`${BUCKET_LABEL[bucket]} — ${items.length}`)));
2470
+ for (const w of items.sort((a, b) => b.unmergedCommits - a.unmergedCommits)) {
2471
+ const detail = bucket === 'unmerged-commits'
2472
+ ? `${w.unmergedCommits} commit${w.unmergedCommits === 1 ? '' : 's'}, ${w.hasRemoteBranch ? 'on remote' : chalk.red('NO remote')}`
2473
+ : bucket === 'uncommitted-changes'
2474
+ ? `${w.dirtyFiles} dirty file${w.dirtyFiles === 1 ? '' : 's'}`
2475
+ : w.reason;
2476
+ console.log(` ${chalk.cyan(w.repoName + '/' + w.name).padEnd(48)} ${chalk.gray((w.branch ?? 'detached').padEnd(28))} ${detail} ${chalk.dim(`${w.ageDays}d · ${fmtWtSize(w.sizeBytes)}`)}`);
2477
+ }
2478
+ }
2479
+ const stranded = summary.buckets['unmerged-commits'].filter((w) => !w.hasRemoteBranch).length;
2480
+ if (stranded > 0) {
2481
+ console.log('\n' + chalk.yellow(`${stranded} branch${stranded === 1 ? '' : 'es'} with unmerged commits on no remote. Recover: agents fleet worktrees --push`));
2482
+ }
2483
+ }
2484
+ /** `--push`: publish every on-no-remote stranded branch. Never deletes. */
2485
+ async function runWorktreesPush(held, opts) {
2486
+ const candidates = held.filter((w) => w.bucket === 'unmerged-commits' && !w.hasRemoteBranch && w.branch);
2487
+ if (candidates.length === 0) {
2488
+ console.log(chalk.green('No stranded branches to publish (nothing with unmerged commits on no remote).'));
2489
+ return;
2490
+ }
2491
+ if (isInteractiveTerminal() && !opts.yes) {
2492
+ console.log(chalk.yellow(`About to push ${candidates.length} stranded branch${candidates.length === 1 ? '' : 'es'} to origin:`));
2493
+ for (const w of candidates)
2494
+ console.log(` ${chalk.cyan(w.repoName + '/' + w.name)} → ${w.branch}`);
2495
+ const { confirm } = await import('@inquirer/prompts');
2496
+ const go = await confirm({ message: 'Push these branches?', default: true }).catch(() => false);
2497
+ if (!go) {
2498
+ console.log(chalk.gray('Cancelled — nothing pushed.'));
2499
+ return;
2500
+ }
2501
+ }
2502
+ let pushed = 0;
2503
+ for (const w of candidates) {
2504
+ const res = await pushStrandedBranch(w.repo, w);
2505
+ if (res.pushed) {
2506
+ pushed++;
2507
+ console.log(chalk.green(` pushed ${w.repoName}/${w.name} (${res.branch})`));
2508
+ }
2509
+ else {
2510
+ console.log(chalk.yellow(` skipped ${w.repoName}/${w.name}: ${res.reason}`));
2511
+ }
2512
+ }
2513
+ console.log('\n' + chalk.green(`Published ${pushed}/${candidates.length} stranded branch${candidates.length === 1 ? '' : 'es'}.`));
2514
+ }
2515
+ /** `--fleet`: run this same read-only surface on every online device, aggregate. */
2516
+ async function runWorktreesHeldFleet(opts) {
2517
+ const reg = await loadDevices();
2518
+ const targets = planFleetTargets(reg);
2519
+ const online = targets.filter((t) => !t.skip);
2520
+ if (online.length === 0) {
2521
+ console.log(chalk.gray("No online devices. Run 'agents devices sync' first."));
2522
+ return;
2523
+ }
2524
+ const self = machineId();
2525
+ const remoteCmd = ['agents', 'fleet', 'worktrees', '--json'];
2526
+ if (opts.home)
2527
+ remoteCmd.push('--home', opts.home);
2528
+ const perDevice = [];
2529
+ for (const t of online) {
2530
+ const name = t.device.name;
2531
+ const isSelf = name === self || isSelfHost(name);
2532
+ const res = isSelf ? runLocalCommand(remoteCmd) : runOnDevice(t.device, remoteCmd);
2533
+ if (res.code !== 0) {
2534
+ console.error(chalk.gray(` ${name}: ${(res.stderr || 'failed').trim().slice(0, 120)}`));
2535
+ continue;
2536
+ }
2537
+ try {
2538
+ perDevice.push({ device: name, held: JSON.parse(res.stdout) });
2539
+ }
2540
+ catch {
2541
+ console.error(chalk.gray(` ${name}: unparseable output (older CLI?)`));
2542
+ }
2543
+ }
2544
+ // Apply the bucket filter to the per-device inputs BEFORE aggregating, so
2545
+ // `total`, per-`devices` totals, and `buckets` all describe the same filtered
2546
+ // set — zeroing buckets after the sum leaves `total` counting rows the output
2547
+ // no longer lists.
2548
+ const scoped = opts.bucket
2549
+ ? perDevice.map((d) => ({ device: d.device, held: d.held.filter((w) => w.bucket === opts.bucket) }))
2550
+ : perDevice;
2551
+ const agg = aggregateHeld(scoped);
2552
+ if (opts.json) {
2553
+ console.log(JSON.stringify(agg, null, 2));
2554
+ return;
2555
+ }
2556
+ console.log(chalk.bold(`Held worktrees across ${perDevice.length} device(s): ${agg.total}`));
2557
+ for (const bucket of BUCKET_ORDER) {
2558
+ const items = agg.buckets[bucket];
2559
+ if (items.length === 0)
2560
+ continue;
2561
+ const color = bucket === 'unmerged-commits' ? chalk.yellow : chalk.gray;
2562
+ console.log('\n' + color(chalk.bold(`${BUCKET_LABEL[bucket]} — ${items.length}`)));
2563
+ for (const w of items) {
2564
+ const dev = w.device ?? '?';
2565
+ const detail = bucket === 'unmerged-commits' ? `${w.unmergedCommits} commits, ${w.hasRemoteBranch ? 'on remote' : chalk.red('NO remote')}` : w.reason;
2566
+ console.log(` ${chalk.magenta(dev.padEnd(14))} ${chalk.cyan((w.repoName + '/' + w.name).padEnd(40))} ${chalk.gray((w.branch ?? 'detached').padEnd(24))} ${detail}`);
2567
+ }
2568
+ }
2366
2569
  }
2367
2570
  /**
2368
2571
  * `agents devices ps` — list tasks dispatched to devices (`agents run --device
@@ -25,10 +25,12 @@ export declare function foldRegistryCandidates(agent: AgentId, inputs: RunCandid
25
25
  * provider can authenticate `agent` and whose credential is present locally
26
26
  * (RUSH-3182). This is what makes a setup-token / API-key account balance-eligible.
27
27
  *
28
- * Passed as the `collect` argument to {@link resolveRunVersion} on the RUN path
29
- * ONLY — the other consumers of {@link collectRunCandidates} (watchdog, session
30
- * recovery, teams placement) keep calling it directly and are unaffected. The
31
- * existing {@link pickBalancedCandidate} selects over the combined list, so the
28
+ * Passed as the `collect` argument to {@link resolveRunVersion} on the RUN path,
29
+ * and used by session recovery (`resolveSessionRecovery`, PHNX-3626) so an
30
+ * origin-account limit can rotate to a healthy provider account and stay NATIVE.
31
+ * The other consumers of {@link collectRunCandidates} (watchdog, teams
32
+ * placement) keep calling it directly and are unaffected. The existing
33
+ * {@link pickBalancedCandidate} selects over the combined list, so the
32
34
  * revoked / rate-limit / exhausted logic is unchanged.
33
35
  */
34
36
  export declare function collectRunCandidatesForRun(agent: AgentId): Promise<RotateCandidate[]>;
@@ -63,10 +63,12 @@ export function foldRegistryCandidates(agent, inputs) {
63
63
  * provider can authenticate `agent` and whose credential is present locally
64
64
  * (RUSH-3182). This is what makes a setup-token / API-key account balance-eligible.
65
65
  *
66
- * Passed as the `collect` argument to {@link resolveRunVersion} on the RUN path
67
- * ONLY — the other consumers of {@link collectRunCandidates} (watchdog, session
68
- * recovery, teams placement) keep calling it directly and are unaffected. The
69
- * existing {@link pickBalancedCandidate} selects over the combined list, so the
66
+ * Passed as the `collect` argument to {@link resolveRunVersion} on the RUN path,
67
+ * and used by session recovery (`resolveSessionRecovery`, PHNX-3626) so an
68
+ * origin-account limit can rotate to a healthy provider account and stay NATIVE.
69
+ * The other consumers of {@link collectRunCandidates} (watchdog, teams
70
+ * placement) keep calling it directly and are unaffected. The existing
71
+ * {@link pickBalancedCandidate} selects over the combined list, so the
70
72
  * revoked / rate-limit / exhausted logic is unchanged.
71
73
  */
72
74
  export async function collectRunCandidatesForRun(agent) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The `agents __usage-ingest` receiver (PHNX-3392 usage-sync).
3
3
  *
4
- * A headed peer's daemon pipes a {@link UsageSyncPayload} JSON envelope to our
4
+ * A legacy headed peer may pipe a {@link UsageSyncPayload} JSON envelope to our
5
5
  * stdin; we merge its identity-keyed rows into the local usage cache newest-wins
6
6
  * ({@link ingestPeerClaudeUsageRows}). Hidden internal verb — intercepted in
7
7
  * index.ts before bootstrap, so it never triggers an update check or a detached
@@ -12,7 +12,9 @@
12
12
  * accepting a wrong shape, but a busy cache lock degrades to best-effort inside
13
13
  * `ingestPeerClaudeUsageRows` like every other cache writer.
14
14
  *
15
- * The payload arrives on stdin, EXCEPT on a Windows receiver: the `agents.ps1`
15
+ * New daemon ticks use the fleet-shared store instead. This compatibility
16
+ * receiver remains for older installed versions. The payload arrives on stdin,
17
+ * EXCEPT on a Windows receiver: the `agents.ps1`
16
18
  * shim does not forward ssh-piped stdin to the node process, so the pusher writes
17
19
  * the payload to a temp file and passes `agents __usage-ingest --from <path>`
18
20
  * (the same workaround the secrets push uses — `buildWindowsStdinImportCommand`).