@phnx-labs/agents-cli 1.22.26 → 1.22.28

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 +293 -0
  2. package/README.md +80 -27
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/bench.d.ts +2 -0
  5. package/dist/commands/bench.js +101 -0
  6. package/dist/commands/doctor.js +24 -10
  7. package/dist/commands/exec.js +71 -26
  8. package/dist/commands/hosts.js +4 -3
  9. package/dist/commands/insights.js +39 -5
  10. package/dist/commands/output.js +100 -25
  11. package/dist/commands/perf.d.ts +10 -0
  12. package/dist/commands/perf.js +14 -6
  13. package/dist/commands/projects.js +24 -10
  14. package/dist/commands/resume.d.ts +11 -0
  15. package/dist/commands/resume.js +51 -0
  16. package/dist/commands/routines.js +31 -4
  17. package/dist/commands/run-account-picker.d.ts +37 -0
  18. package/dist/commands/run-account-picker.js +101 -17
  19. package/dist/commands/secrets.js +86 -2
  20. package/dist/commands/sessions-picker.js +12 -7
  21. package/dist/commands/sessions.d.ts +73 -7
  22. package/dist/commands/sessions.js +391 -49
  23. package/dist/commands/ssh.js +184 -11
  24. package/dist/commands/teams.js +22 -11
  25. package/dist/index.js +16 -20
  26. package/dist/lib/activity.js +45 -99
  27. package/dist/lib/auth-health.d.ts +47 -3
  28. package/dist/lib/auth-health.js +74 -15
  29. package/dist/lib/bench/index.d.ts +4 -0
  30. package/dist/lib/bench/index.js +4 -0
  31. package/dist/lib/bench/runner.d.ts +16 -0
  32. package/dist/lib/bench/runner.js +111 -0
  33. package/dist/lib/bench/schema.d.ts +5 -0
  34. package/dist/lib/bench/schema.js +91 -0
  35. package/dist/lib/bench/storage.d.ts +5 -0
  36. package/dist/lib/bench/storage.js +32 -0
  37. package/dist/lib/bench/types.d.ts +40 -0
  38. package/dist/lib/bench/types.js +1 -0
  39. package/dist/lib/claude-account-token.d.ts +2 -0
  40. package/dist/lib/claude-account-token.js +56 -3
  41. package/dist/lib/cloud/host.js +1 -0
  42. package/dist/lib/crabbox/cli.d.ts +2 -0
  43. package/dist/lib/crabbox/cli.js +2 -0
  44. package/dist/lib/crabbox/lease.js +7 -1
  45. package/dist/lib/daemon.js +20 -0
  46. package/dist/lib/devices/connect.d.ts +2 -0
  47. package/dist/lib/devices/connect.js +7 -0
  48. package/dist/lib/devices/doctor-findings.d.ts +4 -1
  49. package/dist/lib/devices/doctor-findings.js +15 -1
  50. package/dist/lib/devices/harness-inventory.d.ts +97 -0
  51. package/dist/lib/devices/harness-inventory.js +0 -0
  52. package/dist/lib/devices/registry.d.ts +2 -0
  53. package/dist/lib/devices/resolve-target.d.ts +1 -0
  54. package/dist/lib/devices/resolve-target.js +9 -2
  55. package/dist/lib/devices/ssh-config.js +3 -0
  56. package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
  57. package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
  58. package/dist/lib/events.d.ts +1 -1
  59. package/dist/lib/events.js +2 -2
  60. package/dist/lib/exec.js +18 -8
  61. package/dist/lib/fleet/apply.js +9 -7
  62. package/dist/lib/fleet/remote-login.d.ts +4 -3
  63. package/dist/lib/fleet/remote-login.js +11 -9
  64. package/dist/lib/gemini-settings.d.ts +0 -1
  65. package/dist/lib/gemini-settings.js +12 -7
  66. package/dist/lib/git.d.ts +9 -3
  67. package/dist/lib/git.js +39 -22
  68. package/dist/lib/hooks/profile.d.ts +12 -1
  69. package/dist/lib/hooks/profile.js +6 -1
  70. package/dist/lib/hooks.d.ts +8 -5
  71. package/dist/lib/hooks.js +14 -7
  72. package/dist/lib/hosts/dispatch.d.ts +10 -0
  73. package/dist/lib/hosts/dispatch.js +85 -25
  74. package/dist/lib/hosts/logs.js +11 -1
  75. package/dist/lib/hosts/passthrough.d.ts +27 -0
  76. package/dist/lib/hosts/passthrough.js +19 -10
  77. package/dist/lib/hosts/progress.d.ts +12 -1
  78. package/dist/lib/hosts/progress.js +41 -8
  79. package/dist/lib/hosts/providers/devices.js +1 -0
  80. package/dist/lib/hosts/ready.d.ts +45 -4
  81. package/dist/lib/hosts/ready.js +124 -20
  82. package/dist/lib/hosts/reconcile.d.ts +2 -1
  83. package/dist/lib/hosts/reconcile.js +24 -7
  84. package/dist/lib/hosts/reconnect.d.ts +45 -12
  85. package/dist/lib/hosts/reconnect.js +94 -36
  86. package/dist/lib/hosts/registry.d.ts +2 -2
  87. package/dist/lib/hosts/registry.js +4 -5
  88. package/dist/lib/hosts/remote-cmd.d.ts +17 -0
  89. package/dist/lib/hosts/remote-cmd.js +29 -0
  90. package/dist/lib/hosts/tasks.d.ts +3 -0
  91. package/dist/lib/hosts/types.d.ts +1 -0
  92. package/dist/lib/hosts/types.js +3 -0
  93. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  94. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  95. package/dist/lib/menubar/install-menubar.d.ts +9 -6
  96. package/dist/lib/menubar/install-menubar.js +20 -9
  97. package/dist/lib/menubar/snapshot.js +15 -2
  98. package/dist/lib/models.d.ts +42 -6
  99. package/dist/lib/models.js +55 -153
  100. package/dist/lib/perf/db.js +16 -3
  101. package/dist/lib/perf/types.d.ts +12 -1
  102. package/dist/lib/pricing/cost.d.ts +9 -0
  103. package/dist/lib/pricing/cost.js +24 -0
  104. package/dist/lib/pricing/index.d.ts +1 -1
  105. package/dist/lib/pricing/index.js +1 -1
  106. package/dist/lib/project-probe.d.ts +22 -3
  107. package/dist/lib/project-probe.js +105 -17
  108. package/dist/lib/project-status.d.ts +9 -0
  109. package/dist/lib/project-status.js +15 -0
  110. package/dist/lib/redact.js +8 -3
  111. package/dist/lib/refresh.js +22 -11
  112. package/dist/lib/remote-agents-json.d.ts +32 -0
  113. package/dist/lib/remote-agents-json.js +47 -16
  114. package/dist/lib/resource-profiles.js +1 -2
  115. package/dist/lib/rotate.d.ts +22 -0
  116. package/dist/lib/rotate.js +26 -0
  117. package/dist/lib/routine-notify-owner.d.ts +102 -0
  118. package/dist/lib/routine-notify-owner.js +232 -0
  119. package/dist/lib/routines.d.ts +10 -0
  120. package/dist/lib/runner.d.ts +9 -1
  121. package/dist/lib/runner.js +182 -17
  122. package/dist/lib/sandbox.d.ts +0 -2
  123. package/dist/lib/sandbox.js +2 -19
  124. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  125. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  126. package/dist/lib/secrets/agent.d.ts +19 -3
  127. package/dist/lib/secrets/agent.js +61 -22
  128. package/dist/lib/secrets/audit.d.ts +1 -1
  129. package/dist/lib/secrets/audit.js +2 -0
  130. package/dist/lib/secrets/bundles.d.ts +3 -3
  131. package/dist/lib/secrets/bundles.js +15 -5
  132. package/dist/lib/secrets/filestore.d.ts +2 -0
  133. package/dist/lib/secrets/filestore.js +4 -0
  134. package/dist/lib/secrets/lease.d.ts +25 -0
  135. package/dist/lib/secrets/lease.js +44 -0
  136. package/dist/lib/secrets/session-store.d.ts +7 -0
  137. package/dist/lib/secrets/session-store.js +21 -0
  138. package/dist/lib/self-update.d.ts +20 -5
  139. package/dist/lib/self-update.js +93 -16
  140. package/dist/lib/session/active.d.ts +49 -0
  141. package/dist/lib/session/active.js +175 -11
  142. package/dist/lib/session/bash-command.d.ts +16 -0
  143. package/dist/lib/session/bash-command.js +65 -0
  144. package/dist/lib/session/db.d.ts +23 -2
  145. package/dist/lib/session/db.js +86 -20
  146. package/dist/lib/session/discover.d.ts +21 -1
  147. package/dist/lib/session/discover.js +124 -32
  148. package/dist/lib/session/insights.d.ts +19 -0
  149. package/dist/lib/session/insights.js +78 -10
  150. package/dist/lib/session/recovery.d.ts +17 -1
  151. package/dist/lib/session/recovery.js +111 -4
  152. package/dist/lib/session/remote-bundle.js +1 -1
  153. package/dist/lib/session/remote-list.d.ts +10 -9
  154. package/dist/lib/session/remote-list.js +25 -23
  155. package/dist/lib/session/resume-owner.d.ts +55 -0
  156. package/dist/lib/session/resume-owner.js +69 -0
  157. package/dist/lib/session/team-filter.d.ts +65 -0
  158. package/dist/lib/session/team-filter.js +98 -3
  159. package/dist/lib/session/types.d.ts +33 -0
  160. package/dist/lib/smart-launch.d.ts +31 -5
  161. package/dist/lib/smart-launch.js +43 -7
  162. package/dist/lib/ssh-exec.d.ts +27 -0
  163. package/dist/lib/ssh-exec.js +34 -1
  164. package/dist/lib/ssh-tunnel.d.ts +3 -2
  165. package/dist/lib/ssh-tunnel.js +25 -16
  166. package/dist/lib/staleness/detectors/skills.d.ts +2 -0
  167. package/dist/lib/staleness/detectors/skills.js +17 -2
  168. package/dist/lib/staleness/index.d.ts +10 -1
  169. package/dist/lib/staleness/index.js +28 -3
  170. package/dist/lib/startup/command-registry.d.ts +1 -0
  171. package/dist/lib/startup/command-registry.js +2 -0
  172. package/dist/lib/teams/agents.d.ts +1 -0
  173. package/dist/lib/teams/agents.js +38 -11
  174. package/dist/lib/teams/placement-probe.d.ts +1 -1
  175. package/dist/lib/teams/placement-probe.js +24 -24
  176. package/dist/lib/teams/remoteWorktree.d.ts +11 -7
  177. package/dist/lib/teams/remoteWorktree.js +29 -27
  178. package/dist/lib/tmux/session.d.ts +8 -0
  179. package/dist/lib/tmux/session.js +22 -0
  180. package/dist/lib/types.d.ts +2 -0
  181. package/dist/lib/versions.d.ts +1 -0
  182. package/dist/lib/versions.js +40 -22
  183. package/package.json +3 -1
