@phnx-labs/agents-cli 1.20.74 → 1.20.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 (199) hide show
  1. package/CHANGELOG.md +893 -0
  2. package/README.md +36 -10
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.js +21 -3
  5. package/dist/commands/attach.d.ts +10 -0
  6. package/dist/commands/attach.js +41 -0
  7. package/dist/commands/computer.js +3 -3
  8. package/dist/commands/detach-core.d.ts +46 -0
  9. package/dist/commands/detach-core.js +61 -0
  10. package/dist/commands/detach.d.ts +5 -0
  11. package/dist/commands/detach.js +171 -0
  12. package/dist/commands/doctor.js +18 -0
  13. package/dist/commands/exec.js +31 -3
  14. package/dist/commands/feed.d.ts +7 -1
  15. package/dist/commands/feed.js +47 -4
  16. package/dist/commands/go.d.ts +9 -2
  17. package/dist/commands/go.js +16 -6
  18. package/dist/commands/harness.d.ts +14 -0
  19. package/dist/commands/harness.js +145 -0
  20. package/dist/commands/import.js +38 -8
  21. package/dist/commands/inspect.js +5 -2
  22. package/dist/commands/profiles.d.ts +20 -0
  23. package/dist/commands/profiles.js +68 -21
  24. package/dist/commands/repo.js +14 -0
  25. package/dist/commands/routines.js +15 -2
  26. package/dist/commands/secrets.js +44 -21
  27. package/dist/commands/sessions-browser.js +1 -1
  28. package/dist/commands/sessions-export.js +19 -5
  29. package/dist/commands/sessions-migrate.d.ts +29 -0
  30. package/dist/commands/sessions-migrate.js +596 -0
  31. package/dist/commands/sessions-picker.js +16 -1
  32. package/dist/commands/sessions.d.ts +36 -13
  33. package/dist/commands/sessions.js +210 -40
  34. package/dist/commands/setup-computer.js +2 -2
  35. package/dist/commands/ssh.js +71 -35
  36. package/dist/commands/status.js +10 -2
  37. package/dist/commands/versions.js +15 -6
  38. package/dist/commands/view.d.ts +5 -0
  39. package/dist/commands/view.js +24 -4
  40. package/dist/commands/watchdog.d.ts +7 -2
  41. package/dist/commands/watchdog.js +73 -57
  42. package/dist/index.js +58 -7
  43. package/dist/lib/activity.d.ts +14 -4
  44. package/dist/lib/activity.js +17 -5
  45. package/dist/lib/actor.d.ts +50 -0
  46. package/dist/lib/actor.js +166 -0
  47. package/dist/lib/agent-spec/provider.js +2 -1
  48. package/dist/lib/agent-spec/resolve.js +19 -5
  49. package/dist/lib/agent-spec/types.d.ts +9 -1
  50. package/dist/lib/agents.d.ts +21 -0
  51. package/dist/lib/agents.js +70 -6
  52. package/dist/lib/cloud/codex.d.ts +2 -0
  53. package/dist/lib/cloud/codex.js +14 -3
  54. package/dist/lib/cloud/session-index.d.ts +32 -0
  55. package/dist/lib/cloud/session-index.js +58 -0
  56. package/dist/lib/cloud/store.d.ts +7 -0
  57. package/dist/lib/cloud/store.js +25 -0
  58. package/dist/lib/config-transfer.js +4 -0
  59. package/dist/lib/daemon.js +18 -2
  60. package/dist/lib/devices/connect.d.ts +15 -1
  61. package/dist/lib/devices/connect.js +15 -1
  62. package/dist/lib/devices/fleet.d.ts +21 -0
  63. package/dist/lib/devices/fleet.js +26 -0
  64. package/dist/lib/devices/health-report.d.ts +38 -0
  65. package/dist/lib/devices/health-report.js +214 -0
  66. package/dist/lib/devices/health.js +4 -1
  67. package/dist/lib/devices/reachability.d.ts +31 -0
  68. package/dist/lib/devices/reachability.js +40 -0
  69. package/dist/lib/devices/registry.d.ts +33 -0
  70. package/dist/lib/devices/registry.js +37 -0
  71. package/dist/lib/devices/resolve-target.d.ts +15 -27
  72. package/dist/lib/devices/resolve-target.js +63 -102
  73. package/dist/lib/devices/sync.d.ts +18 -0
  74. package/dist/lib/devices/sync.js +23 -1
  75. package/dist/lib/devices/tailscale.d.ts +3 -0
  76. package/dist/lib/devices/tailscale.js +1 -0
  77. package/dist/lib/events.d.ts +1 -1
  78. package/dist/lib/exec.d.ts +66 -0
  79. package/dist/lib/exec.js +195 -6
  80. package/dist/lib/feed-post.d.ts +63 -0
  81. package/dist/lib/feed-post.js +204 -0
  82. package/dist/lib/fleet/apply.d.ts +32 -2
  83. package/dist/lib/fleet/apply.js +97 -10
  84. package/dist/lib/fleet/types.d.ts +11 -0
  85. package/dist/lib/fs-walk.d.ts +13 -0
  86. package/dist/lib/fs-walk.js +16 -7
  87. package/dist/lib/hooks.d.ts +1 -0
  88. package/dist/lib/hooks.js +156 -0
  89. package/dist/lib/hosts/dispatch.d.ts +7 -0
  90. package/dist/lib/hosts/dispatch.js +2 -0
  91. package/dist/lib/hosts/passthrough.js +2 -0
  92. package/dist/lib/hosts/reconnect.d.ts +78 -0
  93. package/dist/lib/hosts/reconnect.js +127 -0
  94. package/dist/lib/hosts/registry.d.ts +75 -14
  95. package/dist/lib/hosts/registry.js +205 -30
  96. package/dist/lib/hosts/remote-cmd.js +1 -0
  97. package/dist/lib/hosts/run-target.d.ts +13 -5
  98. package/dist/lib/hosts/run-target.js +26 -10
  99. package/dist/lib/hosts/session-index.d.ts +18 -1
  100. package/dist/lib/hosts/session-index.js +37 -2
  101. package/dist/lib/hosts/session-marker.d.ts +33 -0
  102. package/dist/lib/hosts/session-marker.js +51 -0
  103. package/dist/lib/hosts/tasks.d.ts +8 -4
  104. package/dist/lib/import.d.ts +2 -0
  105. package/dist/lib/import.js +35 -2
  106. package/dist/lib/menubar/install-menubar.d.ts +20 -0
  107. package/dist/lib/menubar/install-menubar.js +45 -1
  108. package/dist/lib/migrate.d.ts +16 -0
  109. package/dist/lib/migrate.js +36 -0
  110. package/dist/lib/models.d.ts +27 -0
  111. package/dist/lib/models.js +54 -1
  112. package/dist/lib/overdue.d.ts +7 -2
  113. package/dist/lib/overdue.js +18 -11
  114. package/dist/lib/picker.js +4 -1
  115. package/dist/lib/platform/process.d.ts +17 -0
  116. package/dist/lib/platform/process.js +70 -0
  117. package/dist/lib/profiles-presets.js +9 -7
  118. package/dist/lib/profiles.d.ts +31 -0
  119. package/dist/lib/profiles.js +70 -0
  120. package/dist/lib/pty-server.d.ts +2 -10
  121. package/dist/lib/pty-server.js +4 -38
  122. package/dist/lib/registry.d.ts +1 -1
  123. package/dist/lib/registry.js +48 -8
  124. package/dist/lib/rotate.d.ts +18 -0
  125. package/dist/lib/rotate.js +28 -0
  126. package/dist/lib/routines.d.ts +26 -0
  127. package/dist/lib/routines.js +19 -2
  128. package/dist/lib/runner.js +104 -7
  129. package/dist/lib/secrets/agent.d.ts +66 -1
  130. package/dist/lib/secrets/agent.js +154 -90
  131. package/dist/lib/secrets/bundles.d.ts +34 -0
  132. package/dist/lib/secrets/bundles.js +179 -19
  133. package/dist/lib/secrets/index.d.ts +15 -1
  134. package/dist/lib/secrets/index.js +21 -3
  135. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  136. package/dist/lib/secrets/rc-hygiene.js +154 -0
  137. package/dist/lib/secrets/remote.d.ts +7 -4
  138. package/dist/lib/secrets/remote.js +7 -4
  139. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  140. package/dist/lib/secrets/sync-commands.js +21 -0
  141. package/dist/lib/self-update.d.ts +23 -2
  142. package/dist/lib/self-update.js +86 -5
  143. package/dist/lib/session/active.d.ts +114 -7
  144. package/dist/lib/session/active.js +323 -52
  145. package/dist/lib/session/cloud.js +2 -1
  146. package/dist/lib/session/db.d.ts +48 -0
  147. package/dist/lib/session/db.js +238 -14
  148. package/dist/lib/session/detached.d.ts +30 -0
  149. package/dist/lib/session/detached.js +92 -0
  150. package/dist/lib/session/discover.d.ts +465 -2
  151. package/dist/lib/session/discover.js +1648 -468
  152. package/dist/lib/session/fork.js +2 -1
  153. package/dist/lib/session/hook-sessions.d.ts +43 -0
  154. package/dist/lib/session/hook-sessions.js +135 -0
  155. package/dist/lib/session/linear.d.ts +6 -0
  156. package/dist/lib/session/linear.js +55 -0
  157. package/dist/lib/session/migrate-targets.d.ts +65 -0
  158. package/dist/lib/session/migrate-targets.js +94 -0
  159. package/dist/lib/session/migrations.d.ts +37 -0
  160. package/dist/lib/session/migrations.js +60 -0
  161. package/dist/lib/session/parse.d.ts +18 -0
  162. package/dist/lib/session/parse.js +130 -30
  163. package/dist/lib/session/pid-registry.d.ts +34 -2
  164. package/dist/lib/session/pid-registry.js +49 -2
  165. package/dist/lib/session/render.d.ts +7 -2
  166. package/dist/lib/session/render.js +22 -10
  167. package/dist/lib/session/short-id.d.ts +17 -0
  168. package/dist/lib/session/short-id.js +20 -0
  169. package/dist/lib/session/state.js +6 -2
  170. package/dist/lib/shims.d.ts +1 -1
  171. package/dist/lib/shims.js +20 -6
  172. package/dist/lib/sqlite.d.ts +3 -2
  173. package/dist/lib/sqlite.js +27 -4
  174. package/dist/lib/staleness/detectors/commands.js +1 -1
  175. package/dist/lib/staleness/detectors/workflows.js +13 -2
  176. package/dist/lib/staleness/writers/commands.js +7 -7
  177. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  178. package/dist/lib/startup/command-registry.d.ts +1 -0
  179. package/dist/lib/startup/command-registry.js +3 -0
  180. package/dist/lib/state.d.ts +1 -1
  181. package/dist/lib/state.js +1 -32
  182. package/dist/lib/teams/agents.d.ts +19 -10
  183. package/dist/lib/teams/agents.js +40 -39
  184. package/dist/lib/types.d.ts +37 -2
  185. package/dist/lib/usage.d.ts +33 -1
  186. package/dist/lib/usage.js +172 -12
  187. package/dist/lib/versions.d.ts +9 -2
  188. package/dist/lib/versions.js +37 -8
  189. package/dist/lib/watchdog/log.d.ts +43 -0
  190. package/dist/lib/watchdog/log.js +69 -0
  191. package/dist/lib/watchdog/routine.d.ts +44 -0
  192. package/dist/lib/watchdog/routine.js +69 -0
  193. package/dist/lib/watchdog/runner.d.ts +51 -7
  194. package/dist/lib/watchdog/runner.js +239 -64
  195. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  196. package/dist/lib/watchdog/watchdog.js +31 -16
  197. package/dist/lib/workflows.d.ts +16 -0
  198. package/dist/lib/workflows.js +110 -1
  199. package/package.json +1 -1
