@phnx-labs/agents-cli 1.22.42 → 1.22.44

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 (256) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +15 -13
  3. package/dist/bootstrap.js +33 -13
  4. package/dist/cli/command-registry.d.ts +0 -8
  5. package/dist/cli/command-registry.js +1 -25
  6. package/dist/commands/alias.d.ts +7 -4
  7. package/dist/commands/alias.js +33 -8
  8. package/dist/commands/apply.d.ts +7 -10
  9. package/dist/commands/apply.js +15 -35
  10. package/dist/commands/artifacts.js +4 -5
  11. package/dist/commands/audit.d.ts +6 -6
  12. package/dist/commands/audit.js +12 -12
  13. package/dist/commands/auth.js +14 -6
  14. package/dist/commands/beta.d.ts +7 -1
  15. package/dist/commands/beta.js +16 -9
  16. package/dist/commands/commands.js +1 -1
  17. package/dist/commands/computer.js +20 -6
  18. package/dist/commands/daemon.js +1 -1
  19. package/dist/commands/doctor.d.ts +4 -2
  20. package/dist/commands/doctor.js +21 -9
  21. package/dist/commands/events.d.ts +2 -2
  22. package/dist/commands/events.js +6 -5
  23. package/dist/commands/exec.d.ts +1 -1
  24. package/dist/commands/exec.js +11 -6
  25. package/dist/commands/factory.d.ts +1 -1
  26. package/dist/commands/feed.js +1 -30
  27. package/dist/commands/fleet-capture.d.ts +1 -1
  28. package/dist/commands/fleet-capture.js +4 -5
  29. package/dist/commands/harness.js +4 -52
  30. package/dist/commands/hooks.js +1 -1
  31. package/dist/commands/insights.d.ts +1 -1
  32. package/dist/commands/insights.js +3 -2
  33. package/dist/commands/menubar.js +58 -1
  34. package/dist/commands/monitors.js +1 -1
  35. package/dist/commands/org.d.ts +5 -0
  36. package/dist/commands/org.js +83 -29
  37. package/dist/commands/output.js +2 -2
  38. package/dist/commands/profiles.js +2 -2
  39. package/dist/commands/prune.js +5 -4
  40. package/dist/commands/routines.js +43 -25
  41. package/dist/commands/routines.test-fixture.d.ts +86 -0
  42. package/dist/commands/routines.test-fixture.js +345 -0
  43. package/dist/commands/sessions-picker.js +124 -122
  44. package/dist/commands/sessions.test-fixture.d.ts +24 -0
  45. package/dist/commands/sessions.test-fixture.js +224 -0
  46. package/dist/commands/setup.js +9 -1
  47. package/dist/commands/share.d.ts +9 -10
  48. package/dist/commands/share.js +68 -50
  49. package/dist/commands/skills.js +1 -1
  50. package/dist/commands/snapshot.d.ts +1 -1
  51. package/dist/commands/snapshot.js +4 -4
  52. package/dist/commands/ssh.js +15 -62
  53. package/dist/commands/status.d.ts +4 -2
  54. package/dist/commands/status.js +10 -7
  55. package/dist/commands/subagents.js +1 -1
  56. package/dist/commands/sync.d.ts +1 -0
  57. package/dist/commands/sync.js +29 -2
  58. package/dist/commands/view.js +11 -2
  59. package/dist/commands/webhook.js +1 -1
  60. package/dist/lib/accounting/rotate.d.ts +5 -3
  61. package/dist/lib/accounting/rotate.js +36 -7
  62. package/dist/lib/accounting/usage.d.ts +18 -4
  63. package/dist/lib/accounting/usage.js +138 -27
  64. package/dist/lib/agent-spec/agents.d.ts +7 -7
  65. package/dist/lib/agent-spec/agents.js +15 -15
  66. package/dist/lib/analytics/mix-commands.d.ts +6 -28
  67. package/dist/lib/analytics/mix-commands.js +14 -40
  68. package/dist/lib/audit/log.d.ts +2 -2
  69. package/dist/lib/audit/log.js +2 -2
  70. package/dist/lib/beta.js +1 -1
  71. package/dist/lib/browser/domain-skills.d.ts +26 -6
  72. package/dist/lib/browser/domain-skills.js +81 -43
  73. package/dist/lib/cloud/rush.d.ts +0 -15
  74. package/dist/lib/cloud/rush.js +0 -35
  75. package/dist/lib/crabbox/runtimes.d.ts +6 -2
  76. package/dist/lib/crabbox/runtimes.js +21 -5
  77. package/dist/lib/daemon/daemon.js +97 -58
  78. package/dist/lib/daemon/daemon.test-fixture.d.ts +24 -0
  79. package/dist/lib/daemon/daemon.test-fixture.js +71 -0
  80. package/dist/lib/daemon/runner.d.ts +18 -1
  81. package/dist/lib/daemon/runner.js +105 -11
  82. package/dist/lib/daemon-ticks.d.ts +38 -1
  83. package/dist/lib/daemon-ticks.js +52 -5
  84. package/dist/lib/device-config.js +1 -2
  85. package/dist/lib/devices/discovery-policy.d.ts +3 -3
  86. package/dist/lib/devices/discovery-policy.js +3 -3
  87. package/dist/lib/devices/doctor-findings.js +1 -1
  88. package/dist/lib/devices/fleet.d.ts +4 -8
  89. package/dist/lib/devices/fleet.js +3 -15
  90. package/dist/lib/devices/health-report.js +4 -4
  91. package/dist/lib/devices/pool.d.ts +0 -6
  92. package/dist/lib/devices/pool.js +0 -6
  93. package/dist/lib/devices/registry.d.ts +0 -15
  94. package/dist/lib/devices/registry.js +0 -9
  95. package/dist/lib/devices/stats-cache.d.ts +12 -0
  96. package/dist/lib/devices/stats-cache.js +24 -5
  97. package/dist/lib/drift-sync.d.ts +1 -1
  98. package/dist/lib/drift-sync.js +2 -2
  99. package/dist/lib/drift.d.ts +1 -1
  100. package/dist/lib/exec.js +13 -3
  101. package/dist/lib/fleet/capture.d.ts +1 -1
  102. package/dist/lib/fleet/manifest.js +1 -1
  103. package/dist/lib/fleet/remote-login.d.ts +2 -2
  104. package/dist/lib/fleet/remote-login.js +3 -4
  105. package/dist/lib/format.d.ts +9 -0
  106. package/dist/lib/format.js +16 -0
  107. package/dist/lib/github/pr-mergeable.d.ts +49 -0
  108. package/dist/lib/github/pr-mergeable.js +157 -0
  109. package/dist/lib/github/pr-verdict.d.ts +67 -0
  110. package/dist/lib/github/pr-verdict.js +72 -0
  111. package/dist/lib/harness/adapter.d.ts +6 -5
  112. package/dist/lib/hosts/dispatch.d.ts +2 -0
  113. package/dist/lib/hosts/dispatch.js +8 -1
  114. package/dist/lib/hosts/passthrough.d.ts +10 -0
  115. package/dist/lib/hosts/passthrough.js +28 -7
  116. package/dist/lib/hosts/providers/devices.js +1 -13
  117. package/dist/lib/hosts/ready.d.ts +12 -3
  118. package/dist/lib/hosts/ready.js +27 -12
  119. package/dist/lib/hosts/registry.d.ts +4 -5
  120. package/dist/lib/hosts/registry.js +3 -7
  121. package/dist/lib/hosts/session-index.js +1 -1
  122. package/dist/lib/installations/shims.js +1 -1
  123. package/dist/lib/installations/versions.d.ts +40 -0
  124. package/dist/lib/installations/versions.js +78 -4
  125. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
  126. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  127. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
  128. package/dist/lib/menubar/install-menubar.d.ts +109 -0
  129. package/dist/lib/menubar/install-menubar.js +218 -12
  130. package/dist/lib/monitors/config.d.ts +2 -2
  131. package/dist/lib/monitors/config.js +3 -2
  132. package/dist/lib/origin-machine.d.ts +18 -0
  133. package/dist/lib/origin-machine.js +34 -0
  134. package/dist/lib/perf/db.js +1 -1
  135. package/dist/lib/perf/spool.js +1 -1
  136. package/dist/lib/prix-account.d.ts +2 -1
  137. package/dist/lib/prix-account.js +2 -1
  138. package/dist/lib/profiles.d.ts +7 -0
  139. package/dist/lib/profiles.js +26 -1
  140. package/dist/lib/remote-agents-json.js +1 -7
  141. package/dist/lib/routines-placement.d.ts +1 -1
  142. package/dist/lib/routines-placement.js +1 -1
  143. package/dist/lib/sandbox.d.ts +24 -0
  144. package/dist/lib/sandbox.js +101 -0
  145. package/dist/lib/scheduling/routines.d.ts +18 -9
  146. package/dist/lib/scheduling/routines.js +16 -8
  147. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  148. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
  149. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  150. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
  151. package/dist/lib/secrets/agent.js +9 -1
  152. package/dist/lib/secrets/index.js +1 -1
  153. package/dist/lib/secrets/list-filter.js +1 -1
  154. package/dist/lib/service-manifest.d.ts +20 -0
  155. package/dist/lib/service-manifest.js +34 -0
  156. package/dist/lib/session/active.d.ts +4 -16
  157. package/dist/lib/session/active.js +2 -6
  158. package/dist/lib/session/cloud.js +1 -1
  159. package/dist/lib/session/db.d.ts +13 -98
  160. package/dist/lib/session/db.js +5 -12
  161. package/dist/lib/session/discover.d.ts +5 -81
  162. package/dist/lib/session/discover.js +5 -5
  163. package/dist/lib/session/fork.js +1 -1
  164. package/dist/lib/session/live-metadata.js +1 -1
  165. package/dist/lib/session/origin-machine.d.ts +1 -18
  166. package/dist/lib/session/origin-machine.js +4 -34
  167. package/dist/lib/session/relative-time.d.ts +1 -44
  168. package/dist/lib/session/relative-time.js +4 -120
  169. package/dist/lib/session/remote/remote-bundle.d.ts +12 -0
  170. package/dist/lib/session/remote/remote-bundle.js +61 -0
  171. package/dist/lib/session/remote/remote-list.d.ts +148 -0
  172. package/dist/lib/session/remote/remote-list.js +607 -0
  173. package/dist/lib/session/remote/remote.d.ts +132 -0
  174. package/dist/lib/session/remote/remote.js +314 -0
  175. package/dist/lib/session/remote/watch.d.ts +101 -0
  176. package/dist/lib/session/remote/watch.js +241 -0
  177. package/dist/lib/session/remote-bundle.d.ts +1 -12
  178. package/dist/lib/session/remote-bundle.js +3 -61
  179. package/dist/lib/session/remote-list.d.ts +1 -148
  180. package/dist/lib/session/remote-list.js +3 -607
  181. package/dist/lib/session/remote.d.ts +1 -132
  182. package/dist/lib/session/remote.js +3 -314
  183. package/dist/lib/session/short-id.d.ts +1 -17
  184. package/dist/lib/session/short-id.js +4 -20
  185. package/dist/lib/session/watch.d.ts +1 -101
  186. package/dist/lib/session/watch.js +3 -242
  187. package/dist/lib/session/width.d.ts +1 -29
  188. package/dist/lib/session/width.js +4 -101
  189. package/dist/lib/share/config.d.ts +12 -2
  190. package/dist/lib/share/config.js +47 -11
  191. package/dist/lib/share/delete.js +1 -1
  192. package/dist/lib/share/publish.js +1 -1
  193. package/dist/lib/share/worker-template.js +2 -2
  194. package/dist/lib/smart-launch.d.ts +4 -4
  195. package/dist/lib/smart-launch.js +8 -18
  196. package/dist/lib/snapshot.d.ts +2 -2
  197. package/dist/lib/snapshot.js +1 -1
  198. package/dist/lib/staleness/index.d.ts +3 -1
  199. package/dist/lib/staleness/index.js +21 -1
  200. package/dist/lib/staleness/types.d.ts +11 -0
  201. package/dist/lib/staleness/writers/commands.js +13 -4
  202. package/dist/lib/staleness/writers/hooks.js +3 -1
  203. package/dist/lib/staleness/writers/mcp.d.ts +0 -7
  204. package/dist/lib/staleness/writers/mcp.js +16 -1
  205. package/dist/lib/staleness/writers/rules.js +1 -1
  206. package/dist/lib/staleness/writers/skills.js +3 -1
  207. package/dist/lib/staleness/writers/subagents.d.ts +0 -9
  208. package/dist/lib/staleness/writers/subagents.js +18 -1
  209. package/dist/lib/staleness/writers/types.d.ts +10 -0
  210. package/dist/lib/startup/command-registry.d.ts +10 -1
  211. package/dist/lib/startup/command-registry.js +26 -7
  212. package/dist/lib/startup/root-command.d.ts +18 -1
  213. package/dist/lib/startup/root-command.js +18 -1
  214. package/dist/lib/state.js +7 -0
  215. package/dist/lib/teams/placement-probe.js +28 -13
  216. package/dist/lib/teams/scheduler.d.ts +3 -1
  217. package/dist/lib/teams/scheduler.js +1 -1
  218. package/dist/lib/text/relative-time.d.ts +44 -0
  219. package/dist/lib/text/relative-time.js +120 -0
  220. package/dist/lib/text/short-id.d.ts +17 -0
  221. package/dist/lib/text/short-id.js +20 -0
  222. package/dist/lib/text/width.d.ts +29 -0
  223. package/dist/lib/text/width.js +101 -0
  224. package/dist/lib/triggers/handlers.d.ts +7 -0
  225. package/dist/lib/triggers/handlers.js +8 -0
  226. package/dist/lib/triggers/webhook.d.ts +1 -0
  227. package/dist/lib/triggers/webhook.js +2 -1
  228. package/dist/lib/types.d.ts +1 -1
  229. package/dist/lib/view-types.d.ts +7 -0
  230. package/package.json +2 -3
  231. package/dist/bin/agents +0 -0
  232. package/dist/commands/serve.d.ts +0 -10
  233. package/dist/commands/serve.js +0 -68
  234. package/dist/commands/tickets.d.ts +0 -2
  235. package/dist/commands/tickets.js +0 -43
  236. package/dist/commands/trends.d.ts +0 -10
  237. package/dist/commands/trends.js +0 -12
  238. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  239. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  240. package/dist/lib/observe-aliases.d.ts +0 -30
  241. package/dist/lib/observe-aliases.js +0 -38
  242. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  243. package/dist/lib/serve/control.d.ts +0 -95
  244. package/dist/lib/serve/control.js +0 -260
  245. package/dist/lib/serve/data.d.ts +0 -81
  246. package/dist/lib/serve/data.js +0 -91
  247. package/dist/lib/serve/page.d.ts +0 -7
  248. package/dist/lib/serve/page.js +0 -140
  249. package/dist/lib/serve/server.d.ts +0 -80
  250. package/dist/lib/serve/server.js +0 -145
  251. package/dist/lib/serve/stream.d.ts +0 -43
  252. package/dist/lib/serve/stream.js +0 -116
  253. package/dist/lib/serve/token.d.ts +0 -35
  254. package/dist/lib/serve/token.js +0 -85
  255. package/dist/lib/tickets/list.d.ts +0 -53
  256. package/dist/lib/tickets/list.js +0 -153
