@phnx-labs/agents-cli 1.22.42 → 1.22.43

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 (122) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +1 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/bootstrap.js +25 -7
  5. package/dist/cli/command-registry.d.ts +0 -3
  6. package/dist/cli/command-registry.js +0 -8
  7. package/dist/commands/alias.d.ts +7 -4
  8. package/dist/commands/alias.js +33 -8
  9. package/dist/commands/auth.js +7 -3
  10. package/dist/commands/computer.js +20 -6
  11. package/dist/commands/daemon.js +1 -1
  12. package/dist/commands/doctor.d.ts +4 -2
  13. package/dist/commands/doctor.js +20 -5
  14. package/dist/commands/exec.js +3 -2
  15. package/dist/commands/feed.js +1 -30
  16. package/dist/commands/harness.js +3 -3
  17. package/dist/commands/monitors.js +1 -1
  18. package/dist/commands/org.js +9 -8
  19. package/dist/commands/profiles.js +2 -2
  20. package/dist/commands/routines.js +43 -25
  21. package/dist/commands/sessions-picker.js +124 -122
  22. package/dist/commands/setup.js +5 -1
  23. package/dist/commands/snapshot.d.ts +1 -1
  24. package/dist/commands/snapshot.js +4 -4
  25. package/dist/commands/status.d.ts +4 -2
  26. package/dist/commands/status.js +10 -7
  27. package/dist/commands/sync.d.ts +1 -0
  28. package/dist/commands/sync.js +29 -2
  29. package/dist/commands/view.js +6 -0
  30. package/dist/commands/webhook.js +1 -1
  31. package/dist/lib/accounting/rotate.d.ts +5 -3
  32. package/dist/lib/accounting/rotate.js +36 -7
  33. package/dist/lib/accounting/usage.d.ts +18 -4
  34. package/dist/lib/accounting/usage.js +138 -27
  35. package/dist/lib/agent-spec/agents.d.ts +7 -7
  36. package/dist/lib/agent-spec/agents.js +15 -15
  37. package/dist/lib/cloud/rush.d.ts +0 -15
  38. package/dist/lib/cloud/rush.js +0 -35
  39. package/dist/lib/crabbox/runtimes.d.ts +6 -2
  40. package/dist/lib/crabbox/runtimes.js +21 -5
  41. package/dist/lib/daemon/daemon.js +86 -58
  42. package/dist/lib/daemon/runner.d.ts +18 -1
  43. package/dist/lib/daemon/runner.js +105 -11
  44. package/dist/lib/devices/doctor-findings.js +1 -1
  45. package/dist/lib/devices/health-report.js +1 -1
  46. package/dist/lib/drift-sync.d.ts +1 -1
  47. package/dist/lib/drift-sync.js +2 -2
  48. package/dist/lib/drift.d.ts +1 -1
  49. package/dist/lib/exec.js +13 -3
  50. package/dist/lib/format.d.ts +9 -0
  51. package/dist/lib/format.js +16 -0
  52. package/dist/lib/github/pr-mergeable.d.ts +49 -0
  53. package/dist/lib/github/pr-mergeable.js +157 -0
  54. package/dist/lib/github/pr-verdict.d.ts +67 -0
  55. package/dist/lib/github/pr-verdict.js +72 -0
  56. package/dist/lib/harness/adapter.d.ts +6 -5
  57. package/dist/lib/hosts/passthrough.d.ts +10 -0
  58. package/dist/lib/hosts/passthrough.js +28 -6
  59. package/dist/lib/hosts/session-index.js +1 -1
  60. package/dist/lib/installations/shims.js +1 -1
  61. package/dist/lib/installations/versions.d.ts +40 -0
  62. package/dist/lib/installations/versions.js +57 -2
  63. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  64. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  65. package/dist/lib/menubar/install-menubar.js +19 -8
  66. package/dist/lib/monitors/config.d.ts +2 -2
  67. package/dist/lib/monitors/config.js +3 -2
  68. package/dist/lib/origin-machine.d.ts +18 -0
  69. package/dist/lib/origin-machine.js +34 -0
  70. package/dist/lib/perf/db.js +1 -1
  71. package/dist/lib/perf/spool.js +1 -1
  72. package/dist/lib/profiles.d.ts +7 -0
  73. package/dist/lib/profiles.js +26 -1
  74. package/dist/lib/sandbox.d.ts +24 -0
  75. package/dist/lib/sandbox.js +101 -0
  76. package/dist/lib/scheduling/routines.d.ts +18 -9
  77. package/dist/lib/scheduling/routines.js +16 -8
  78. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  79. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  80. package/dist/lib/secrets/agent.js +9 -1
  81. package/dist/lib/secrets/index.js +1 -1
  82. package/dist/lib/secrets/list-filter.js +1 -1
  83. package/dist/lib/service-manifest.d.ts +20 -0
  84. package/dist/lib/service-manifest.js +34 -0
  85. package/dist/lib/session/cloud.js +1 -1
  86. package/dist/lib/session/db.js +1 -1
  87. package/dist/lib/session/fork.js +1 -1
  88. package/dist/lib/session/live-metadata.js +1 -1
  89. package/dist/lib/session/origin-machine.d.ts +1 -18
  90. package/dist/lib/session/origin-machine.js +4 -34
  91. package/dist/lib/session/relative-time.d.ts +1 -44
  92. package/dist/lib/session/relative-time.js +4 -120
  93. package/dist/lib/session/remote-list.js +1 -1
  94. package/dist/lib/session/remote.js +2 -2
  95. package/dist/lib/session/short-id.d.ts +1 -17
  96. package/dist/lib/session/short-id.js +4 -20
  97. package/dist/lib/session/width.d.ts +1 -29
  98. package/dist/lib/session/width.js +4 -101
  99. package/dist/lib/snapshot.d.ts +2 -2
  100. package/dist/lib/snapshot.js +1 -1
  101. package/dist/lib/startup/command-registry.d.ts +4 -1
  102. package/dist/lib/startup/command-registry.js +11 -4
  103. package/dist/lib/teams/placement-probe.js +1 -1
  104. package/dist/lib/teams/scheduler.js +1 -1
  105. package/dist/lib/text/relative-time.d.ts +44 -0
  106. package/dist/lib/text/relative-time.js +120 -0
  107. package/dist/lib/text/short-id.d.ts +17 -0
  108. package/dist/lib/text/short-id.js +20 -0
  109. package/dist/lib/text/width.d.ts +29 -0
  110. package/dist/lib/text/width.js +101 -0
  111. package/dist/lib/triggers/handlers.d.ts +7 -0
  112. package/dist/lib/triggers/handlers.js +8 -0
  113. package/dist/lib/triggers/webhook.d.ts +1 -0
  114. package/dist/lib/triggers/webhook.js +2 -1
  115. package/dist/lib/view-types.d.ts +4 -0
  116. package/package.json +1 -1
  117. package/dist/commands/tickets.d.ts +0 -2
  118. package/dist/commands/tickets.js +0 -43
  119. package/dist/lib/observe-aliases.d.ts +0 -30
  120. package/dist/lib/observe-aliases.js +0 -38
  121. package/dist/lib/tickets/list.d.ts +0 -53
  122. package/dist/lib/tickets/list.js +0 -153