@@ -12,8 +12,12 @@
12
12
  * local run. The `[host/<name>]` label mirrors the cloud path's
13
13
  * `[cloud/<status>]` convention.
14
14
  */
15
+ import * as fs from 'fs';
15
16
  import { upsertSession } from '../session/db.js';
16
17
  import { SESSION_AGENTS } from '../session/types.js';
18
+ import { localLogPath, updateTask } from './tasks.js';
19
+ import { parseSessionIdMarker } from './session-marker.js';
20
+ import { deriveShortId } from '../session/short-id.js';
17
21
  /**
18
22
  * Build the SessionMeta for a host-dispatched run. Returns null when the run has
19
23
  * no captured session id (nothing stable to key/resume on) or its agent isn't a
@@ -27,7 +31,7 @@ export function hostSessionMeta(task, ctx) {
27
31
  return null;
28
32
  return {
29
33
  id,
30
- shortId: id.slice(0, 8),
34
+ shortId: deriveShortId(id),
31
35
  agent: task.agent,
32
36
  timestamp: task.createdAt,
33
37
  cwd: ctx.cwd,
@@ -57,6 +61,37 @@ export function registerHostSession(task, ctx) {
57
61
  /* index write is best-effort; the run is already live on the host */
58
62
  }
59
63
  }