@@ -40,30 +40,30 @@ export function remotePathExpr(p) {
40
40
  * downstream remote command (launch `cd`, worktree create, polling) works from an
41
41
  * absolute path with no tilde-expansion hazard.
42
42
  */
43
- export function resolveRemoteRepoRoot(target, repoPath) {
43
+ export function resolveRemoteRepoRoot(target, repoPath, opts = {}) {
44
44
  assertValidSshTarget(target);
45
45
  const cmd = `git -C ${remotePathExpr(repoPath)} rev-parse --show-toplevel 2>/dev/null`;
46
- const res = sshExec(target, cmd, { timeoutMs: 15000, multiplex: true });
46
+ const res = sshExec(target, cmd, { timeoutMs: 15000, multiplex: true, extraSshArgs: opts.extraSshArgs });
47
47
  const root = res.stdout.trim();
48
48
  return res.code === 0 && root ? root : null;
49
49
  }
50
50
  /** Run one git command in `repoPath` on the host; throw with stderr on failure. */
51
- function remoteGit(target, repoPath, args, timeoutMs = 60000) {
51
+ function remoteGit(target, repoPath, args, timeoutMs = 60000, opts = {}) {
52
52
  const cmd = ['git', '-C', repoPath, ...args].map(shellQuote).join(' ');
53
- const res = sshExec(target, cmd, { timeoutMs, multiplex: true });
53
+ const res = sshExec(target, cmd, { timeoutMs, multiplex: true, extraSshArgs: opts.extraSshArgs });
54
54
  if (res.code !== 0) {
55
55
  throw new Error(`remote git failed on ${target} (${args[0]}): ${(res.stderr || res.stdout).trim() || 'ssh error'}`);
56
56
  }
57
57
  return res.stdout.trim();
58
58
  }
59
59
  /** True when `repoPath` is a git working tree on the host. */
60
- export function isRemoteGitRepo(target, repoPath) {
60
+ export function isRemoteGitRepo(target, repoPath, opts = {}) {
61
61
  assertValidSshTarget(target);
62
62
  // remotePathExpr so a `~`/`$HOME`-relative path (the canonical repos dir) expands
63
63
  // on the host — plain shellQuote would single-quote the tilde into a literal,
64
64
  // making an existing checkout look absent and forcing a doomed re-clone.
65
65
  const cmd = `git -C ${remotePathExpr(repoPath)} rev-parse --git-dir 2>/dev/null`;
66
- const res = sshExec(target, cmd, { timeoutMs: 15000, multiplex: true });
66
+ const res = sshExec(target, cmd, { timeoutMs: 15000, multiplex: true, extraSshArgs: opts.extraSshArgs });
67
67
  return res.code === 0;
68
68
  }
69
69
  /**
@@ -71,12 +71,12 @@ export function isRemoteGitRepo(target, repoPath) {
71
71
  * Falls back to `main` when origin/HEAD isn't set, matching the local recipe's
72
72
  * `remote set-head` step.
73
73
  */
74
- export function remoteDefaultBranch(target, repoPath) {
74
+ export function remoteDefaultBranch(target, repoPath, opts = {}) {
75
75
  assertValidSshTarget(target);
76
76
  try {
77
77
  // Refresh origin/HEAD first so a repo cloned before the default was set resolves.
78
- sshExec(target, ['git', '-C', repoPath, 'remote', 'set-head', 'origin', '--auto'].map(shellQuote).join(' '), { timeoutMs: 20000, multiplex: true });
79
- const ref = remoteGit(target, repoPath, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);
78
+ sshExec(target, ['git', '-C', repoPath, 'remote', 'set-head', 'origin', '--auto'].map(shellQuote).join(' '), { timeoutMs: 20000, multiplex: true, extraSshArgs: opts.extraSshArgs });
79
+ const ref = remoteGit(target, repoPath, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], 60000, opts);
80
80
  return ref.replace(/^origin\//, '') || 'main';
81
81
  }
82
82
  catch {
@@ -91,15 +91,15 @@ export function remoteDefaultBranch(target, repoPath) {
91
91
  * `worktree add -b … origin/<default>` so a stale checkout can't fork the
92
92
  * teammate off old code.
93
93
  */
94
- export function createRemoteWorktree(target, repoPath, worktreeName) {
94
+ export function createRemoteWorktree(target, repoPath, worktreeName, opts = {}) {
95
95
  assertValidSshTarget(target);
96
96
  assertName(worktreeName);
97
- const gitRoot = remoteGit(target, repoPath, ['rev-parse', '--show-toplevel']);
98
- const base = remoteDefaultBranch(target, gitRoot);
97
+ const gitRoot = remoteGit(target, repoPath, ['rev-parse', '--show-toplevel'], 60000, opts);
98
+ const base = remoteDefaultBranch(target, gitRoot, opts);
99
99
  const worktreePath = `${gitRoot}/.agents/worktrees/${worktreeName}`;
100
100
  const branchName = `agents/${worktreeName}`;
101
- remoteGit(target, gitRoot, ['fetch', 'origin'], 120000);
102
- remoteGit(target, gitRoot, ['worktree', 'add', '-b', branchName, worktreePath, `origin/${base}`], 120000);
101
+ remoteGit(target, gitRoot, ['fetch', 'origin'], 120000, opts);
102
+ remoteGit(target, gitRoot, ['worktree', 'add', '-b', branchName, worktreePath, `origin/${base}`], 120000, opts);
103
103
  return worktreePath;
104
104
  }
105
105
  // Team-name → repos-dir slug: same allowlist a worktree name uses, so it lands
@@ -126,24 +126,24 @@ function repoSlug(name) {
126
126
  * expands `$HOME` — single-quoting the tilde would defeat expansion, the same
127
127
  * hazard dispatch.ts avoids.
128
128
  */
129
- export function ensureRemoteRepo(target, repo, slug) {
129
+ export function ensureRemoteRepo(target, repo, slug, opts = {}) {
130
130
  assertValidSshTarget(target);
131
131
  const safeSlug = repoSlug(slug);
132
132
  const canonical = `~/.agents/repos/${safeSlug}`;
133
133
  // 1. Canonical checkout already exists → fetch and reuse.
134
- if (isRemoteGitRepo(target, canonical)) {
134
+ if (isRemoteGitRepo(target, canonical, opts)) {
135
135
  // Best-effort refresh; a fetch failure (offline origin) shouldn't block reuse.
136
- sshExec(target, `git -C ${remotePathExpr(canonical)} fetch origin`, { timeoutMs: 120000, multiplex: true });
137
- const root = resolveRemoteRepoRoot(target, canonical);
136
+ sshExec(target, `git -C ${remotePathExpr(canonical)} fetch origin`, { timeoutMs: 120000, multiplex: true, extraSshArgs: opts.extraSshArgs });
137
+ const root = resolveRemoteRepoRoot(target, canonical, opts);
138
138
  if (!root)
139
139
  throw new Error(`Repo at ${canonical} on ${target} vanished mid-provision.`);
140
140
  return root;
141
141
  }
142
142
  // 2. `repo` is a path that already exists on the host as a git repo → use it.
143
143
  if (repo && !looksLikeUrl(repo)) {
144
- const existing = resolveRemoteRepoRoot(target, repo);
144
+ const existing = resolveRemoteRepoRoot(target, repo, opts);
145
145
  if (existing) {
146
- sshExec(target, `git -C ${remotePathExpr(existing)} fetch origin`, { timeoutMs: 120000, multiplex: true });
146
+ sshExec(target, `git -C ${remotePathExpr(existing)} fetch origin`, { timeoutMs: 120000, multiplex: true, extraSshArgs: opts.extraSshArgs });
147
147
  return existing;
148
148
  }
149
149
  }
@@ -159,12 +159,12 @@ export function ensureRemoteRepo(target, repo, slug) {
159
159
  // blocks *shell* injection and is orthogonal to *git-transport* injection.
160
160
  assertSafeGitTransport(repo);
161
161
  const clone = sshExec(target, `mkdir -p ${remotePathExpr('~/.agents/repos')} && ` +
162
- `git clone -- ${shellQuote(repo)} ${remotePathExpr(canonical)}`, { timeoutMs: 600000, multiplex: true });
162
+ `git clone -- ${shellQuote(repo)} ${remotePathExpr(canonical)}`, { timeoutMs: 600000, multiplex: true, extraSshArgs: opts.extraSshArgs });
163
163
  if (clone.code !== 0) {
164
164
  throw new Error(`git clone ${repo} into ${canonical} on ${target} failed: ` +
165
165
  `${(clone.stderr || clone.stdout).trim() || 'ssh error'}`);
166
166
  }
167
- const root = resolveRemoteRepoRoot(target, canonical);
167
+ const root = resolveRemoteRepoRoot(target, canonical, opts);
168
168
  if (!root)
169
169
  throw new Error(`Cloned ${repo} into ${canonical} on ${target} but it isn't a git repo.`);
170
170
  return root;
@@ -176,10 +176,10 @@ function looksLikeUrl(repo) {
176
176
  repo.startsWith('git@'));
177
177
  }
178
178
  /** True when the host worktree has uncommitted changes (staged or unstaged). */
179
- export function remoteWorktreeDirty(target, worktreePath) {
179
+ export function remoteWorktreeDirty(target, worktreePath, opts = {}) {
180
180
  assertValidSshTarget(target);
181
181
  const cmd = ['git', '-C', worktreePath, 'status', '--porcelain'].map(shellQuote).join(' ');
182
- const res = sshExec(target, cmd, { timeoutMs: 20000, multiplex: true });
182
+ const res = sshExec(target, cmd, { timeoutMs: 20000, multiplex: true, extraSshArgs: opts.extraSshArgs });
183
183
  if (res.code !== 0)
184
184
  return false;
185
185
  return res.stdout.trim().length > 0;
@@ -189,17 +189,18 @@ export function remoteWorktreeDirty(target, worktreePath) {
189
189
  * "not a working tree" error and ignores a missing branch, matching local
190
190
  * `removeWorktree` semantics so cleanup never throws on a half-gone worktree.
191
191
  */
192
- export function removeRemoteWorktree(target, repoPath, worktreeName, deleteBranch = true) {
192
+ export function removeRemoteWorktree(target, repoPath, worktreeName, deleteBranch = true, opts = {}) {
193
193
  assertValidSshTarget(target);
194
194
  assertName(worktreeName);
195
- const gitRoot = remoteGit(target, repoPath, ['rev-parse', '--show-toplevel']);
195
+ const gitRoot = remoteGit(target, repoPath, ['rev-parse', '--show-toplevel'], 60000, opts);
196
196
  const worktreePath = `${gitRoot}/.agents/worktrees/${worktreeName}`;
197
197
  const branchName = `agents/${worktreeName}`;
198
- const rm = sshExec(target, ['git', '-C', gitRoot, 'worktree', 'remove', '--force', worktreePath].map(shellQuote).join(' '), { timeoutMs: 60000, multiplex: true });
198
+ const rm = sshExec(target, ['git', '-C', gitRoot, 'worktree', 'remove', '--force', worktreePath].map(shellQuote).join(' '), { timeoutMs: 60000, multiplex: true, extraSshArgs: opts.extraSshArgs });
199
199
  if (rm.code !== 0 && /is not a working tree/.test(rm.stderr + rm.stdout)) {
200
200
  sshExec(target, ['git', '-C', gitRoot, 'worktree', 'prune'].map(shellQuote).join(' '), {
201
201
  timeoutMs: 30000,
202
202
  multiplex: true,
203
+ extraSshArgs: opts.extraSshArgs,
203
204
  });
204
205
  }
205
206
  if (deleteBranch) {
@@ -207,6 +208,7 @@ export function removeRemoteWorktree(target, repoPath, worktreeName, deleteBranc
207
208
  sshExec(target, ['git', '-C', gitRoot, 'branch', '-D', branchName].map(shellQuote).join(' '), {
208
209
  timeoutMs: 20000,
209
210
  multiplex: true,
211
+ extraSshArgs: opts.extraSshArgs,
210
212
  });
211
213
  }
212
214
  }
@@ -179,6 +179,14 @@ export declare const AGENT_HOOK_SCHEMA = 5;
179
179
  export declare function agentPaneDiedHook(sessionName: string, agentPane: string): string;
180
180
  /** Stamp a session's hook-schema marker to the current version. */
181
181
  export declare function markSessionHookSchema(name: string, socket?: string): Promise<void>;
182
+ /**
183
+ * Decide whether a native resume may attach an existing managed tmux session.
184
+ * Only a positively resolved, living agent pane is reusable. Metadata-less
185
+ * legacy sessions and stale metadata resolve their real first pane through
186
+ * tmux before the decision; a dead or unreadable session is reaped so the
187
+ * caller can create a fresh wrapper for the resumed harness.
188
+ */
189
+ export declare function prepareSessionForResume(name: string, socket?: string): Promise<'attach' | 'create'>;
182
190
  /**
183
191
  * Retrofit the current guarded `pane-died` hook onto every managed `agents run`
184
192
  * session whose hook predates AGENT_HOOK_SCHEMA. Idempotent and NON-DESTRUCTIVE:
@@ -359,6 +359,28 @@ async function lowestPaneId(name, socket) {
359
359
  return undefined;
360
360
  return ids.reduce((lo, id) => (parseInt(id.slice(1), 10) < parseInt(lo.slice(1), 10) ? id : lo));
361
361
  }
362
+ /**
363
+ * Decide whether a native resume may attach an existing managed tmux session.
364
+ * Only a positively resolved, living agent pane is reusable. Metadata-less
365
+ * legacy sessions and stale metadata resolve their real first pane through
366
+ * tmux before the decision; a dead or unreadable session is reaped so the
367
+ * caller can create a fresh wrapper for the resumed harness.
368
+ */
369
+ export async function prepareSessionForResume(name, socket) {
370
+ const sock = socket ?? getDefaultSocketPath();
371
+ if (!(await hasSession(name, sock)))
372
+ return 'create';
373
+ const recordedPane = readSessionMeta(name)?.pane;
374
+ const recordedState = recordedPane ? await paneExitStatus(recordedPane, sock) : undefined;
375
+ const pane = recordedPane && recordedState?.found
376
+ ? recordedPane
377
+ : await lowestPaneId(name, sock);
378
+ const state = pane ? await paneExitStatus(pane, sock) : undefined;
379
+ if (pane && state?.found && !state.dead)
380
+ return 'attach';
381
+ await killSession(name, sock);
382
+ return 'create';
383
+ }
362
384
  /**
363
385
  * Retrofit the current guarded `pane-died` hook onto every managed `agents run`
364
386
  * session whose hook predates AGENT_HOOK_SCHEMA. Idempotent and NON-DESTRUCTIVE:
@@ -1064,6 +1064,8 @@ export interface HostEntry {
1064
1064
  address?: string;
1065
1065
  /** SSH user — inline hosts only. */
1066
1066
  user?: string;
1067
+ /** Explicit private key inherited from a fleet device profile. */
1068
+ identityFile?: string;
1067
1069
  /** Captured at enroll probe. */
1068
1070
  os?: string;
1069
1071
  /** Free-form capability tags for routing (e.g. ['gpu']). */
@@ -525,6 +525,7 @@ export declare function syncResourcesToVersion(agent: AgentId, version: string,
525
525
  projectDir?: string;
526
526
  cwd?: string;
527
527
  force?: boolean;
528
+ available?: AvailableResources;
528
529
  }): SyncResult;
529
530
  /**
530
531
  * Get the effective HOME directory for an agent.
@@ -346,11 +346,21 @@ function skillDirsMatch(src, dest) {
346
346
  return false;
347
347
  }
348
348
  else {
349
- if (!fs.existsSync(destPath))
349
+ // Stat-first (RUSH-2320 #2): size mismatch = definitive miss, no reads.
350
+ // Equal mtimes across different trees are accidental — content-compare
351
+ // whenever sizes match.
352
+ let srcStat;
353
+ let destStat;
354
+ try {
355
+ srcStat = fs.statSync(srcPath);
356
+ destStat = fs.statSync(destPath);
357
+ }
358
+ catch {
350
359
  return false;
351
- const srcContent = fs.readFileSync(srcPath, 'utf-8');
352
- const destContent = fs.readFileSync(destPath, 'utf-8');
353
- if (srcContent !== destContent)
360
+ }
361
+ if (srcStat.size !== destStat.size)
362
+ return false;
363
+ if (fs.readFileSync(srcPath, 'utf-8') !== fs.readFileSync(destPath, 'utf-8'))
354
364
  return false;
355
365
  }
356
366
  }
@@ -2580,7 +2590,28 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2580
2590
  // Extra DotAgent repos registered via `agents repo add`. Looked up last so
2581
2591
  // project/user/system repos win on name collisions.
2582
2592
  const extraRepos = getEnabledExtraRepos();
2583
- const available = getAvailableResources(cwd);
2593
+ // Project-layer fan-out always runs — even on the early guard hit — so the
2594
+ // `projectSkipped` contract is preserved for callers (RUSH-2320 #4).
2595
+ if (projectAgentsDir) {
2596
+ result.projectSkipped = syncProjectResourcesToAgent(agent, version, projectAgentsDir).skipped;
2597
+ }
2598
+ // Fast guard BEFORE getAvailableResources / pattern expansion /
2599
+ // ensureVersionResourcePatterns. Guard needs only loadManifest + isStale
2600
+ // (~6 ms); the work that used to sit ahead of it was ~12.5 ms of the 21.5 ms
2601
+ // guard-hit path and is discarded when nothing changed (RUSH-2320 #4).
2602
+ // Behavior note: a valid manifest implies a prior full sync already wrote
2603
+ // version resource patterns, so skipping ensureVersionResourcePatterns on
2604
+ // the hit path is safe in practice but is not byte-identical to the old
2605
+ // order (that call always wrote missing pattern defaults).
2606
+ if (!userPassedSelection && !options.force) {
2607
+ const manifest = loadManifest(agent, version);
2608
+ if (manifest && !isStale(manifest, agent, version, cwd)) {
2609
+ return { ...result };
2610
+ }
2611
+ }
2612
+ // Prefer a caller-supplied inventory (refresh already built one) so multi-
2613
+ // version fan-out does not re-scan resource trees per version (RUSH-2320 #5).
2614
+ const available = options.available ?? getAvailableResources(cwd);
2584
2615
  // Write default resource selection patterns for this version (idempotent —
2585
2616
  // only sets fields that aren't already present, preserving user edits).
2586
2617
  {
@@ -2640,21 +2671,6 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2640
2671
  }
2641
2672
  }
2642
2673
  }
2643
- if (projectAgentsDir) {
2644
- result.projectSkipped = syncProjectResourcesToAgent(agent, version, projectAgentsDir).skipped;
2645
- }
2646
- // Fast guard: skip the entire sync when the caller requested a full sync and
2647
- // nothing has changed since the last full sync. Pattern-derived selections
2648
- // still count as full syncs because they are the persisted intended scope,
2649
- // not a one-off caller override.
2650
- if (!userPassedSelection && !options.force) {
2651
- const manifest = loadManifest(agent, version);
2652
- if (manifest && !isStale(manifest, agent, version, cwd)) {
2653
- // Nothing synced, but the project sync above already ran — carry its
2654
- // skipped files out so the caller can still report them.
2655
- return { ...result };
2656
- }
2657
- }
2658
2674
  // Helper: remove a path (symlink or real) if it exists
2659
2675
  const removePath = (p) => {
2660
2676
  try {
@@ -3023,9 +3039,11 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
3023
3039
  // launch can skip the slow path. Pattern-derived selections still count as
3024
3040
  // "full" — the agents.yaml patterns describe the intended scope, not a
3025
3041
  // one-off override, so the resulting state matches what the manifest
3026
- // records as the synced set.
3042
+ // records as the synced set. Carry forward still-fresh fingerprints from
3043
+ // the previous manifest so we do not re-hash an unchanged tree (RUSH-2320 #3).
3027
3044
  if (!userPassedSelection) {
3028
- const manifest = buildSyncManifest(agent, version, cwd);
3045
+ const previous = loadManifest(agent, version);
3046
+ const manifest = buildSyncManifest(agent, version, cwd, previous);
3029
3047
  manifest.writtenCommands = writtenCommands;
3030
3048
  saveManifest(agent, version, manifest);
3031
3049
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.26",
3
+ "version": "1.22.28",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -54,6 +54,8 @@
54
54
  "changelog": "bun scripts/gen-changelog.ts",
55
55
  "start": "node dist/index.js",
56
56
  "test": "node ./node_modules/vitest/vitest.mjs run",
57
+ "bench": "node ./node_modules/vitest/vitest.mjs bench --run",
58
+ "typecheck:bench": "tsc --noEmit --ignoreConfig --skipLibCheck --module esnext --moduleResolution bundler --target es2022 --strict --esModuleInterop --lib es2022 --types node src/lib/*.bench.ts src/lib/**/*.bench.ts",
57
59
  "test:remote": "scripts/sandbox.sh 'bun install && bun run build && bun run test'",
58
60
  "verify-docs": "scripts/verify-docs.sh",
59
61
  "test:watch": "node ./node_modules/vitest/vitest.mjs"