@@ -25,21 +25,6 @@ export interface AccountManifest {
25
25
  fp: string;
26
26
  versions: AccountManifestEntry[];
27
27
  }
28
- /**
29
- * Read the raw OAuth credentials for one Claude version. On Mac, prefer the
30
- * Keychain blob (canonical). On Linux/CI, fall back to `.claude/.credentials.json`
31
- * inside the version home (where the Linux Claude CLI stores its OAuth).
32
- *
33
- * Returns null when no credentials are findable — caller treats as "version
34
- * is installed but not signed in" and skips it from the manifest.
35
- *
36
- * NOTE: the darwin branch returns the UNWRAPPED oauth sub-object (what the Rush
37
- * server re-wraps). It is NOT the shape Claude Code reads back from
38
- * `.credentials.json`. The lease exporter (crabbox/runtimes.ts) therefore only
39
- * reuses the Linux `.credentials.json` branch here and reads the wrapped raw
40
- * Keychain payload itself on darwin.
41
- */
42
- export declare function readClaudeCredentialsBlob(home: string): Promise<string | null>;
43
28
  /**
44
29
  * Build a manifest of the user's local Claude installations to send on every
45
30
  * cloud dispatch. The manifest is the contract the server uses to detect when
@@ -13,7 +13,6 @@ import { resolveDispatchRepos, normalizeProviderStatus, MAX_IMAGES_PER_DISPATCH
13
13
  import { parseSSE } from './stream.js';
14
14
  import { listInstalledVersions, getVersionHomePath } from '../installations/versions.js';
15
15
  import { getAccountInfo } from '../agents.js';
16
- import { loadClaudeOauth } from '../accounting/usage.js';
17
16
  import { selectBalancedVersion } from '../accounting/rotate.js';
18
17
  const PROXY_BASE = process.env.RUSH_PROXY_BASE ?? 'https://api.prix.dev';
19
18
  const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
@@ -112,40 +111,6 @@ function parsePromptCode(body) {
112
111
  return null;
113
112
  }
114
113
  }
115
- /**
116
- * Read the raw OAuth credentials for one Claude version. On Mac, prefer the
117
- * Keychain blob (canonical). On Linux/CI, fall back to `.claude/.credentials.json`
118
- * inside the version home (where the Linux Claude CLI stores its OAuth).
119
- *
120
- * Returns null when no credentials are findable — caller treats as "version
121
- * is installed but not signed in" and skips it from the manifest.
122
- *
123
- * NOTE: the darwin branch returns the UNWRAPPED oauth sub-object (what the Rush
124
- * server re-wraps). It is NOT the shape Claude Code reads back from
125
- * `.credentials.json`. The lease exporter (crabbox/runtimes.ts) therefore only
126
- * reuses the Linux `.credentials.json` branch here and reads the wrapped raw
127
- * Keychain payload itself on darwin.
128
- */
129
- export async function readClaudeCredentialsBlob(home) {
130
- if (process.platform === 'darwin') {
131
- const oauth = await loadClaudeOauth(home);
132
- if (oauth && oauth.accessToken) {
133
- return JSON.stringify(oauth);
134
- }
135
- }
136
- const credsPath = path.join(home, '.claude', '.credentials.json');
137
- try {
138
- if (fs.existsSync(credsPath)) {
139
- const raw = fs.readFileSync(credsPath, 'utf-8').trim();
140
- if (raw)
141
- return raw;
142
- }
143
- }
144
- catch {
145
- // fall through to null
146
- }
147
- return null;
148
- }
149
114
  /**
150
115
  * Build a manifest of the user's local Claude installations to send on every
151
116
  * cloud dispatch. The manifest is the contract the server uses to detect when
@@ -85,8 +85,12 @@ export declare const CLAUDE_TOKEN_REMOTE = ".claude/.credentials.json";
85
85
  * versioned home) uses a hash-suffixed service, so we try the bare service first,
86
86
  * then enumerate installed version homes (preferring the account whose email
87
87
  * matches `preferEmail`, so the token matches the `.claude.json` config we copy).
88
- * Off macOS the local Claude CLI stores the token in `.credentials.json` already —
89
- * reuse the rush.ts Linux branch verbatim.
88
+ * Off macOS the local Claude CLI stores the wrapped rotating blob in
89
+ * `.credentials.json` already. Read that file here with a shape check
90
+ * (`claudeAiOauth.accessToken`). Do not reintroduce a shared helper that
91
+ * also serves Rush Cloud dispatch — dispatch is email-only (SING-1b) and
92
+ * the old `readClaudeCredentialsBlob` path was the #1767 TTY-banner leak
93
+ * (RUSH-2359).
90
94
  *
91
95
  * The reader/service/version helpers are injected so unit tests never touch the
92
96
  * real Keychain.
@@ -19,7 +19,6 @@ import { getAccountInfo } from '../agents.js';
19
19
  import { getKeychainToken } from '../secrets/index.js';
20
20
  import { getClaudeKeychainService } from '../accounting/usage.js';
21
21
  import { listInstalledVersions, getVersionHomePath } from '../installations/versions.js';
22
- import { readClaudeCredentialsBlob } from '../cloud/rush.js';
23
22
  export const LEASE_RUNTIMES = [
24
23
  { id: 'claude', label: 'Claude Code', localCandidates: ['.claude/.claude.json', '.claude.json'], remote: '.claude.json' },
25
24
  { id: 'codex', label: 'Codex CLI', localCandidates: ['.codex/auth.json'], remote: '.codex/auth.json' },
@@ -193,8 +192,12 @@ function isClaudeCredentialsBlob(s) {
193
192
  * versioned home) uses a hash-suffixed service, so we try the bare service first,
194
193
  * then enumerate installed version homes (preferring the account whose email
195
194
  * matches `preferEmail`, so the token matches the `.claude.json` config we copy).
196
- * Off macOS the local Claude CLI stores the token in `.credentials.json` already —
197
- * reuse the rush.ts Linux branch verbatim.
195
+ * Off macOS the local Claude CLI stores the wrapped rotating blob in
196
+ * `.credentials.json` already. Read that file here with a shape check
197
+ * (`claudeAiOauth.accessToken`). Do not reintroduce a shared helper that
198
+ * also serves Rush Cloud dispatch — dispatch is email-only (SING-1b) and
199
+ * the old `readClaudeCredentialsBlob` path was the #1767 TTY-banner leak
200
+ * (RUSH-2359).
198
201
  *
199
202
  * The reader/service/version helpers are injected so unit tests never touch the
200
203
  * real Keychain.
@@ -248,9 +251,22 @@ export async function resolveClaudeCredentialsBlob(opts) {
248
251
  }
249
252
  return null;
250
253
  }
251
- // Off darwin: the local Claude CLI already stores the wrapped blob on disk.
254
+ // Off darwin: the local Claude CLI stores the wrapped rotating blob on disk.
255
+ // Read it directly so a file-based setup-token (Rush Cloud dispatch) is not
256
+ // mistaken for a native OAuth login that SING-1b must refuse to copy.
252
257
  const home = process.env.AGENTS_REAL_HOME || os.homedir();
253
- return readClaudeCredentialsBlob(home);
258
+ const credsPath = path.join(home, '.claude', '.credentials.json');
259
+ try {
260
+ if (fs.existsSync(credsPath)) {
261
+ const raw = fs.readFileSync(credsPath, 'utf-8').trim();
262
+ if (raw && isClaudeCredentialsBlob(raw))
263
+ return raw;
264
+ }
265
+ }
266
+ catch {
267
+ /* no readable credentials file */
268
+ }
269
+ return null;
254
270
  }