64
+ /**
65
+ * Relate a remote-created session id back to a followed host dispatch.
66
+ *
67
+ * The remote run (dispatched with `--emit-session-id`) prints its resolved id as
68
+ * a stdout sentinel (see session-marker.ts) that rides the followed log into the
69
+ * task's local mirror. Read that mirror, parse the id, and stamp it on the task
70
+ * so `findTaskBySessionId` and the session-index registration work for agents
71
+ * that never take a forced `--session-id` — closing the gap where every
72
+ * non-Claude host run was orphaned.
73
+ *
74
+ * Returns the updated task when an id was captured (and differs from what's on
75
+ * the record), else null. A task that already carries an id (Claude's forced id,
76
+ * a resume) is left untouched — the marker only fills a genuinely empty slot, so
77
+ * it can never overwrite an authoritative id with a stale echo. Best-effort: a
78
+ * missing/unreadable mirror or absent marker yields null, never an exception.
79
+ */
80
+ export function captureRemoteSessionId(task) {
81
+ if (task.sessionId)
82
+ return null;
83
+ let text;
84
+ try {
85
+ text = fs.readFileSync(localLogPath(task.id), 'utf8');
86
+ }
87
+ catch {
88
+ return null; // no local mirror (unfollowed run, or read raced the follow)
89
+ }
90
+ const captured = parseSessionIdMarker(text);
91
+ if (!captured)
92
+ return null;
93
+ return updateTask(task.id, { sessionId: captured });
94
+ }
60
95
  /**
61
96
  * Register an interactive host run (no prompt, TTY forwarded over SSH) in the
62
97
  * local session index. Unlike detached host runs, there is no remote log/exit
@@ -69,7 +104,7 @@ export function registerInteractiveHostSession(ctx) {
69
104
  try {
70
105
  upsertSession({
71
106
  id: ctx.sessionId,
72
- shortId: ctx.sessionId.slice(0, 8),
107
+ shortId: deriveShortId(ctx.sessionId),
73
108
  agent: ctx.agent,
74
109
  timestamp: ctx.createdAt ?? new Date().toISOString(),
75
110
  cwd: ctx.cwd,
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Relate a remote-created session id back to the launching agent.
3
+ *
4
+ * A host dispatch (`agents run --host`) launches `agents run` on the remote box.
5
+ * Only Claude accepts a forced `--session-id`, so for every OTHER agent the
6
+ * remote run coins its OWN session id (via its SessionStart hook) and the
7
+ * launcher never learns it — the run is orphaned in `agents sessions`.
8
+ *
9
+ * This module closes that gap without an extra SSH round-trip: the dispatch
10
+ * forwards `--emit-session-id`, which makes the remote `agents run` print its
11
+ * resolved session id as a stable one-line sentinel to stdout. That line rides
12
+ * the same combined log the follow already mirrors locally, so the launcher
13
+ * parses the id straight out of the streamed output and stamps it on the task.
14
+ *
15
+ * The marker is a fixed ASCII frame with the id on its own line. A real session
16
+ * id (`[A-Za-z0-9._-]`) never contains the marker's own bytes, so the parser can
17
+ * scan for the LAST occurrence and stay robust to an agent echoing the token in
18
+ * its own output.
19
+ */
20
+ /**
21
+ * The single line the remote run prints to stdout so the launcher can capture
22
+ * the session id the remote coined. Newline-framed on both sides so it lands on
23
+ * its own line regardless of what the agent last wrote.
24
+ */
25
+ export declare function sessionIdMarkerLine(sessionId: string): string;
26
+ /**
27
+ * Extract the session id from a chunk of followed remote output, or null when no
28
+ * marker is present. Scans for the LAST marker (so an id echoed into the log
29
+ * earlier can never mask the real trailing sentinel) and validates the captured
30
+ * token against the session-id charset — a malformed frame yields null rather
31
+ * than a bogus id that could never match a real session.
32
+ */
33
+ export declare function parseSessionIdMarker(text: string): string | null;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Relate a remote-created session id back to the launching agent.
3
+ *
4
+ * A host dispatch (`agents run --host`) launches `agents run` on the remote box.
5
+ * Only Claude accepts a forced `--session-id`, so for every OTHER agent the
6
+ * remote run coins its OWN session id (via its SessionStart hook) and the
7
+ * launcher never learns it — the run is orphaned in `agents sessions`.
8
+ *
9
+ * This module closes that gap without an extra SSH round-trip: the dispatch
10
+ * forwards `--emit-session-id`, which makes the remote `agents run` print its
11
+ * resolved session id as a stable one-line sentinel to stdout. That line rides
12
+ * the same combined log the follow already mirrors locally, so the launcher
13
+ * parses the id straight out of the streamed output and stamps it on the task.
14
+ *
15
+ * The marker is a fixed ASCII frame with the id on its own line. A real session
16
+ * id (`[A-Za-z0-9._-]`) never contains the marker's own bytes, so the parser can
17
+ * scan for the LAST occurrence and stay robust to an agent echoing the token in
18
+ * its own output.
19
+ */
20
+ const MARKER_PREFIX = '@@AGENTS_SESSION_ID ';
21
+ const MARKER_SUFFIX = '@@';
22
+ /** Only characters a real agent session id can hold — no marker bytes, no spaces. */
23
+ const SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/;
24
+ /**
25
+ * The single line the remote run prints to stdout so the launcher can capture
26
+ * the session id the remote coined. Newline-framed on both sides so it lands on
27
+ * its own line regardless of what the agent last wrote.
28
+ */
29
+ export function sessionIdMarkerLine(sessionId) {
30
+ return `\n${MARKER_PREFIX}${sessionId}${MARKER_SUFFIX}\n`;
31
+ }
32
+ /**
33
+ * Extract the session id from a chunk of followed remote output, or null when no
34
+ * marker is present. Scans for the LAST marker (so an id echoed into the log
35
+ * earlier can never mask the real trailing sentinel) and validates the captured
36
+ * token against the session-id charset — a malformed frame yields null rather
37
+ * than a bogus id that could never match a real session.
38
+ */
39
+ export function parseSessionIdMarker(text) {
40
+ const last = text.lastIndexOf(MARKER_PREFIX);
41
+ if (last === -1)
42
+ return null;
43
+ const start = last + MARKER_PREFIX.length;
44
+ const end = text.indexOf(MARKER_SUFFIX, start);
45
+ if (end === -1)
46
+ return null;
47
+ const id = text.slice(start, end).trim();
48
+ if (!SESSION_ID_RE.test(id))
49
+ return null;
50
+ return id;
51
+ }
@@ -24,10 +24,14 @@ export interface HostTask {
24
24
  */
25
25
  name?: string;
26
26
  /**
27
- * The agent session id the remote run was launched with (Claude only — the
28
- * only agent that accepts `--session-id` to force a NEW session's id). Lets
29
- * `agents sessions`/resume-by-id map a discovered session back to the host it
30
- * lives on. Absent for agents that don't take an explicit session id.
27
+ * The remote run's agent session id, so `agents sessions`/resume-by-id can map
28
+ * a discovered session back to the host it lives on. Two sources: for Claude
29
+ * (the only agent that accepts `--session-id`) it's the id we FORCED at
30
+ * dispatch; for every other agent it's the id the remote COINED, captured from
31
+ * the run's stdout sentinel (`--emit-session-id`, see session-marker.ts) once
32
+ * the follow returns and stamped on the record via `captureRemoteSessionId`.
33
+ * Absent only until that capture lands — e.g. an unfollowed (`--no-follow`)
34
+ * non-Claude run whose id the reconcile path fills in later.
31
35
  */
32
36
  sessionId?: string;
33
37
  /** Remote paths (under the host's ~/.agents/.cache/hosts/). */
@@ -127,10 +127,12 @@ export declare function resolvePackageDirFromBinary(binaryPath: string): string
127
127
  */
128
128
  export declare function seedIsolatedConfigFromLocal(agentId: AgentId, version: string, opts?: {
129
129
  withAuth?: boolean;
130
+ all?: boolean;
130
131
  }): {
131
132
  seeded: boolean;
132
133
  from: string;
133
134
  to: string;
134
135
  skippedAuth: string[];
136
+ skippedRuntime: string[];
135
137
  error?: string;
136
138
  };
@@ -263,13 +263,35 @@ export function seedIsolatedConfigFromLocal(agentId, version, opts = {}) {
263
263
  const versionHome = path.join(getVersionsDir(), agentId, version, 'home');
264
264
  // Mirror importAgentConfig's derivation so nested config dirs (e.g. Antigravity's
265
265
  // ~/.gemini/antigravity-cli) land where the shim expects them.
266
- const dest = path.join(versionHome, path.relative(os.homedir(), configDir));
267
- const result = { seeded: false, from: configDir, to: dest, skippedAuth: [] };
266
+ let dest = path.join(versionHome, path.relative(os.homedir(), configDir));
267
+ // Codex uses a SUN_LEN-safe CODEX_HOME: `home/.codex` is a SYMLINK to
268
+ // `~/.agents/.codex-homes/<version>/.codex`, because the real path is too long for a
269
+ // unix socket. cpSync cannot overwrite a symlink with a directory (it fails with
270
+ // "Cannot overwrite non-directory"), and writing beside it would put settings
271
+ // somewhere the agent never reads. Follow the link and seed the actual home.
272
+ try {
273
+ if (fs.lstatSync(dest).isSymbolicLink())
274
+ dest = fs.realpathSync(dest);
275
+ }
276
+ catch {
277
+ /* not created yet — the plain path is correct */
278
+ }
279
+ const result = { seeded: false, from: configDir, to: dest, skippedAuth: [], skippedRuntime: [] };
268
280
  if (!fs.existsSync(configDir))
269
281
  return result;
270
282
  // Known credential paths, relative to the config dir. `authFiles` covers the agents
271
283
  // that declare them for cross-version carry; the rest are verified filenames for
272
284
  // agents that do not declare any (codex `auth.json`, claude `.credentials.json`).
285
+ // Runtime state, not settings. Seeding a sandbox with the user's session history,
286
+ // logs and caches duplicated 757MB on a real machine — 349MB of `sessions` alone —
287
+ // for a copy that wants config. These are regenerated by the agent as it runs, and
288
+ // an isolated copy keeps its own; carrying them over also drags conversation history
289
+ // into a sandbox the user may have created precisely to keep separate.
290
+ const RUNTIME_PREFIXES = ['sessions', 'log', 'logs', 'cache', '.tmp', 'tmp', 'generated_images'];
291
+ const RUNTIME_FILES = ['history.jsonl', 'session_index.jsonl'];
292
+ const isRuntime = (rel) => !opts.all && (RUNTIME_FILES.includes(rel) ||
293
+ /\.sqlite(-shm|-wal)?$/.test(rel) ||
294
+ RUNTIME_PREFIXES.some((d) => rel === d || rel.startsWith(d + path.sep)));
273
295
  const authRel = new Set([
274
296
  ...(agent.authFiles ?? []),
275
297
  'auth.json',
@@ -282,12 +304,23 @@ export function seedIsolatedConfigFromLocal(agentId, version, opts = {}) {
282
304
  const inside = agentsDir + path.sep;
283
305
  fs.cpSync(configDir, dest, {
284
306
  recursive: true,
307
+ // `force: true` is Node's default, but Bun drops it when a `filter` is supplied —
308
+ // existing files are then silently left alone. `dist/bin/agents` is bun-compiled,
309
+ // so this is a production path, not just a test artifact. State it explicitly.
310
+ force: true,
285
311
  filter: (src) => {
286
312
  const rel = path.relative(configDir, src);
287
313
  if (!opts.withAuth && rel && (authRel.has(rel) || rel.startsWith('credentials' + path.sep))) {
288
314
  result.skippedAuth.push(rel);
289
315
  return false;
290
316
  }
317
+ if (rel && isRuntime(rel)) {
318
+ // Record only the top-level name so the report stays one line per item.
319
+ const top = rel.split(path.sep)[0];
320
+ if (!result.skippedRuntime.includes(top))
321
+ result.skippedRuntime.push(top);
322
+ return false;
323
+ }
291
324
  // Same rule as the export path: a link into ~/.agents would dangle for a copy
292
325
  // that is supposed to stand on its own.
293
326
  try {
@@ -18,6 +18,26 @@
18
18
  export declare function menubarDisabledByUser(): boolean;
19
19
  /** True if the launchd plist for the menu-bar service is installed. */
20
20
  export declare function menubarServiceInstalled(): boolean;
21
+ /** True when the bundle carries a signature the kernel will accept at launch. */
22
+ export declare function codesignVerifies(appPath: string): boolean;
23
+ /**
24
+ * Guarantee the installed bundle has a valid signature on THIS machine.
25
+ *
26
+ * npm's pack/extract strips the ad-hoc/linker signature the release baked into
27
+ * the helper, leaving `code object is not signed at all`. On macOS 26+ the
28
+ * kernel's code-signing monitor SIGKILLs an unsigned/invalid binary at launch
29
+ * (`SIGKILL (Code Signature Invalid)`), so under the launchd `KeepAlive` service
30
+ * an ad-hoc release helper crash-loops forever and its unstable identity makes
31
+ * the Accessibility grant (needed for the clip→paste keystroke in Clip.swift)
32
+ * re-prompt every time. A fresh ad-hoc re-sign gives the on-disk bytes a
33
+ * matching cdhash, which the kernel accepts.
34
+ *
35
+ * A Developer-ID-signed helper survives npm untouched — its embedded signature
36
+ * still verifies — so we leave it alone and only re-sign when verification
37
+ * fails. Returns whether the bundle ends up validly signed. No-op cost on the
38
+ * common (already-valid) path is a single `codesign --verify`.
39
+ */
40
+ export declare function ensureValidSignature(appPath: string): boolean;
21
41
  /**
22
42
  * Copy the bundled `.app` to the stable user path (idempotent unless forced).
23
43
  * Returns the installed executable path, or null if no source bundle ships
@@ -135,6 +135,37 @@ function copyAppBundle(src, dest) {
135
135
  throw new Error(`Failed to copy ${src} -> ${dest}: ${msg || 'unknown error'}`);
136
136
  }
137
137
  }
138
+ /** True when the bundle carries a signature the kernel will accept at launch. */
139
+ export function codesignVerifies(appPath) {
140
+ const r = spawnSync('codesign', ['--verify', '--strict', appPath], { stdio: ['ignore', 'ignore', 'ignore'] });
141
+ return r.status === 0;
142
+ }
143
+ /**
144
+ * Guarantee the installed bundle has a valid signature on THIS machine.
145
+ *
146
+ * npm's pack/extract strips the ad-hoc/linker signature the release baked into
147
+ * the helper, leaving `code object is not signed at all`. On macOS 26+ the
148
+ * kernel's code-signing monitor SIGKILLs an unsigned/invalid binary at launch
149
+ * (`SIGKILL (Code Signature Invalid)`), so under the launchd `KeepAlive` service
150
+ * an ad-hoc release helper crash-loops forever and its unstable identity makes
151
+ * the Accessibility grant (needed for the clip→paste keystroke in Clip.swift)
152
+ * re-prompt every time. A fresh ad-hoc re-sign gives the on-disk bytes a
153
+ * matching cdhash, which the kernel accepts.
154
+ *
155
+ * A Developer-ID-signed helper survives npm untouched — its embedded signature
156
+ * still verifies — so we leave it alone and only re-sign when verification
157
+ * fails. Returns whether the bundle ends up validly signed. No-op cost on the
158
+ * common (already-valid) path is a single `codesign --verify`.
159
+ */
160
+ export function ensureValidSignature(appPath) {
161
+ if (codesignVerifies(appPath))
162
+ return true;
163
+ // Drop any quarantine/xattrs the tarball round-trip added (they can break
164
+ // codesign), then re-sign ad-hoc under the helper's stable bundle identifier.
165
+ spawnSync('xattr', ['-cr', appPath], { stdio: ['ignore', 'ignore', 'ignore'] });
166
+ spawnSync('codesign', ['--force', '--sign', '-', '--identifier', SERVICE_LABEL, appPath], { stdio: ['ignore', 'ignore', 'ignore'] });
167
+ return codesignVerifies(appPath);
168
+ }
138
169
  /**
139
170
  * Copy the bundled `.app` to the stable user path (idempotent unless forced).
140
171
  * Returns the installed executable path, or null if no source bundle ships
@@ -147,9 +178,14 @@ export function ensureMenubarAppInstalled(opts = {}) {
147
178
  if (!src)
148
179
  return null;
149
180
  const dest = installedAppPath();
150
- if (!opts.forceReinstall && fs.existsSync(dest))
181
+ if (!opts.forceReinstall && fs.existsSync(dest)) {
182
+ // Self-heal an already-installed bundle whose signature npm stripped on a
183
+ // prior upgrade (macOS 26+ SIGKILLs it otherwise) without a forced recopy.
184
+ ensureValidSignature(dest);
151
185
  return installedExecutablePath();
186
+ }
152
187
  copyAppBundle(src, dest);
188
+ ensureValidSignature(dest);
153
189
  return installedExecutablePath();
154
190
  }
155
191
  function xmlEscape(s) {
@@ -241,6 +277,14 @@ export function enableMenubarService(opts = { clearOptOut: true }) {
241
277
  const exec = ensureMenubarAppInstalled({ forceReinstall: true });
242
278
  if (!exec)
243
279
  return false;
280
+ // Never bootstrap a helper the kernel will kill on launch: an invalid
281
+ // signature under launchd KeepAlive is an infinite crash loop. If the bundle
282
+ // can't be made valid (re-sign already attempted in ensureMenubarAppInstalled),
283
+ // skip the service rather than spin the loop.
284
+ if (!codesignVerifies(installedAppPath())) {
285
+ process.stderr.write('agents: menu-bar helper has no valid code signature; skipping launch to avoid a crash loop.\n');
286
+ return false;
287
+ }
244
288
  if (opts.clearOptOut) {
245
289
  try {
246
290
  fs.rmSync(disabledSentinelPath(), { force: true });
@@ -117,5 +117,21 @@ export declare function migrateExtrasExtrasToAgentsExtras(historyDir?: string):
117
117
  * Params default to the real routines dir; injectable for tests.
118
118
  */
119
119
  export declare function migrateRoutineDeviceToDevices(routinesDir?: string): void;
120
+ /**
121
+ * Fold the legacy watchdog enable sentinel into the watchdog routine.
122
+ *
123
+ * The always-on watchdog used to be gated by a presence sentinel at
124
+ * `<runtime-state>/watchdog/enabled`; it is now a daemon routine. A user who had
125
+ * run `agents watchdog enable` under the old build has that file on disk — without
126
+ * this, upgrading would silently drop them back to OFF (the always-on nudge just
127
+ * stops), the worst failure mode for a "survives reboots" feature. If the sentinel
128
+ * exists, ensure the `watchdog` routine exists AND is enabled, then delete the
129
+ * sentinel so this runs exactly once. If ensuring the routine fails, the sentinel
130
+ * is left in place so a later run retries rather than silently losing the opt-in.
131
+ *
132
+ * Both seams are injectable so the migration is unit-testable without touching the
133
+ * real routines dir (and without racing the routine module's own tests).
134
+ */
135
+ export declare function migrateWatchdogSentinelToRoutine(sentinelPath?: string, ensure?: (enabled: boolean) => void): void;
120
136
  /** Run all idempotent migrations. Safe to call multiple times. */
121
137
  export declare function runMigration(): Promise<void>;
@@ -13,6 +13,7 @@ import { atomicWriteFileSync } from './fs-atomic.js';
13
13
  import { machineId } from './machine-id.js';
14
14
  import { AGENTS, agentConfigDirName, findInPath } from './agents.js';
15
15
  import { createLink } from './platform/index.js';
16
+ import { ensureWatchdogRoutine } from './watchdog/routine.js';
16
17
  const HOME = process.env.HOME ?? os.homedir();
17
18
  const USER_DIR = path.join(HOME, '.agents');
18
19
  /** Canonical system-repo location (post-fold). */
@@ -1981,6 +1982,37 @@ export function migrateRoutineDeviceToDevices(routinesDir) {
1981
1982
  console.error(`Migrated ${migrated} routine${migrated === 1 ? '' : 's'}: device → devices`);
1982
1983
  }
1983
1984
  }
1985
+ /**
1986
+ * Fold the legacy watchdog enable sentinel into the watchdog routine.
1987
+ *
1988
+ * The always-on watchdog used to be gated by a presence sentinel at
1989
+ * `<runtime-state>/watchdog/enabled`; it is now a daemon routine. A user who had
1990
+ * run `agents watchdog enable` under the old build has that file on disk — without
1991
+ * this, upgrading would silently drop them back to OFF (the always-on nudge just
1992
+ * stops), the worst failure mode for a "survives reboots" feature. If the sentinel
1993
+ * exists, ensure the `watchdog` routine exists AND is enabled, then delete the
1994
+ * sentinel so this runs exactly once. If ensuring the routine fails, the sentinel
1995
+ * is left in place so a later run retries rather than silently losing the opt-in.
1996
+ *
1997
+ * Both seams are injectable so the migration is unit-testable without touching the
1998
+ * real routines dir (and without racing the routine module's own tests).
1999
+ */
2000
+ export function migrateWatchdogSentinelToRoutine(sentinelPath = path.join(CACHE_DIR, 'state', 'watchdog', 'enabled'), ensure = ensureWatchdogRoutine) {
2001
+ if (!fs.existsSync(sentinelPath))
2002
+ return;
2003
+ try {
2004
+ ensure(true);
2005
+ }
2006
+ catch (err) {
2007
+ console.error(`watchdog sentinel migration: could not create the routine (${err.message}); leaving the sentinel for a later retry`);
2008
+ return;
2009
+ }
2010
+ try {
2011
+ fs.rmSync(sentinelPath);
2012
+ }
2013
+ catch { /* already gone */ }
2014
+ console.error('Migrated watchdog: legacy enable sentinel → watchdog routine (kept enabled)');
2015
+ }
1984
2016
  /** Run all idempotent migrations. Safe to call multiple times. */
1985
2017
  export async function runMigration() {
1986
2018
  // MUST run first: every other migrator reads SYSTEM_DIR (the new path).
@@ -2039,6 +2071,10 @@ export async function runMigration() {
2039
2071
  migrateExtrasExtrasToAgentsExtras();
2040
2072
  // Rewrite routine YAML files: singular `device:` -> plural `devices: []`.
2041
2073
  migrateRoutineDeviceToDevices();
2074
+ // Fold the legacy watchdog enable sentinel into the watchdog routine so a user
2075
+ // who opted in under the old build stays opted in after upgrading. After the
2076
+ // routine rewrites above so the routines dir is in its canonical shape.
2077
+ migrateWatchdogSentinelToRoutine();
2042
2078
  // Symlink repair runs LAST so it can find the post-move version homes.
2043
2079
  repairAgentConfigSymlinks();
2044
2080
  // Repair self-referential node_modules/.bin/<cli> symlinks (the droid
@@ -96,6 +96,33 @@ export declare function resolveModel(agent: AgentId, version: string, requested:
96
96
  * like `${agent}-default`.
97
97
  */
98
98
  export declare function resolveEffectiveModel(agent: AgentId, version: string, requested?: string): string | null;
99
+ /** Where the model a given agent+version will actually run with came from. */
100
+ export type ConfiguredModelSource = 'run-default' | 'config' | 'cli-default';
101
+ export interface ConfiguredModel {
102
+ /** The model id the agent will run with (e.g. `opus`, `gpt-5-codex`). */
103
+ model: string;
104
+ source: ConfiguredModelSource;
105
+ }
106
+ /**
107
+ * The model a given agent+version is actually configured to use right now, with
108
+ * where that selection comes from. First hit wins:
109
+ * 1. run-default — the user's agents-cli `run.defaults` in agents.yaml
110
+ * 2. config — the agent's OWN native settings.json `model` field
111
+ * 3. cli-default — the CLI's built-in default: the catalog's `isDefault` model
112
+ * if one is flagged (e.g. Kimi), otherwise the literal
113
+ * `default` for a model-capable agent whose runtime picks its
114
+ * own default (Claude/Codex don't flag one — Claude's own UI
115
+ * calls this "Default").
116
+ * Each layer is a real source the agent consults; `version` must be concrete.
117
+ * Returns null only when the agent exposes no model catalog at all.
118
+ */
119
+ export declare function resolveConfiguredModel(agent: AgentId, version: string): ConfiguredModel | null;
120
+ /**
121
+ * Join the identity cluster — `agent@version · model · account` — with a dim
122
+ * separator, dropping empty pieces. Pieces are pre-colored by the caller so the
123
+ * same cluster reads identically across `view`, `use`, `add`, and `status`.
124
+ */
125
+ export declare function formatAgentIdentity(...parts: Array<string | null | undefined>): string;
99
126
  /**
100
127
  * Build the per-agent CLI flags for a unified reasoning effort knob.
101
128
  *
@@ -10,8 +10,11 @@
10
10
  import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import { execFileSync } from 'child_process';
13
- import { getVersionDir } from './versions.js';
13
+ import chalk from 'chalk';
14
+ import { getVersionDir, getVersionHomePath } from './versions.js';
14
15
  import { getModelsCachePath } from './state.js';
16
+ import { agentConfigDirName } from './agents.js';
17
+ import { resolveRunDefaults } from './run-defaults.js';
15
18
  const CACHE_PATH = getModelsCachePath();
16
19
  /**
17
20
  * Bump when the extractor logic changes shape in an incompatible way so cached
@@ -864,6 +867,56 @@ export function resolveEffectiveModel(agent, version, requested) {
864
867
  const def = catalog.models.find((m) => m.isDefault);
865
868
  return def?.id ?? null;
866
869
  }
870
+ /**
871
+ * The model a given agent+version is actually configured to use right now, with
872
+ * where that selection comes from. First hit wins:
873
+ * 1. run-default — the user's agents-cli `run.defaults` in agents.yaml
874
+ * 2. config — the agent's OWN native settings.json `model` field
875
+ * 3. cli-default — the CLI's built-in default: the catalog's `isDefault` model
876
+ * if one is flagged (e.g. Kimi), otherwise the literal
877
+ * `default` for a model-capable agent whose runtime picks its
878
+ * own default (Claude/Codex don't flag one — Claude's own UI
879
+ * calls this "Default").
880
+ * Each layer is a real source the agent consults; `version` must be concrete.
881
+ * Returns null only when the agent exposes no model catalog at all.
882
+ */
883
+ export function resolveConfiguredModel(agent, version) {
884
+ const runModel = resolveRunDefaults(agent, version).model;
885
+ if (runModel && runModel.trim() !== '')
886
+ return { model: runModel, source: 'run-default' };
887
+ const nativeModel = readNativeConfigModel(agent, version);
888
+ if (nativeModel)
889
+ return { model: nativeModel, source: 'config' };
890
+ const catalog = getModelCatalog(agent, version);
891
+ if (catalog) {
892
+ const flagged = catalog.models.find((m) => m.isDefault);
893
+ return { model: flagged?.id ?? 'default', source: 'cli-default' };
894
+ }
895
+ return null;
896
+ }
897
+ /**
898
+ * Best-effort read of the agent's own `model` from its native settings.json
899
+ * (e.g. `~/.agents/.history/versions/claude/<ver>/home/.claude/settings.json`).
900
+ * A missing/malformed file is a fall-through, not an error.
901
+ */
902
+ function readNativeConfigModel(agent, version) {
903
+ try {
904
+ const settingsPath = path.join(getVersionHomePath(agent, version), agentConfigDirName(agent), 'settings.json');
905
+ const parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
906
+ return typeof parsed.model === 'string' && parsed.model.trim() !== '' ? parsed.model : null;
907
+ }
908
+ catch {
909
+ return null;
910
+ }
911
+ }
912
+ /**
913
+ * Join the identity cluster — `agent@version · model · account` — with a dim
914
+ * separator, dropping empty pieces. Pieces are pre-colored by the caller so the
915
+ * same cluster reads identically across `view`, `use`, `add`, and `status`.
916
+ */
917
+ export function formatAgentIdentity(...parts) {
918
+ return parts.filter((p) => !!p && p.length > 0).join(` ${chalk.gray('·')} `);
919
+ }
867
920
  /** Find the closest matching model ids/aliases using edit distance. */
868
921
  function pickSuggestions(requested, catalog) {
869
922
  const all = [...catalog.models.map((m) => m.id), ...Object.keys(catalog.aliases)];
@@ -21,6 +21,11 @@ export interface OverdueJob {
21
21
  /** Return every enabled, recurring job whose most recent expected fire was
22
22
  * missed. One-shot jobs are excluded — they fire at most once. */
23
23
  export declare function detectOverdueJobs(now?: Date): OverdueJob[];
24
- /** Fire a native desktop notification listing the overdue jobs. Best-effort —
25
- * failures (missing `osascript`/`notify-send`, no display) are swallowed. */
24
+ /**
25
+ * Fire a native desktop notification. Best-effort — failures (missing
26
+ * `osascript`/`notify-send`, no display, headless box) are swallowed so a
27
+ * notification attempt can never take the daemon down.
28
+ */
29
+ export declare function notifyDesktop(title: string, body: string): void;
30
+ /** Fire a native desktop notification listing the overdue jobs. Best-effort. */
26
31
  export declare function notifyOverdue(jobs: OverdueJob[]): void;
@@ -76,17 +76,12 @@ export function detectOverdueJobs(now = new Date()) {
76
76
  }
77
77
  return overdue;
78
78
  }
79
- /** Fire a native desktop notification listing the overdue jobs. Best-effort —
80
- * failures (missing `osascript`/`notify-send`, no display) are swallowed. */
81
- export function notifyOverdue(jobs) {
82
- if (jobs.length === 0)
83
- return;
84
- const title = jobs.length === 1
85
- ? `Routine overdue: ${jobs[0].name}`
86
- : `${jobs.length} routines overdue`;
87
- const body = jobs.length === 1
88
- ? `Missed ${jobs[0].expectedAt.toLocaleString()}. Run: agents routines catchup`
89
- : `${jobs.map((j) => j.name).join(', ')} — agents routines catchup`;
79
+ /**
80
+ * Fire a native desktop notification. Best-effort — failures (missing
81
+ * `osascript`/`notify-send`, no display, headless box) are swallowed so a
82
+ * notification attempt can never take the daemon down.
83
+ */
84
+ export function notifyDesktop(title, body) {
90
85
  const platform = os.platform();
91
86
  try {
92
87
  if (platform === 'darwin') {
@@ -117,3 +112,15 @@ export function notifyOverdue(jobs) {
117
112
  // Notification is best-effort; nothing to do.
118
113
  }
119
114
  }
115
+ /** Fire a native desktop notification listing the overdue jobs. Best-effort. */
116
+ export function notifyOverdue(jobs) {
117
+ if (jobs.length === 0)
118
+ return;
119
+ const title = jobs.length === 1
120
+ ? `Routine overdue: ${jobs[0].name}`
121
+ : `${jobs.length} routines overdue`;
122
+ const body = jobs.length === 1
123
+ ? `Missed ${jobs[0].expectedAt.toLocaleString()}. Run: agents routines catchup`
124
+ : `${jobs.map((j) => j.name).join(', ')} — agents routines catchup`;
125
+ notifyDesktop(title, body);
126
+ }
@@ -343,7 +343,10 @@ export function dynamicPicker(config) {
343
343
  const [loading, setLoading] = useState(true);
344
344
  const [query, setQuery] = useState('');
345
345
  const [mode, setMode] = useState('nav');
346
- const [previewOpen, setPreviewOpen] = useState(false);
346
+ // Default the preview pane open when the caller supplies a preview builder —
347
+ // matches the static `itemPicker`, so the session browser shows a live preview
348
+ // as you arrow through rows instead of hiding it behind `tab`.
349
+ const [previewOpen, setPreviewOpen] = useState(Boolean(cfg.buildPreview));
347
350
  const [active, setActive] = useState(0);
348
351
  const [flash, setFlash] = useState('');
349
352
  const prefix = usePrefix({ status, theme });
@@ -49,3 +49,20 @@ export declare function backgroundSpawnOptions(opts?: {
49
49
  * throw from `process.kill(pid, 0)` as "not running".
50
50
  */
51
51
  export declare function isAlive(pid: number): boolean;
52
+ /**
53
+ * A stable identifier for the process at `pid` as of when it started, or null if
54
+ * unknowable. Used to defeat PID reuse: acting on a pid is only safe when the
55
+ * process still occupies the slot we observed earlier. The value is only ever
56
+ * compared for equality against an earlier capture of the SAME pid, so the format
57
+ * need only be stable, not parseable.
58
+ *
59
+ * Linux: field 22 of /proc/<pid>/stat (starttime in clock ticks since boot).
60
+ * macOS: `ps -o lstart= -p <pid>`.
61
+ * Windows: CreationDate from Win32_Process, as a culture-independent FILETIME.
62
+ *
63
+ * This is the single source of truth. `pty-server.ts` and `teams/agents.ts` each
64
+ * carried their own copy; the Windows branch was missing from both, so every
65
+ * caller there silently ran with NO pid-reuse protection — including
66
+ * `agents teams stop`, which is how it could SIGKILL an unrelated process group.
67
+ */
68
+ export declare function captureProcessStartTime(pid: number): string | null;