@@ -43,6 +43,14 @@ const ENV_ALLOWLIST = [
43
43
  'VISUAL',
44
44
  'NO_COLOR',
45
45
  'FORCE_COLOR',
46
+ // GitHub CLI — same host credentials interactive `agents run` already sees.
47
+ // Without these, a sandboxed monitor `--run` child has no gh auth even when
48
+ // the daemon user is logged in (RUSH-2860).
49
+ 'GH_TOKEN',
50
+ 'GH_HOST',
51
+ 'GH_ENTERPRISE_TOKEN',
52
+ 'GITHUB_TOKEN',
53
+ 'GH_CONFIG_DIR',
46
54
  ];
47
55
  /** Tools safe to grant as wildcards (no filesystem access). */
48
56
  const SAFE_TOOLS = {
@@ -57,6 +65,32 @@ function tomlString(value) {
57
65
  }
58
66
  return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
59
67
  }
68
+ /**
69
+ * Absolute path to this host's `gh` config directory (`hosts.yml` + `config.yml`),
70
+ * or null when the host has never run `gh auth login`. Prefer `GH_CONFIG_DIR` when
71
+ * the parent already pinned one; otherwise `$XDG_CONFIG_HOME/gh` or `~/.config/gh`.
72
+ */
73
+ export function resolveHostGhConfigDir() {
74
+ // Honor an explicit pin only when it actually exists; otherwise fall through
75
+ // to the default locations so a stale/broken GH_CONFIG_DIR cannot hide a
76
+ // healthy ~/.config/gh (RUSH-2860).
77
+ if (process.env.GH_CONFIG_DIR && fs.existsSync(process.env.GH_CONFIG_DIR)) {
78
+ return process.env.GH_CONFIG_DIR;
79
+ }
80
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(resolveRealHome(), '.config');
81
+ const ghDir = path.join(configHome, 'gh');
82
+ return fs.existsSync(ghDir) ? ghDir : null;
83
+ }
84
+ /** True when this host holds usable GitHub CLI auth (hosts.yml or a token env). */
85
+ export function hostHasGhAuth() {
86
+ if (process.env.GH_TOKEN || process.env.GH_ENTERPRISE_TOKEN || process.env.GITHUB_TOKEN) {
87
+ return true;
88
+ }
89
+ const dir = resolveHostGhConfigDir();
90
+ if (!dir)
91
+ return false;
92
+ return fs.existsSync(path.join(dir, 'hosts.yml'));
93
+ }
60
94
  /** Build a restricted environment for a sandboxed process, setting HOME to the overlay. */