255
271
  /**
256
272
  * The credential-provisioning snippet for a `--lease` box.
@@ -11,7 +11,7 @@ import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import * as os from 'os';
13
13
  import { getDaemonDir } from '../state.js';
14
- import { isolatedHomeSuffix, namespacedServiceLabel, serviceManifestHomeEnv } from '../service-manifest.js';
14
+ import { isolatedHomeSuffix, namespacedServiceLabel, serviceManifestHomeEnv, serviceManagerRegistrationAllowed } from '../service-manifest.js';
15
15
  import { isAlive, killTree, backgroundSpawnOptions, waitForExit } from '../platform/index.js';
16
16
  import { listJobs as listAllJobs } from '../scheduling/routines.js';
17
17
  import { syncAllProjectRoutines } from '../routines-project.js';
@@ -1763,6 +1763,9 @@ export { getAgentsBinPath };
1763
1763
  * in fact up. Returns null when the service isn't running or the query fails.
1764
1764
  */
1765
1765
  function readServiceManagerPid(platform = os.platform()) {
1766
+ const reg = serviceManagerRegistrationAllowed();
1767
+ if (!reg.allowed)
1768
+ return null;
1766
1769
  try {
1767
1770
  if (platform === 'linux') {
1768
1771
  const out = execFileSync('systemctl', ['--user', 'show', '-p', 'MainPID', '--value', daemonSystemdUnitName()], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
@@ -1912,61 +1915,72 @@ function startDaemonLocked(agentsBin, releaseLock) {
1912
1915
  releaseLock();
1913
1916
  return startDetached({ agentsBin });
1914
1917
  };
1918
+ const reg = serviceManagerRegistrationAllowed();
1915
1919
  if (platform === 'darwin') {
1916
- try {
1917
- const plistPath = getLaunchdPlistPath();
1918
- const plistDir = path.dirname(plistPath);
1919
- if (!fs.existsSync(plistDir)) {
1920
- fs.mkdirSync(plistDir, { recursive: true });
1921
- }
1922
- // The plist carries no credential (RUSH-1759 — the daemon reads the OAuth
1923
- // token itself at startup); still create owner-only atomically to match the
1924
- // detached path and keep the log/PATH surface owner-private.
1925
- writeOwnerOnlyServiceManifest(plistPath, generateLaunchdPlist(agentsBin));
1920
+ if (reg.allowed) {
1926
1921
  try {
1927
- execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
1922
+ const plistPath = getLaunchdPlistPath();
1923
+ const plistDir = path.dirname(plistPath);
1924
+ if (!fs.existsSync(plistDir)) {
1925
+ fs.mkdirSync(plistDir, { recursive: true });
1926
+ }
1927
+ // The plist carries no credential (RUSH-1759 — the daemon reads the OAuth
1928
+ // token itself at startup); still create owner-only atomically to match the
1929
+ // detached path and keep the log/PATH surface owner-private.
1930
+ writeOwnerOnlyServiceManifest(plistPath, generateLaunchdPlist(agentsBin));
1931
+ try {
1932
+ execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
1933
+ }
1934
+ catch { /* not loaded, expected */ }
1935
+ // launchctl prints `Load failed:` and exits 0 when the label is in a
1936
+ // stuck state from a prior session — so a zero exit code isn't proof
1937
+ // of success. If no pid materializes within the window, give up on
1938
+ // launchd and fall through to a plain detached spawn.
1939
+ execFileSync('launchctl', ['load', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
1940
+ // Launch issued — the child needs this lock to claim (RUSH-2417).
1941
+ releaseLock();
1942
+ const pid = waitForPid(3000) ?? readServiceManagerPid();
1943
+ if (pid)
1944
+ return { pid, method: 'launchd' };
1945
+ // launchctl claimed success but nothing ran. Fall through.
1946
+ }
1947
+ catch {
1948
+ // load threw — fall through to detached spawn
1928
1949
  }
1929
- catch { /* not loaded, expected */ }
1930
- // launchctl prints `Load failed:` and exits 0 when the label is in a
1931
- // stuck state from a prior session — so a zero exit code isn't proof
1932
- // of success. If no pid materializes within the window, give up on
1933
- // launchd and fall through to a plain detached spawn.
1934
- execFileSync('launchctl', ['load', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
1935
- // Launch issued — the child needs this lock to claim (RUSH-2417).
1936
- releaseLock();
1937
- const pid = waitForPid(3000) ?? readServiceManagerPid();
1938
- if (pid)
1939
- return { pid, method: 'launchd' };
1940
- // launchctl claimed success but nothing ran. Fall through.
1941
1950
  }
1942
- catch {
1943
- // load threw — fall through to detached spawn
1951
+ else {
1952
+ process.stderr.write(`[agents] ${reg.reason}\n`);
1944
1953
  }
1945
1954
  return detachedFallback();
1946
1955
  }
1947
1956
  if (platform === 'linux') {
1948
- try {
1949
- const unitPath = getSystemdUnitPath();
1950
- const unitDir = path.dirname(unitPath);
1951
- if (!fs.existsSync(unitDir)) {
1952
- fs.mkdirSync(unitDir, { recursive: true });
1957
+ if (reg.allowed) {
1958
+ try {
1959
+ const unitPath = getSystemdUnitPath();
1960
+ const unitDir = path.dirname(unitPath);
1961
+ if (!fs.existsSync(unitDir)) {
1962
+ fs.mkdirSync(unitDir, { recursive: true });
1963
+ }
1964
+ // Carries no credential (RUSH-1759 — the daemon reads the OAuth token
1965
+ // itself at startup); owner-only to keep the PATH/log surface private.
1966
+ writeOwnerOnlyServiceManifest(unitPath, generateSystemdUnit(agentsBin));
1967
+ execFileSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf-8' });
1968
+ execFileSync('systemctl', ['--user', 'enable', daemonSystemdUnitName()], { encoding: 'utf-8' });
1969
+ execFileSync('systemctl', ['--user', 'start', daemonSystemdUnitName()], { encoding: 'utf-8' });
1970
+ // Launch issued — the child needs this lock to claim (RUSH-2417).
1971
+ releaseLock();
1972
+ const pid = waitForPid(3000) ?? readServiceManagerPid();
1973
+ if (pid)
1974
+ return { pid, method: 'systemd' };
1975
+ // systemctl returned success but no PID surfaced — fall through to a
1976
+ // plain detached spawn rather than reporting a null PID.
1977
+ }
1978
+ catch {
1979
+ // start threw — fall through to detached spawn
1953
1980
  }
1954
- // Carries no credential (RUSH-1759 — the daemon reads the OAuth token
1955
- // itself at startup); owner-only to keep the PATH/log surface private.
1956
- writeOwnerOnlyServiceManifest(unitPath, generateSystemdUnit(agentsBin));
1957
- execFileSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf-8' });
1958
- execFileSync('systemctl', ['--user', 'enable', daemonSystemdUnitName()], { encoding: 'utf-8' });
1959
- execFileSync('systemctl', ['--user', 'start', daemonSystemdUnitName()], { encoding: 'utf-8' });
1960
- // Launch issued — the child needs this lock to claim (RUSH-2417).
1961
- releaseLock();
1962
- const pid = waitForPid(3000) ?? readServiceManagerPid();
1963
- if (pid)
1964
- return { pid, method: 'systemd' };
1965
- // systemctl returned success but no PID surfaced — fall through to a
1966
- // plain detached spawn rather than reporting a null PID.
1967
1981
  }
1968
- catch {
1969
- // start threw — fall through to detached spawn
1982
+ else {
1983
+ process.stderr.write(`[agents] ${reg.reason}\n`);
1970
1984
  }
1971
1985
  return detachedFallback();
1972
1986
  }
@@ -2261,29 +2275,43 @@ export function stopDaemon() {
2261
2275
  const released = [];
2262
2276
  const surviving = [];
2263
2277
  let escalated = false;
2278
+ const reg = serviceManagerRegistrationAllowed();
2264
2279
  if (platform === 'darwin') {
2265
2280
  const plistPath = getLaunchdPlistPath();
2266
2281
  if (fs.existsSync(plistPath)) {
2282
+ if (reg.allowed) {
2283
+ try {
2284
+ execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8' });
2285
+ }
2286
+ catch (err) {
2287
+ if (process.env.AGENTS_DEBUG) {
2288
+ console.error(`[debug] launchctl unload failed: ${err.message}`);
2289
+ }
2290
+ }
2291
+ }
2292
+ else {
2293
+ process.stderr.write(`[agents] ${reg.reason}\n`);
2294
+ }
2267
2295
  try {
2268
- execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8' });
2269
2296
  fs.unlinkSync(plistPath);
2270
2297
  }
2298
+ catch { /* plist already removed */ }
2299
+ }
2300
+ }
2301
+ if (platform === 'linux') {
2302
+ if (reg.allowed) {
2303
+ try {
2304
+ execFileSync('systemctl', ['--user', 'stop', daemonSystemdUnitName()], { encoding: 'utf-8' });
2305
+ execFileSync('systemctl', ['--user', 'disable', daemonSystemdUnitName()], { encoding: 'utf-8' });
2306
+ }
2271
2307
  catch (err) {
2272
2308
  if (process.env.AGENTS_DEBUG) {
2273
- console.error(`[debug] launchctl unload failed: ${err.message}`);
2309
+ console.error(`[debug] systemctl stop failed: ${err.message}`);
2274
2310
  }
2275
2311
  }
2276
2312
  }
2277
- }
2278
- if (platform === 'linux') {
2279
- try {
2280
- execFileSync('systemctl', ['--user', 'stop', daemonSystemdUnitName()], { encoding: 'utf-8' });
2281
- execFileSync('systemctl', ['--user', 'disable', daemonSystemdUnitName()], { encoding: 'utf-8' });
2282
- }
2283
- catch (err) {
2284
- if (process.env.AGENTS_DEBUG) {
2285
- console.error(`[debug] systemctl stop failed: ${err.message}`);
2286
- }
2313
+ else {
2314
+ process.stderr.write(`[agents] ${reg.reason}\n`);
2287
2315
  }
2288
2316
  const unitPath = getSystemdUnitPath();
2289
2317
  if (fs.existsSync(unitPath)) {
@@ -74,6 +74,23 @@ interface RoutineAttempt {
74
74
  * but the gate should have paused such a routine before it ever spawned.
75
75
  */
76
76
  export declare function routineSpawnCwd(config: Pick<JobConfig, 'name' | 'project' | 'cwd' | 'agent' | 'workflow' | 'command'>): string;
77
+ /**
78
+ * Bake the daemon-job argv skeleton from AGENT_COMMANDS.
79
+ *
80
+ * The two tables used to be independent copies of the same launch decision.
81
+ * Argv now comes from AGENT_COMMANDS.base / promptFlag / jsonFlags / modeFlags.
82
+ * Two documented exceptions, not a second table:
83
+ * - kimi uses `--prompt` (long form). `-p` is the exec alias; combining
84
+ * either with --plan/--auto/--yolo aborts, so routineModeArgs emits no
85
+ * mode flag. The daemon has always spawned the long form.
86
+ * - claude places `--verbose` (from jsonFlags) before the prompt and bakes
87
+ * modeFlags.plan so claudeAdapter.routineModeArgs can splice
88
+ * plan → acceptEdits / auto / skip.
89
+ *
90
+ * Returns undefined when `agent` is outside ROUTINE_AGENT_IDS (gemini, grok,
91
+ * and every other harness the daemon does not fire locally).
92
+ */
93
+ export declare function bakeRoutineArgv(agent: string): string[] | undefined;
77
94
  /** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
78
95
  export declare function buildJobCommand(config: JobConfig, resolvedPrompt: string, forwardAccount?: boolean): string[];
79
96
  /**
@@ -161,7 +178,7 @@ export declare function buildHostDispatchOptions(config: JobConfig, ctx: {
161
178
  runDir: string;
162
179
  detached: boolean;
163
180
  }): import('../hosts/run-target.js').HostPromptRun;
164
- export declare function dispatchesViaAgentsRun(config: Pick<JobConfig, 'workflow' | 'resume'>): boolean;
181
+ export declare function dispatchesViaAgentsRun(config: Pick<JobConfig, 'workflow' | 'resume' | 'agent'>): boolean;
165
182
  /**
166
183
  * Merge sandbox/base env with the canonical per-version exec env
167
184
  * (CLAUDE_CONFIG_DIR / CODEX_HOME / …) so routines share account isolation
@@ -21,7 +21,7 @@ import { getCliLaunch, getAgentsBinDir } from '../cli-entry.js';
21
21
  import { resolveJobPrompt, parseTimeout, writeRunMeta, listRuns, getJobRunsDir, getRunDir, checkJobDeviceEligibility, finalizeRunMeta, resolveJobExecutionContext, slotRunId, claimRunSlot, readRunMeta, resolveHostStrategy, } from '../scheduling/routines.js';
22
22
  import { getRunsDir, getUserAgentsDir, readMeta, getDaemonDir } from '../state.js';
23
23
  import { shortCodexHome } from '../codex-home.js';
24
- import { prepareJobHome, buildSpawnEnv, getJobHomePath } from '../sandbox.js';
24
+ import { prepareJobHome, buildSpawnEnv, getJobHomePath, assertSandboxForwardsHostGhAuth } from '../sandbox.js';
25
25
  import { resolveModel, buildReasoningFlags } from '../models.js';
26
26
  import { createTimer, redactPrompt, emitRoutineEnd } from '../feed/events.js';
27
27
  import { resolveHarnessAdapter } from '../harness/index.js';
@@ -39,7 +39,8 @@ import { resolveClaudeSetupToken } from '../claude-account-token.js';
39
39
  import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, formatNoHealthyAccountError, } from '../accounting/rotate.js';
40
40
  import { readAuthHealth, isDeadVerdict } from '../auth-health.js';
41
41
  import { machineId } from '../machine-id.js';
42
- import { isSelfUpdatingAgent, ROUTINE_AGENT_COMMANDS, isAgentHardDeprecated, hardDeprecationError } from '../agents.js';
42
+ import { isSelfUpdatingAgent, ROUTINE_AGENT_IDS, isAgentHardDeprecated, hardDeprecationError } from '../agents.js';
43
+ import { isCustomHarnessName, readProfile } from '../profiles.js';
43
44
  export class RoutineAlreadyRunningError extends Error {
44
45
  constructor(jobName, runId) {
45
46
  super(`Routine '${jobName}' already has a running execution (${runId})`);
@@ -308,7 +309,7 @@ function allocateRoutineAttempt(config, trigger) {
308
309
  // harness leaves the command table (gemini did), and the legacy routine must
309
310
  // still land a visible 'blocked' record, never a generic buildJobCommand
310
311
  // failure (RUSH-2202).
311
- if (!config.workflow && config.agent && isAgentHardDeprecated(config.agent)) {
312
+ if (!config.workflow && config.agent && !isCustomHarnessName(config.agent) && isAgentHardDeprecated(config.agent)) {
312
313
  const reason = hardDeprecationError(config.agent);
313
314
  return {
314
315
  proceed: false,
@@ -488,6 +489,48 @@ export function routineSpawnCwd(config) {
488
489
  const ctx = resolveJobExecutionContext(config, { mode: 'local' });
489
490
  return ctx.absoluteCwd ?? os.homedir();
490
491
  }
492
+ /**
493
+ * Bake the daemon-job argv skeleton from AGENT_COMMANDS.
494
+ *
495
+ * The two tables used to be independent copies of the same launch decision.
496
+ * Argv now comes from AGENT_COMMANDS.base / promptFlag / jsonFlags / modeFlags.
497
+ * Two documented exceptions, not a second table:
498
+ * - kimi uses `--prompt` (long form). `-p` is the exec alias; combining
499
+ * either with --plan/--auto/--yolo aborts, so routineModeArgs emits no
500
+ * mode flag. The daemon has always spawned the long form.
501
+ * - claude places `--verbose` (from jsonFlags) before the prompt and bakes
502
+ * modeFlags.plan so claudeAdapter.routineModeArgs can splice
503
+ * plan → acceptEdits / auto / skip.
504
+ *
505
+ * Returns undefined when `agent` is outside ROUTINE_AGENT_IDS (gemini, grok,
506
+ * and every other harness the daemon does not fire locally).
507
+ */
508
+ export function bakeRoutineArgv(agent) {
509
+ if (!ROUTINE_AGENT_IDS.includes(agent))
510
+ return undefined;
511
+ const template = AGENT_COMMANDS[agent];
512
+ if (!template)
513
+ return undefined;
514
+ const json = template.jsonFlags ?? [];
515
+ const cmd = [...template.base];
516
+ if (agent === 'kimi') {
517
+ cmd.push('--prompt', '{prompt}', ...json);
518
+ return cmd;
519
+ }
520
+ if (agent === 'claude') {
521
+ const verbose = json.includes('--verbose') ? ['--verbose'] : [];
522
+ const jsonRest = json.filter((flag) => flag !== '--verbose');
523
+ cmd.push(template.promptFlag, ...verbose, '{prompt}', ...jsonRest, ...(template.modeFlags.plan ?? []));
524
+ return cmd;
525
+ }
526
+ if (template.promptFlag === 'positional') {
527
+ cmd.push('{prompt}', ...json);
528
+ }
529
+ else {
530
+ cmd.push(template.promptFlag, '{prompt}', ...json);
531
+ }
532
+ return cmd;
533
+ }
491
534
  /** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
492
535
  export function buildJobCommand(config, resolvedPrompt, forwardAccount = true) {
493
536
  // Workflow branch: delegate to `agents run <workflow>` which handles subagent
@@ -514,9 +557,25 @@ export function buildJobCommand(config, resolvedPrompt, forwardAccount = true) {
514
557
  cmd.push('--account', config.account);
515
558
  return cmd;
516
559
  }
517
- const template = ROUTINE_AGENT_COMMANDS[agent];
560
+ // Custom-harness branch: delegate to `agents run <name>`, which owns profile
561
+ // resolution (host binary, model env, provider auth) — the same delegation
562
+ // the workflow and resume branches use. The profile pins its own version and
563
+ // auth, so no command template, binary pinning, or account-env injection
564
+ // applies here.
565
+ if (isCustomHarnessName(agent)) {
566
+ const cmd = ['agents', 'run', agent, resolvedPrompt, '--mode', config.mode];
567
+ if (config.account && forwardAccount)
568
+ cmd.push('--account', config.account);
569
+ return cmd;
570
+ }
571
+ const template = bakeRoutineArgv(agent);
518
572
  if (!template) {
519
- throw new Error(`Unsupported agent for daemon jobs: ${agent}`);
573
+ // A name outside both tables was either never valid or was a custom
574
+ // harness whose profile has since been deleted — validateJob accepted it
575
+ // against a profile that existed then. Name the repair, not just the miss.
576
+ throw new Error(`Unsupported agent for daemon jobs: ${agent}. ` +
577
+ `If '${agent}' was a custom harness, its profile no longer exists on this device — ` +
578
+ `recreate it (agents harness add ${agent} ...) or point the routine at another agent.`);
520
579
  }
521
580
  let cmd = template.map((part) => part.replace('{prompt}', resolvedPrompt));
522
581
  // Canonicalize mode (accepts legacy `full` as alias for `skip`).
@@ -554,7 +613,8 @@ export function buildJobCommand(config, resolvedPrompt, forwardAccount = true) {
554
613
  * Reasoning level (config.config.reasoning) maps to per-agent flags via models.ts.
555
614
  */
556
615
  function appendModelAndReasoning(cmd, config) {
557
- // Only called from buildJobCommand's agent path config.agent is set.
616
+ // Only called from buildJobCommand's agent path AFTER the custom-harness
617
+ // branch returned — config.agent is a native id here.
558
618
  const agent = config.agent;
559
619
  const model = config.config?.model;
560
620
  if (model) {
@@ -636,6 +696,8 @@ function transcriptBasePath(runDir) {
636
696
  export function snapshotRoutineTranscriptBase(meta, runDir, overlayHome) {
637
697
  if (!meta.agent)
638
698
  return;
699
+ // A host/cloud custom-harness run records the harness name, which has no
700
+ // transcript spec — the null-specs guard below skips it, as before.
639
701
  const specs = ROUTINE_TRANSCRIPT_SPECS[meta.agent];
640
702
  if (!specs)
641
703
  return;
@@ -674,6 +736,8 @@ function readTranscriptBase(runDir) {
674
736
  export function archiveRoutineTranscripts(meta, runDir, overlayHome) {
675
737
  if (!meta.agent)
676
738
  return;
739
+ // A host/cloud custom-harness run records the harness name, which has no
740
+ // transcript spec — the null-specs guard below skips it, as before.
677
741
  const specs = ROUTINE_TRANSCRIPT_SPECS[meta.agent];
678
742
  if (!specs)
679
743
  return;
@@ -808,6 +872,13 @@ export async function resolveRoutineLaunch(config, cwd = process.cwd(), deps = {
808
872
  if (config.workflow) {
809
873
  return { chain: [], rotation: null, pinned: false };
810
874
  }
875
+ if (config.agent && isCustomHarnessName(config.agent)) {
876
+ // A custom harness pins its own version/auth in the profile, so there is
877
+ // no version/account chain to resolve here — `agents run <name>` owns it
878
+ // (matching exec's "strategy ignored: custom harness pins its own
879
+ // version/auth").
880
+ return { chain: [], rotation: null, pinned: false };
881
+ }
811
882
  // resolveRoutineLaunch is only called for agent jobs (workflow returns above;
812
883
  // command jobs branch out of execute*Job before reaching this).
813
884
  const agent = config.agent;
@@ -1012,7 +1083,7 @@ export function buildHostDispatchOptions(config, ctx) {
1012
1083
  };
1013
1084
  }
1014
1085
  export function dispatchesViaAgentsRun(config) {
1015
- return Boolean(config.workflow || config.resume);
1086
+ return Boolean(config.workflow || config.resume || (config.agent && isCustomHarnessName(config.agent)));
1016
1087
  }
1017
1088
  /**
1018
1089
  * Merge sandbox/base env with the canonical per-version exec env
@@ -1214,7 +1285,11 @@ async function executeJobPlaced(config, deps, attempt) {
1214
1285
  // the agent's config dir, and the sandbox overlay home has only a freshly-generated
1215
1286
  // config with no session store. So a resume job is never sandboxed, regardless of
1216
1287
  // `config.sandbox` (see the resume branch in buildJobCommand).
1217
- const useSandbox = config.sandbox !== false && !config.resume;
1288
+ // Resume needs the REAL home (session store); a custom harness needs it too —
1289
+ // the delegated `agents run <name>` resolves ~/.agents (profiles, setup
1290
+ // sentinel, version homes) from HOME, which the overlay would hide. Exec
1291
+ // still isolates the run in the host version home it swaps HOME into.
1292
+ const useSandbox = config.sandbox !== false && !config.resume && !(config.agent && isCustomHarnessName(config.agent));
1218
1293
  const overlayHome = useSandbox ? prepareJobHome(config) : undefined;
1219
1294
  const runId = attempt.runId;
1220
1295
  const runDir = getRunDir(config.name, runId);
@@ -1225,7 +1300,11 @@ async function executeJobPlaced(config, deps, attempt) {
1225
1300
  // Workflows run via `agents run <workflow>` which delegates to claude under the hood.
1226
1301
  // Use 'claude' as the effective agent for report extraction and metadata when workflow is set.
1227
1302
  // (command jobs branched out earlier, so config.agent is set on the non-workflow path.)
1228
- const effectiveAgent = config.workflow ? 'claude' : config.agent;
1303
+ const effectiveAgent = config.workflow
1304
+ ? 'claude'
1305
+ : isCustomHarnessName(config.agent)
1306
+ ? readProfile(config.agent).host.agent
1307
+ : config.agent;
1229
1308
  if (!dispatchesViaAgentsRun(config)) {
1230
1309
  const { findUnifiedAccount, resolveAccountSelection, resolveCredentialAccount } = await import('../account-registry.js');
1231
1310
  const meta = readMeta();
@@ -1238,6 +1317,9 @@ async function executeJobPlaced(config, deps, attempt) {
1238
1317
  Object.assign(baseEnv, resolveCredentialAccount(selectedAccount, effectiveAgent).env);
1239
1318
  }
1240
1319
  }
1320
+ // RUSH-2860: if this host holds gh auth, the sandbox child MUST see it.
1321
+ if (useSandbox)
1322
+ assertSandboxForwardsHostGhAuth(baseEnv);
1241
1323
  const meta = {
1242
1324
  jobName: config.name,
1243
1325
  runId,
@@ -1728,7 +1810,11 @@ async function executeJobDetachedClaimed(config, attempt, hooks) {
1728
1810
  // the agent's config dir, and the sandbox overlay home has only a freshly-generated
1729
1811
  // config with no session store. So a resume job is never sandboxed, regardless of
1730
1812
  // `config.sandbox` (see the resume branch in buildJobCommand).
1731
- const useSandbox = config.sandbox !== false && !config.resume;
1813
+ // Resume needs the REAL home (session store); a custom harness needs it too —
1814
+ // the delegated `agents run <name>` resolves ~/.agents (profiles, setup
1815
+ // sentinel, version homes) from HOME, which the overlay would hide. Exec
1816
+ // still isolates the run in the host version home it swaps HOME into.
1817
+ const useSandbox = config.sandbox !== false && !config.resume && !(config.agent && isCustomHarnessName(config.agent));
1732
1818
  const overlayHome = useSandbox ? prepareJobHome(config) : undefined;
1733
1819
  const runId = attempt.runId;
1734
1820
  const runDir = getRunDir(config.name, runId);
@@ -1755,7 +1841,15 @@ async function executeJobDetachedClaimed(config, attempt, hooks) {
1755
1841
  })()
1756
1842
  // Non-command path only: config.agent is always set here (command/workflow branch earlier).
1757
1843
  : buildRoutineSpawnEnv(baseEnv, config.agent, version, config.timezone, overlayHome);
1758
- const effectiveAgent = config.workflow ? 'claude' : config.agent;
1844
+ // RUSH-2860: if this host holds gh auth, the sandbox child MUST see it —
1845
+ // otherwise monitors runs records ok while every gh call inside fails.
1846
+ if (useSandbox)
1847
+ assertSandboxForwardsHostGhAuth(spawnEnv);
1848
+ const effectiveAgent = config.workflow
1849
+ ? 'claude'
1850
+ : isCustomHarnessName(config.agent)
1851
+ ? readProfile(config.agent).host.agent
1852
+ : config.agent;
1759
1853
  const meta = {
1760
1854
  jobName: config.name,
1761
1855
  runId,
@@ -42,7 +42,7 @@ import { AGENTS, ALL_AGENT_IDS, supportsAccountInspection } from '../agents.js';
42
42
  import { blocksLocalScripts } from '../platform/winpath.js';
43
43
  import { loginHint } from '../signin-badge.js';
44
44
  import { CONFIG_ENV_ISOLATED_AGENTS } from '../installations/shims.js';
45
- import { padToWidth, stringWidth } from '../session/width.js';
45
+ import { padToWidth, stringWidth } from '../text/width.js';
46
46
  import { windowsSshEnrollmentProblem } from './windows-ssh-enrollment.js';
47
47
  const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
48
48
  /** Agents with NO per-version credential isolation: their login is shared across
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { padToWidth, stringWidth, terminalWidth, truncateToWidth } from '../session/width.js';
2
+ import { padToWidth, stringWidth, terminalWidth, truncateToWidth } from '../text/width.js';
3
3
  import { fmtBytes, headroom } from './health.js';
4
4
  import { formatCheckedAge } from '../auth-health.js';
5
5
  import { compareFleetInventories } from './fleet-divergence.js';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Interactive drift-sync flow — the single "we detected drift, want to fix it?"
3
- * action, shared by `agents status`, `agents doctor`, and the menu-bar "NEEDS
3
+ * action, shared by `agents sync status`, `agents doctor`, and the menu-bar "NEEDS
4
4
  * SYNC" row.
5
5
  *
6
6
  * It composes existing pieces, re-implementing nothing:
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Interactive drift-sync flow — the single "we detected drift, want to fix it?"
3
- * action, shared by `agents status`, `agents doctor`, and the menu-bar "NEEDS
3
+ * action, shared by `agents sync status`, `agents doctor`, and the menu-bar "NEEDS
4
4
  * SYNC" row.
5
5
  *
6
6
  * It composes existing pieces, re-implementing nothing:
@@ -115,7 +115,7 @@ export async function promptDriftSync(opts = {}) {
115
115
  if (opts.yes || !isInteractiveTerminal()) {
116
116
  if (!opts.yes) {
117
117
  // Non-TTY without --yes: report, don't act, don't throw.
118
- console.log(chalk.gray('\nRun `agents status --yes` to sync, or `agents status` in a terminal to choose.'));
118
+ console.log(chalk.gray('\nRun `agents sync status --yes` to sync, or `agents sync status` in a terminal to choose.'));
119
119
  return base;
120
120
  }
121
121
  const systemPulled = await pullSystem(status);
@@ -62,7 +62,7 @@ export interface DriftSummary {
62
62
  * True when the install is out of sync: any installed version is stale,
63
63
  * never-synced, carries unwired hooks, or has a broken generated hook runtime,
64
64
  * OR a source layer is behind origin.
65
- * `agents doctor` surfaces it as "run `agents status`"; `agents doctor --check`
65
+ * `agents doctor` surfaces it as "run `agents sync status`"; `agents doctor --check`
66
66
  * maps it to a non-zero exit. Orphans are a `prune` concern, not sync drift, so they do
67
67
  * NOT set this flag (mirrors the sync-status engine: an orphan alone never
68
68
  * flags needsSync).