61
95
  export function buildSpawnEnv(overlayHome, extraEnv) {
62
96
  const env = {
@@ -68,6 +102,18 @@ export function buildSpawnEnv(overlayHome, extraEnv) {
68
102
  env[key] = process.env[key];
69
103
  }
70
104
  }
105
+ // Pin GH_CONFIG_DIR at the REAL host config so `gh` does not look under the
106
+ // disposable overlay HOME (which has no hosts.yml). Same posture as linking
107
+ // Cursor's auth.json — forward the credentials the daemon user already holds
108
+ // (RUSH-2860). Always prefer resolveHostGhConfigDir over a stale allowlisted
109
+ // value; an explicit extraEnv GH_CONFIG_DIR still wins below.
110
+ const hostGh = resolveHostGhConfigDir();
111
+ if (hostGh) {
112
+ env.GH_CONFIG_DIR = hostGh;
113
+ }
114
+ else {
115
+ delete env.GH_CONFIG_DIR;
116
+ }
71
117
  if (extraEnv) {
72
118
  Object.assign(env, extraEnv);
73
119
  }
@@ -101,11 +147,66 @@ export function prepareJobHome(config) {
101
147
  else if (config.agent === 'cursor') {
102
148
  generateCursorConfig(overlayHome);
103
149
  }
150
+ // Host tool credentials / setup the overlay HOME would otherwise hide.
151
+ // Cursor already links its own auth above; gh is harness-agnostic and needed
152
+ // by every sandboxed `--run` that shells out to `gh` (RUSH-2860 — monitor
153
+ // merge agents saw "not logged into any GitHub hosts" while the daemon user
154
+ // was fine).
155
+ //
156
+ // Deliberately NOT linking the real ~/.agents here. It would put the secrets
157
+ // master key (.secrets-key) and the encrypted store (.cache/secrets) together
158
+ // at a predictable path inside the overlay, through a writable symlink, in a
159
+ // child whose prompt can carry untrusted text from a watched source. The
160
+ // separate "agents-cli is not set up" defect (state.ts ignores AGENTS_USER_DIR)
161
+ // gets its own scoped change: RUSH-2954.
162
+ linkHostGhConfig(overlayHome);
104
163
  if (config.allow?.dirs) {
105
164
  symlinkAllowedDirs(overlayHome, config.allow.dirs);
106
165
  }
107
166
  return overlayHome;
108
167
  }
168
+ /**
169
+ * Link this host's `gh` config directory into the disposable overlay so
170
+ * `$HOME/.config/gh` resolves even when `GH_CONFIG_DIR` is unset. Mirrors
171
+ * `generateCursorConfig`: same-host credentials, never copied to another box.
172
+ */
173
+ export function linkHostGhConfig(overlayHome) {
174
+ const realGhDir = resolveHostGhConfigDir();
175
+ if (!realGhDir || !fs.existsSync(realGhDir))
176
+ return;
177
+ const overlayGhDir = path.join(overlayHome, '.config', 'gh');
178
+ if (fs.existsSync(overlayGhDir))
179
+ return;
180
+ fs.mkdirSync(path.dirname(overlayGhDir), { recursive: true });
181
+ try {
182
+ createLink(realGhDir, overlayGhDir);
183
+ }
184
+ catch {
185
+ /* cross-volume or link creation refused: GH_CONFIG_DIR in buildSpawnEnv is the backup */
186
+ }
187
+ }
188
+ /**
189
+ * Refuse to launch a sandboxed child when THIS host holds GitHub auth but the
190
+ * spawn env would hide it. The RUSH-2860 failure mode was silent: the monitor
191
+ * fire recorded `ok` (spawn succeeded) while every `gh` call inside the child
192
+ * failed. Prefer forwarding (buildSpawnEnv / linkHostGhConfig); this assert is
193
+ * the regression tripwire — if forwarding ever regresses, fail loud at spawn
194
+ * instead of recording a hollow success. No-ops when the host has no gh auth
195
+ * (jobs that do not need GitHub still run).
196
+ */
197
+ export function assertSandboxForwardsHostGhAuth(spawnEnv) {
198
+ if (!hostHasGhAuth())
199
+ return;
200
+ if (spawnEnv.GH_TOKEN || spawnEnv.GH_ENTERPRISE_TOKEN || spawnEnv.GITHUB_TOKEN)
201
+ return;
202
+ if (spawnEnv.GH_CONFIG_DIR && fs.existsSync(path.join(spawnEnv.GH_CONFIG_DIR, 'hosts.yml')))
203
+ return;
204
+ if (spawnEnv.HOME && fs.existsSync(path.join(spawnEnv.HOME, '.config', 'gh', 'hosts.yml')))
205
+ return;
206
+ throw new Error("sandbox spawn would hide this host's GitHub auth from the child " +
207
+ '(no GH_CONFIG_DIR / ~/.config/gh / GH_TOKEN). Refusing to launch — ' +
208
+ 'the agent would record ok then fail every gh call (RUSH-2860).');
209
+ }
109
210
  /** Link this host's Cursor login and CLI config into the disposable overlay. */
110
211
  export function generateCursorConfig(overlayHome) {
111
212
  const realConfigHome = process.env.XDG_CONFIG_HOME || path.join(resolveRealHome(), '.config');
@@ -107,8 +107,14 @@ export interface JobConfig {
107
107
  schedule?: string;
108
108
  /** Event/webhook fire condition. Optional when `schedule` is set. */
109
109
  trigger?: JobTrigger;
110
- /** Which agent runs the routine. Optional — omitted for `workflow`/`command` routines. Exactly one of agent/workflow/command must be set. */
111
- agent?: AgentId;
110
+ /**
111
+ * Which agent runs the routine — a native harness id, or the name of a
112
+ * custom harness (`agents harness list`), which the runner delegates to
113
+ * `agents run <name>` the same way workflow jobs are. Optional — omitted
114
+ * for `workflow`/`command` routines. Exactly one of agent/workflow/command
115
+ * must be set.
116
+ */
117
+ agent?: AgentId | (string & {});
112
118
  workflow?: string;
113
119
  /**
114
120
  * A plain shell command run directly instead of an agent/workflow — no LLM,
@@ -298,16 +304,16 @@ export interface JobConfig {
298
304
  * `lib/monitors/config.ts`) — re-gating here was double-gating on the wrong
299
305
  * key.
300
306
  *
301
- * Deliberately narrow: a monitor's `routine` action fires a REAL routine, which
302
- * keeps its activation gate, so a routine defined but not activated on this
303
- * device is still refused.
307
+ * Deliberately narrow: a monitor's or webhook handler's `routine` action fires a
308
+ * REAL routine, which keeps its activation gate, so a routine defined but not
309
+ * activated on this device is still refused.
304
310
  *
305
311
  * Runtime-only, enforced at both ends of the schema boundary: `writeJob`
306
312
  * strips it, and `readJobFileResult` refuses a definition that carries it
307
313
  * (a hand-authored one would otherwise fire on every box regardless of
308
314
  * activation, since the daemon's load path never calls `validateJob`).
309
315
  */
310
- dispatchedBy?: 'monitor';
316
+ dispatchedBy?: 'monitor' | 'webhook';
311
317
  }
312
318
  /**
313
319
  * Canonical form of a routine's `projects` field: drop non-string and empty
@@ -399,7 +405,7 @@ export declare function resolveJobExecutionContext(config: Pick<JobConfig, 'name
399
405
  export interface RunMeta {
400
406
  jobName: string;
401
407
  runId: string;
402
- agent?: AgentId;
408
+ agent?: AgentId | (string & {});
403
409
  /**
404
410
  * Resolved agent version this run launched under. Re-pointed to each failover
405
411
  * attempt's version as the single-shot chain advances (runner.ts), so it names
@@ -504,10 +510,13 @@ export declare function finalizeRunMeta(meta: RunMeta, status: RunMeta['status']
504
510
  * `yosemite-s0` all agree. Every fire path (cron scheduler, webhook,
505
511
  * catchup/overdue, manual run) gates on this.
506
512
  *
507
- * A monitor-dispatched job ({@link JobConfig.dispatchedBy} `=== 'monitor'`) skips
513
+ * A monitor- or webhook-dispatched job ({@link JobConfig.dispatchedBy} set) skips
508
514
  * the routine activation manifest: it is not a routine, so its name can never be
509
515
  * a member and the lookup could only ever answer "not activated here"
510
- * (RUSH-2681). Its ownership was already decided by the monitor's `device:` pin.
516
+ * (RUSH-2681 for monitors; the same hole broke every `run.agent`/`run.workflow`
517
+ * webhook handler, which the receiver logged as `fired` while the run record read
518
+ * `skipped` with an empty allowlist). Ownership was already decided upstream — by
519
+ * the monitor's `device:` pin, or by the one box the webhook was delivered to.
511
520
  */
512
521
  export declare function jobRunsOnThisDevice(config: Pick<JobConfig, 'name' | 'devices' | 'dispatchedBy'>): boolean;
513
522
  /**
@@ -14,6 +14,7 @@ import { getRoutinesDir, getSystemRoutinesDir, getRunsDir, ensureAgentsDir, getP
14
14
  import * as os from 'os';
15
15
  import { safeJoin, isSafeSegmentName } from '../paths.js';
16
16
  import { isSafeProjectName, loadProjectDef, projectBasePath } from '../projects.js';
17
+ import { isCustomHarnessName } from '../profiles.js';
17
18
  import { resolveRoutineExecutionContext, } from '../routine-context.js';
18
19
  import { atomicWriteFileSync } from '../fs-atomic.js';
19
20
  import { ALL_AGENT_IDS, ROUTINE_AGENT_IDS } from '../agents.js';
@@ -351,13 +352,16 @@ export function finalizeRunMeta(meta, status, exitCode, opts) {
351
352
  * `yosemite-s0` all agree. Every fire path (cron scheduler, webhook,
352
353
  * catchup/overdue, manual run) gates on this.
353
354
  *
354
- * A monitor-dispatched job ({@link JobConfig.dispatchedBy} `=== 'monitor'`) skips
355
+ * A monitor- or webhook-dispatched job ({@link JobConfig.dispatchedBy} set) skips
355
356
  * the routine activation manifest: it is not a routine, so its name can never be
356
357
  * a member and the lookup could only ever answer "not activated here"
357
- * (RUSH-2681). Its ownership was already decided by the monitor's `device:` pin.
358
+ * (RUSH-2681 for monitors; the same hole broke every `run.agent`/`run.workflow`
359
+ * webhook handler, which the receiver logged as `fired` while the run record read
360
+ * `skipped` with an empty allowlist). Ownership was already decided upstream — by
361
+ * the monitor's `device:` pin, or by the one box the webhook was delivered to.
358
362
  */
359
363
  export function jobRunsOnThisDevice(config) {
360
- if (config.dispatchedBy !== 'monitor') {
364
+ if (config.dispatchedBy === undefined) {
361
365
  const activated = routineEnabledOnThisDevice(config.name);
362
366
  if (activated !== null)
363
367
  return activated;
@@ -636,7 +640,7 @@ export function readJobFileResult(filePath) {
636
640
  // written back (writeJob deletes it), so its presence here means hand-authored
637
641
  // state, not drift.
638
642
  if (Object.prototype.hasOwnProperty.call(parsed, 'dispatchedBy')) {
639
- return { config: null, problem: '`dispatchedBy:` is a runtime-only monitor marker, not a routine field — routine is inert' };
643
+ return { config: null, problem: '`dispatchedBy:` is a runtime-only monitor/webhook marker, not a routine field — routine is inert' };
640
644
  }
641
645
  return {
642
646
  config: {
@@ -844,13 +848,17 @@ export function validateJob(config) {
844
848
  if (config.command !== undefined && (typeof config.command !== 'string' || config.command.trim() === '')) {
845
849
  errors.push('command must be a non-empty shell command string');
846
850
  }
847
- if (hasAgent && config.agent && !ALL_AGENT_IDS.includes(config.agent)) {
848
- errors.push(`agent must be one of: ${ALL_AGENT_IDS.join(', ')}`);
851
+ if (hasAgent && config.agent && !ALL_AGENT_IDS.includes(config.agent) && !isCustomHarnessName(config.agent)) {
852
+ errors.push(`agent must be one of: ${ALL_AGENT_IDS.join(', ')}, or a custom harness (agents harness list)`);
849
853
  }
850
854
  else if (hasAgent && config.agent && strategy === 'local' &&
851
- !ROUTINE_AGENT_IDS.includes(config.agent)) {
855
+ !ROUTINE_AGENT_IDS.includes(config.agent) &&
856
+ // A custom harness is exempt from the local-daemon command table: the
857
+ // runner delegates it to `agents run <name>`, the same path workflow
858
+ // jobs take, so no ROUTINE_AGENT_COMMANDS template is needed.
859
+ !isCustomHarnessName(config.agent)) {
852
860
  // The local daemon only knows how to build a command for the agents in
853
- // ROUTINE_AGENT_IDS (runner.ts's AGENT_COMMANDS table) — anything else is a
861
+ // ROUTINE_AGENT_IDS (baked from AGENT_COMMANDS in runner.ts) — anything else is a
854
862
  // real, installable agent (it passed the ALL_AGENT_IDS check above) but one
855
863
  // the daemon can't fire itself, so reject it now instead of accepting the
856
864
  // routine and failing at fire time (runner.ts buildJobCommand: "Unsupported
@@ -8,8 +8,6 @@
8
8
  <string>Agents CLI</string>
9
9
  <key>CFBundleDisplayName</key>
10
10
  <string>Agents CLI</string>
11
- <key>CFBundleIconFile</key>
12
- <string>AppIcon</string>
13
11
  <key>CFBundleExecutable</key>
14
12
  <string>Agents CLI</string>
15
13
  <key>CFBundlePackageType</key>
@@ -3,21 +3,9 @@
3
3
  <plist version="1.0">
4
4
  <dict>
5
5
  <key>files</key>
6
- <dict>
7
- <key>Resources/AppIcon.icns</key>
8
- <data>
9
- DFq5H08EkhgWIC3UvGMR9B58BZw=
10
- </data>
11
- </dict>
6
+ <dict/>
12
7
  <key>files2</key>
13
8
  <dict>
14
- <key>Resources/AppIcon.icns</key>
15
- <dict>
16
- <key>hash2</key>
17
- <data>
18
- mBSjM6jlvN7J1jQowsvqTl7CHM0pur0e6qsHMuzk5k0=
19
- </data>
20
- </dict>
21
9
  <key>embedded.provisionprofile</key>
22
10
  <dict>
23
11
  <key>hash2</key>
@@ -36,6 +36,7 @@ import { getCliVersion, getCliVersionFresh } from '../version.js';
36
36
  import { getCliLaunch } from '../cli-entry.js';
37
37
  import { GLOBAL_HARNESS, bundleScopeChain } from './scope.js';
38
38
  import { deleteLeaseSession, rehydrateSessions, pruneSessionsOnSleep } from './session-store.js';
39
+ import { serviceManagerRegistrationAllowed } from '../service-manifest.js';
39
40
  import { SYNC_GET_CMD, SYNC_PING_CMD, SYNC_LOCK_CMD } from './sync-commands.js';
40
41
  import { MAX_LEASE_MS, MIN_LEASE_MS } from './lease.js';
41
42
  import { selectLeasedEnv } from './lease.js';
@@ -316,7 +317,11 @@ export function retireLegacySecretsAgentService() {
316
317
  const plist = servicePlistPath();
317
318
  // Only the real LaunchAgents dir is launchd-managed; a relocated (test) dir
318
319
  // has no bootstrapped job, so skip launchctl and just remove the plist.
319
- if (!process.env.AGENTS_SECRETS_LAUNCHAGENTS_DIR) {
320
+ // Also skip launchctl under a redirected HOME: launchctl is per-user-session
321
+ // and HOME-independent, so a sandboxed process would still talk to the real
322
+ // launchd (RUSH-2968).
323
+ const reg = serviceManagerRegistrationAllowed();
324
+ if (!process.env.AGENTS_SECRETS_LAUNCHAGENTS_DIR && reg.allowed) {
320
325
  const uid = process.getuid?.() ?? 0;
321
326
  try {
322
327
  execFileSync('launchctl', ['bootout', `gui/${uid}/${SERVICE_LABEL}`], { stdio: ['ignore', 'ignore', 'ignore'] });
@@ -328,6 +333,9 @@ export function retireLegacySecretsAgentService() {
328
333
  catch { /* not loaded */ }
329
334
  }
330
335
  }
336
+ else if (!reg.allowed) {
337
+ process.stderr.write(`[agents] ${reg.reason}\n`);
338
+ }
331
339
  try {
332
340
  fs.unlinkSync(plist);
333
341
  }
@@ -32,7 +32,7 @@ import { windowsBackend, usesFileFallback as windowsUsesFileFallback, importNati
32
32
  import { isHeadlessSecretsContext } from './headless.js';
33
33
  import { KEYCHAIN_READ_BACKOFF_TTL_MS, clearKeychainReadBackoff, isKeychainReadBackedOff, noteKeychainReadFailure, } from './read-backoff.js';
34
34
  import { getKeychainHelperPath } from './install-helper.js';
35
- import { deriveShortId } from '../session/short-id.js';
35
+ import { deriveShortId } from '../text/short-id.js';
36
36
  const SERVICE_PREFIX = 'agents-cli';
37
37
  export const SECRETS_ITEM_PREFIX = `${SERVICE_PREFIX}.secrets.`;
38
38
  const BUNDLES_ITEM_PREFIX = `${SERVICE_PREFIX}.bundles.`;
@@ -21,7 +21,7 @@ import { parseCommaSeparatedList } from '../format.js';
21
21
  // From the leaf module, NOT discover.js — the latter imports `../sqlite.js`, so
22
22
  // reaching through it for one parser would pull node:sqlite into every `agents
23
23
  // secrets` invocation and print Node's SQLite ExperimentalWarning on stderr.
24
- import { parseTimeFilter } from '../session/relative-time.js';
24
+ import { parseTimeFilter } from '../text/relative-time.js';
25
25
  import { describeBundle, bundlePolicy, SECRET_TYPES, } from './bundles.js';
26
26
  /** Ref kinds a var's value can have, from `describeBundle`. */
27
27
  export const REF_KINDS = ['literal', 'keychain', 'env', 'file', 'exec'];
@@ -27,3 +27,23 @@ export declare function serviceManifestHomeEnv(): {
27
27
  HOME: string;
28
28
  AGENTS_REAL_HOME: string;
29
29
  };
30
+ /**
31
+ * Whether this process may safely register (or tear down) a service with the
32
+ * user's real service manager.
33
+ *
34
+ * `launchctl` and `systemctl --user` are per-user-session and HOME-independent:
35
+ * a process running under a redirected HOME still talks to the SAME launchd or
36
+ * systemd instance as a normal login session. service-manifest.ts namespaces the
37
+ * job label under a redirected HOME, but that only changes the identifier — the
38
+ * registration still lands in the real service manager. A hermetic test fork (or
39
+ * any CLI running under a sandbox HOME) must therefore never touch launchd/
40
+ * systemd unless it has explicitly opted in.
41
+ *
42
+ * `AGENTS_SERVICE_MANAGER_ALLOW_REDIRECTED_HOME=1` is the test seam for the
43
+ * shim/PATH-based tests that intentionally exercise the registration code path
44
+ * under a redirected HOME. Production code must never set it.
45
+ */
46
+ export declare function serviceManagerRegistrationAllowed(): {
47
+ allowed: boolean;
48
+ reason: string;
49
+ };
@@ -78,3 +78,37 @@ export function serviceManifestHomeEnv() {
78
78
  const home = process.env.HOME || os.homedir();
79
79
  return { HOME: home, AGENTS_REAL_HOME: process.env.AGENTS_REAL_HOME || home };
80
80
  }
81
+ /**
82
+ * Whether this process may safely register (or tear down) a service with the
83
+ * user's real service manager.
84
+ *
85
+ * `launchctl` and `systemctl --user` are per-user-session and HOME-independent:
86
+ * a process running under a redirected HOME still talks to the SAME launchd or
87
+ * systemd instance as a normal login session. service-manifest.ts namespaces the
88
+ * job label under a redirected HOME, but that only changes the identifier — the
89
+ * registration still lands in the real service manager. A hermetic test fork (or
90
+ * any CLI running under a sandbox HOME) must therefore never touch launchd/
91
+ * systemd unless it has explicitly opted in.
92
+ *
93
+ * `AGENTS_SERVICE_MANAGER_ALLOW_REDIRECTED_HOME=1` is the test seam for the
94
+ * shim/PATH-based tests that intentionally exercise the registration code path
95
+ * under a redirected HOME. Production code must never set it.
96
+ */
97
+ export function serviceManagerRegistrationAllowed() {
98
+ const suffix = isolatedHomeSuffix();
99
+ if (!suffix) {
100
+ return { allowed: true, reason: 'production HOME: service-manager registration allowed' };
101
+ }
102
+ if (process.env.AGENTS_SERVICE_MANAGER_ALLOW_REDIRECTED_HOME === '1') {
103
+ return {
104
+ allowed: true,
105
+ reason: `test seam AGENTS_SERVICE_MANAGER_ALLOW_REDIRECTED_HOME allows registration under sandbox-${suffix}`,
106
+ };
107
+ }
108
+ return {
109
+ allowed: false,
110
+ reason: `refusing service-manager registration under redirected HOME (sandbox-${suffix}): ` +
111
+ `launchctl/systemd are per-user-session and HOME-independent, so a sandboxed process ` +
112
+ `would register in the real service manager (RUSH-2968)`,
113
+ };
114
+ }
@@ -59,7 +59,7 @@ export declare function attributedSetLostPids(prev: Set<number>, next: Set<numbe
59
59
  * the same pid-reuse guard {@link isPidAlive} uses elsewhere.
60
60
  */
61
61
  export declare function filterCachedUnattributed(sessions: ActiveSession[], attributed: Set<number>, alive: (pid: number, startedAtMs?: number) => boolean): ActiveSession[];
62
- export type ActiveContext = 'terminal' | 'teams' | 'cloud' | 'headless';
62
+ type ActiveContext = 'terminal' | 'teams' | 'cloud' | 'headless';
63
63
  /** The SessionMeta fields the live-row backfill reads — the enrichment a running process cannot report. */
64
64
  export type BackfillMeta = Pick<SessionMeta, 'version' | 'timestamp' | 'label' | 'ticketId' | 'prUrl' | 'prNumber' | 'origin' | 'routineName'>;
65
65
  export declare function backfillActiveRowsFromMeta(sessions: ActiveSession[], metaById: Map<string, BackfillMeta>): void;
@@ -335,7 +335,7 @@ export interface ActiveSession {
335
335
  paneId?: string;
336
336
  }
337
337
  export declare function activeStatusFromCloudStatus(status: CloudTaskStatus): ActiveStatus;
338
- export interface ActiveQueryOptions {
338
+ interface ActiveQueryOptions {
339
339
  /** Skip the `ps` scan for ad-hoc headless agents. */
340
340
  skipHeadless?: boolean;
341
341
  /**
@@ -359,14 +359,6 @@ export interface ActiveQueryOptions {
359
359
  export declare class TmuxDiscoveryDegradedError extends Error {
360
360
  constructor(message: string);
361
361
  }
362
- /**
363
- * A live process can only borrow an indexed session file if that transcript
364
- * has been touched recently enough to plausibly belong to the process. This is
365
- * deliberately wider than ACTIVE_MTIME_WINDOW_MS: an inactive-but-live CLI can
366
- * be idle for longer than 2 minutes, but it must not attach to a weeks-old
367
- * transcript just because a GUI app service with the same basename is alive.
368
- */
369
- export declare const ACTIVE_SESSION_STALE_MS: number;
370
362
  /**
371
363
  * A session whose transcript hasn't been written in this long is ABANDONED /
372
364
  * dangling — the framework can no longer treat it as live work, whether its PID
@@ -420,8 +412,6 @@ export declare function resolveNamesToSessionIds(sessionNames: string[], deps: {
420
412
  * the start time can't be read, we keep the existence answer.
421
413
  */
422
414
  export declare function isPidAlive(pid: number, startedAtMs?: number): boolean;
423
- /** Test seam: drop the Claude session-file path memo. */
424
- export declare function clearClaudeSessionFileCacheForTest(): void;
425
415
  /**
426
416
  * Resolve a Claude transcript for `cwd` across the given project roots, newest
427
417
  * mtime winning when the same id/cwd resolves in more than one version home
@@ -553,8 +543,6 @@ export declare function listTeamsActive(opts?: {
553
543
  }): Promise<ActiveSession[]>;
554
544
  /** Live editor-terminal agents across every IDE window. */
555
545
  export declare function listTerminalsActive(): Promise<ActiveSession[]>;
556
- /** Cloud tasks still in a non-terminal state. `tasks.db` may not exist; that's fine. */
557
- export declare function listCloudActive(): ActiveSession[];
558
546
  interface ProcRow {
559
547
  pid: number;
560
548
  ppid: number;
@@ -628,7 +616,7 @@ export declare function foldSubordinateAgents(candidates: AgentCandidate[], ppid
628
616
  */
629
617
  export declare function listUnattributedActive(attributed: Set<number>): Promise<ActiveSession[]>;
630
618
  /** One tmux pane's resolved agent identity for the authoritative source. */
631
- export interface PaneIdentity {
619
+ interface PaneIdentity {
632
620
  agent: string;
633
621
  /** Exact session id when resolvable (launch registry, or the hook join). */
634
622
  sessionId?: string;
@@ -679,7 +667,7 @@ export declare function listTmuxAgentSessions(): Promise<ActiveSession[]>;
679
667
  */
680
668
  export declare function getActiveSessions(opts?: ActiveQueryOptions): Promise<ActiveSession[]>;
681
669
  /** Local discovery sources probed by {@link describeActiveDiscoveryHealth}. */
682
- export interface ActiveDiscoveryHealth {
670
+ interface ActiveDiscoveryHealth {
683
671
  /** Sources that failed on the probe (currently only `'tmux'`). Empty = nothing degraded locally. */
684
672
  degradedSources: string[];
685
673
  }
@@ -262,7 +262,7 @@ export class TmuxDiscoveryDegradedError extends Error {
262
262
  * be idle for longer than 2 minutes, but it must not attach to a weeks-old
263
263
  * transcript just because a GUI app service with the same basename is alive.
264
264
  */
265
- export const ACTIVE_SESSION_STALE_MS = 24 * 60 * 60_000;
265
+ const ACTIVE_SESSION_STALE_MS = 24 * 60 * 60_000;
266
266
  /**
267
267
  * A session whose transcript hasn't been written in this long is ABANDONED /
268
268
  * dangling — the framework can no longer treat it as live work, whether its PID
@@ -541,10 +541,6 @@ function claudeProjectDirName(cwd) {
541
541
  */
542
542
  const CLAUDE_SESSION_FILE_CACHE_MAX = 256;
543
543
  const claudeSessionFileCache = new Map();
544
- /** Test seam: drop the Claude session-file path memo. */
545
- export function clearClaudeSessionFileCacheForTest() {
546
- claudeSessionFileCache.clear();
547
- }
548
544
  /**
549
545
  * Locate the active Claude session file for a process. If we know the session
550
546
  * UUID (from terminal env or team parent), prefer the exact match. Otherwise
@@ -1094,7 +1090,7 @@ export async function listTerminalsActive() {
1094
1090
  });
1095
1091
  }
1096
1092
  /** Cloud tasks still in a non-terminal state. `tasks.db` may not exist; that's fine. */
1097
- export function listCloudActive() {
1093
+ function listCloudActive() {
1098
1094
  let tasks;
1099
1095
  try {
1100
1096
  tasks = listActiveTasks();
@@ -13,7 +13,7 @@ import * as fs from 'fs';
13
13
  import * as path from 'path';
14
14
  import * as os from 'os';
15
15
  import * as yaml from 'yaml';
16
- import { deriveShortId } from './short-id.js';
16
+ import { deriveShortId } from '../text/short-id.js';
17
17
  import { getCacheDir } from '../state.js';
18
18
  const PROXY_BASE = process.env.RUSH_PROXY_BASE ?? 'https://api.prix.dev';
19
19
  const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');