@phnx-labs/agents-cli 1.20.78 → 1.20.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/CHANGELOG.md +595 -0
  2. package/README.md +26 -26
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +9 -0
  5. package/dist/commands/activity.js +153 -24
  6. package/dist/commands/apply.js +11 -4
  7. package/dist/commands/cli.js +1 -1
  8. package/dist/commands/commands.d.ts +1 -1
  9. package/dist/commands/commands.js +7 -6
  10. package/dist/commands/defaults.js +3 -11
  11. package/dist/commands/doctor.d.ts +66 -0
  12. package/dist/commands/doctor.js +596 -33
  13. package/dist/commands/events.js +1 -1
  14. package/dist/commands/exec.js +24 -5
  15. package/dist/commands/feed.d.ts +4 -0
  16. package/dist/commands/feed.js +87 -11
  17. package/dist/commands/harness.js +2 -2
  18. package/dist/commands/hooks.js +1 -1
  19. package/dist/commands/import.js +8 -4
  20. package/dist/commands/logs.js +6 -0
  21. package/dist/commands/menubar.js +12 -0
  22. package/dist/commands/models.js +1 -1
  23. package/dist/commands/packages.js +6 -6
  24. package/dist/commands/permissions.js +1 -1
  25. package/dist/commands/profiles.js +1 -1
  26. package/dist/commands/pull.d.ts +1 -1
  27. package/dist/commands/pull.js +4 -4
  28. package/dist/commands/repo.d.ts +78 -0
  29. package/dist/commands/repo.js +187 -11
  30. package/dist/commands/routines.d.ts +8 -1
  31. package/dist/commands/routines.js +241 -77
  32. package/dist/commands/rules.js +13 -12
  33. package/dist/commands/secrets.d.ts +3 -1
  34. package/dist/commands/secrets.js +100 -32
  35. package/dist/commands/sessions-inject.d.ts +7 -4
  36. package/dist/commands/sessions-inject.js +18 -18
  37. package/dist/commands/sessions-picker.js +32 -3
  38. package/dist/commands/sessions-resume.d.ts +16 -0
  39. package/dist/commands/sessions-resume.js +14 -13
  40. package/dist/commands/sessions.d.ts +115 -0
  41. package/dist/commands/sessions.js +189 -35
  42. package/dist/commands/set.d.ts +15 -0
  43. package/dist/commands/set.js +79 -0
  44. package/dist/commands/setup-fleet.d.ts +21 -0
  45. package/dist/commands/setup-fleet.js +201 -0
  46. package/dist/commands/setup-secrets.d.ts +20 -0
  47. package/dist/commands/setup-secrets.js +223 -0
  48. package/dist/commands/setup.js +17 -3
  49. package/dist/commands/skills.js +1 -1
  50. package/dist/commands/sync.js +7 -2
  51. package/dist/commands/teams-picker.js +0 -1
  52. package/dist/commands/teams.d.ts +33 -1
  53. package/dist/commands/teams.js +208 -44
  54. package/dist/commands/versions.js +10 -5
  55. package/dist/commands/view.d.ts +1 -0
  56. package/dist/commands/view.js +11 -0
  57. package/dist/index.js +52 -8
  58. package/dist/lib/acp/harnesses.js +0 -7
  59. package/dist/lib/activity.d.ts +156 -0
  60. package/dist/lib/activity.js +282 -0
  61. package/dist/lib/agents.d.ts +9 -2
  62. package/dist/lib/agents.js +72 -63
  63. package/dist/lib/browser/service.js +3 -0
  64. package/dist/lib/browser/types.d.ts +7 -0
  65. package/dist/lib/capabilities.js +6 -2
  66. package/dist/lib/crabbox/lease.js +2 -2
  67. package/dist/lib/crabbox/setup-copy.d.ts +4 -4
  68. package/dist/lib/crabbox/setup-copy.js +4 -4
  69. package/dist/lib/daemon.d.ts +18 -0
  70. package/dist/lib/daemon.js +47 -6
  71. package/dist/lib/devices/fleet.d.ts +1 -1
  72. package/dist/lib/devices/fleet.js +1 -1
  73. package/dist/lib/doctor-diff.d.ts +20 -0
  74. package/dist/lib/doctor-diff.js +6 -1
  75. package/dist/lib/drift.d.ts +31 -11
  76. package/dist/lib/drift.js +58 -7
  77. package/dist/lib/events.d.ts +8 -1
  78. package/dist/lib/events.js +10 -1
  79. package/dist/lib/exec.d.ts +7 -0
  80. package/dist/lib/exec.js +69 -11
  81. package/dist/lib/feed-post.d.ts +28 -1
  82. package/dist/lib/feed-post.js +110 -2
  83. package/dist/lib/fleet/apply.js +8 -1
  84. package/dist/lib/fleet/auth-sync.d.ts +18 -1
  85. package/dist/lib/fleet/auth-sync.js +25 -11
  86. package/dist/lib/fleet/remote-login.js +5 -0
  87. package/dist/lib/git.d.ts +14 -14
  88. package/dist/lib/git.js +41 -44
  89. package/dist/lib/hooks.d.ts +71 -0
  90. package/dist/lib/hooks.js +245 -95
  91. package/dist/lib/hosts/dispatch.d.ts +28 -1
  92. package/dist/lib/hosts/dispatch.js +33 -5
  93. package/dist/lib/hosts/option.js +2 -2
  94. package/dist/lib/hosts/passthrough.d.ts +22 -1
  95. package/dist/lib/hosts/passthrough.js +238 -13
  96. package/dist/lib/hosts/run-target.d.ts +2 -0
  97. package/dist/lib/hosts/run-target.js +1 -0
  98. package/dist/lib/hq/floor.d.ts +1 -1
  99. package/dist/lib/hq/floor.js +6 -1
  100. package/dist/lib/mcp.js +3 -71
  101. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  102. package/dist/lib/menubar/install-menubar.d.ts +24 -0
  103. package/dist/lib/menubar/install-menubar.js +52 -2
  104. package/dist/lib/menubar/notify-desktop.d.ts +15 -0
  105. package/dist/lib/menubar/notify-desktop.js +41 -5
  106. package/dist/lib/merged-resources.d.ts +11 -0
  107. package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
  108. package/dist/lib/permissions.d.ts +0 -32
  109. package/dist/lib/permissions.js +4 -157
  110. package/dist/lib/picker.js +1 -1
  111. package/dist/lib/platform/process.d.ts +16 -5
  112. package/dist/lib/platform/process.js +54 -0
  113. package/dist/lib/plugins.d.ts +0 -8
  114. package/dist/lib/plugins.js +4 -110
  115. package/dist/lib/project-resources.js +4 -1
  116. package/dist/lib/refresh.d.ts +5 -3
  117. package/dist/lib/refresh.js +7 -5
  118. package/dist/lib/resources/commands.js +4 -1
  119. package/dist/lib/resources/mcp.js +0 -4
  120. package/dist/lib/resources/permissions.d.ts +1 -1
  121. package/dist/lib/resources/permissions.js +1 -5
  122. package/dist/lib/resources/rules.js +4 -1
  123. package/dist/lib/resources/skills.js +4 -1
  124. package/dist/lib/resources/subagents.js +4 -0
  125. package/dist/lib/resources/types.d.ts +1 -1
  126. package/dist/lib/routines.d.ts +33 -0
  127. package/dist/lib/routines.js +122 -0
  128. package/dist/lib/run-defaults.d.ts +1 -0
  129. package/dist/lib/run-defaults.js +9 -0
  130. package/dist/lib/runner.d.ts +0 -12
  131. package/dist/lib/runner.js +49 -10
  132. package/dist/lib/scheduler.js +16 -4
  133. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  134. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  135. package/dist/lib/secrets/agent.d.ts +16 -0
  136. package/dist/lib/secrets/agent.js +105 -3
  137. package/dist/lib/secrets/audit.d.ts +46 -0
  138. package/dist/lib/secrets/audit.js +56 -0
  139. package/dist/lib/secrets/bundles.d.ts +11 -8
  140. package/dist/lib/secrets/bundles.js +31 -23
  141. package/dist/lib/secrets/index.d.ts +2 -1
  142. package/dist/lib/secrets/index.js +8 -2
  143. package/dist/lib/secrets/remote.d.ts +75 -2
  144. package/dist/lib/secrets/remote.js +139 -5
  145. package/dist/lib/secrets/sync.js +3 -3
  146. package/dist/lib/session/active.d.ts +128 -26
  147. package/dist/lib/session/active.js +192 -53
  148. package/dist/lib/session/actor-sidecar.d.ts +23 -0
  149. package/dist/lib/session/actor-sidecar.js +101 -0
  150. package/dist/lib/session/db.d.ts +4 -1
  151. package/dist/lib/session/db.js +65 -8
  152. package/dist/lib/session/discover.d.ts +4 -0
  153. package/dist/lib/session/discover.js +13 -0
  154. package/dist/lib/session/hook-sessions.d.ts +9 -0
  155. package/dist/lib/session/hook-sessions.js +57 -8
  156. package/dist/lib/session/pid-registry.d.ts +13 -0
  157. package/dist/lib/session/render.d.ts +2 -0
  158. package/dist/lib/session/render.js +1 -1
  159. package/dist/lib/session/types.d.ts +16 -0
  160. package/dist/lib/staleness/detectors/commands.js +2 -2
  161. package/dist/lib/staleness/detectors/permissions.js +1 -45
  162. package/dist/lib/staleness/writers/commands.js +4 -5
  163. package/dist/lib/staleness/writers/hooks.js +1 -1
  164. package/dist/lib/startup/command-registry.d.ts +1 -2
  165. package/dist/lib/startup/command-registry.js +2 -4
  166. package/dist/lib/state.js +57 -1
  167. package/dist/lib/subagents-registry.js +5 -12
  168. package/dist/lib/subagents.d.ts +0 -10
  169. package/dist/lib/subagents.js +0 -12
  170. package/dist/lib/teams/agents.d.ts +0 -2
  171. package/dist/lib/teams/agents.js +10 -27
  172. package/dist/lib/teams/index.d.ts +1 -1
  173. package/dist/lib/teams/index.js +1 -1
  174. package/dist/lib/types.d.ts +17 -9
  175. package/dist/lib/usage.d.ts +28 -5
  176. package/dist/lib/usage.js +271 -8
  177. package/dist/lib/versions.js +7 -1
  178. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  179. package/dist/lib/watchdog/watchdog.js +25 -13
  180. package/package.json +1 -2
  181. package/dist/commands/check.d.ts +0 -15
  182. package/dist/commands/check.js +0 -180
  183. package/dist/commands/resources.d.ts +0 -5
@@ -1,6 +1,6 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import { AGENTS, agentConfigDirName } from './agents.js';
3
+ import { AGENTS, agentConfigDirName, isAgentHardDeprecated } from './agents.js';
4
4
  import { supports } from './capabilities.js';
5
5
  import { buildCommandSkillContent, commandSkillName, readSkillSourceCommandMarker, shouldInstallCommandAsSkill } from './command-skills.js';
6
6
  import { commandAppliesTo, parseCommandMetadata } from './commands.js';
@@ -16,6 +16,9 @@ export function projectAgentRoot(projectRoot, agent) {
16
16
  return path.join(projectRoot, agentConfigDirName(agent));
17
17
  }
18
18
  export function syncProjectResourcesToAgent(agent, version, projectAgentsDir) {
19
+ if (isAgentHardDeprecated(agent)) {
20
+ return { synced: [], skipped: [] };
21
+ }
19
22
  const projectRoot = path.dirname(projectAgentsDir);
20
23
  const agentRoot = projectAgentRoot(projectRoot, agent);
21
24
  const manifest = loadProjectManifest(agentRoot);
@@ -3,9 +3,11 @@
3
3
  * sync resources into installed version homes, register hooks, add shims to
4
4
  * PATH, prompt for missing default versions, install declared host-CLIs.
5
5
  *
6
- * Used by `agents repo refresh` (user-facing) and any other caller that needs
7
- * to re-derive local state from declared configuration. Does NOT do any git
8
- * operations — that lives in `agents repo pull`.
6
+ * The reconcile stage behind `agents sync` (the umbrella `--local` path calls
7
+ * this; see sync-umbrella.ts) and any other caller that needs to re-derive local
8
+ * state from declared configuration. Does NOT do any git operations — that lives
9
+ * in `agents repo pull`. (Also still reachable via the deprecated, hidden
10
+ * `agents repo refresh` alias.)
9
11
  */
10
12
  import type { AgentId } from './types.js';
11
13
  export interface RefreshOptions {
@@ -3,9 +3,11 @@
3
3
  * sync resources into installed version homes, register hooks, add shims to
4
4
  * PATH, prompt for missing default versions, install declared host-CLIs.
5
5
  *
6
- * Used by `agents repo refresh` (user-facing) and any other caller that needs
7
- * to re-derive local state from declared configuration. Does NOT do any git
8
- * operations — that lives in `agents repo pull`.
6
+ * The reconcile stage behind `agents sync` (the umbrella `--local` path calls
7
+ * this; see sync-umbrella.ts) and any other caller that needs to re-derive local
8
+ * state from declared configuration. Does NOT do any git operations — that lives
9
+ * in `agents repo pull`. (Also still reachable via the deprecated, hidden
10
+ * `agents repo refresh` alias.)
9
11
  */
10
12
  import * as fs from 'fs';
11
13
  import * as path from 'path';
@@ -13,7 +15,7 @@ import chalk from 'chalk';
13
15
  import ora from 'ora';
14
16
  import { select, confirm } from '@inquirer/prompts';
15
17
  import { capableAgents } from './capabilities.js';
16
- import { AGENTS, ALL_AGENT_IDS, getAllCliStates, registerMcpToTargets, agentLabel, } from './agents.js';
18
+ import { AGENTS, MANAGED_AGENT_IDS, getAllCliStates, registerMcpToTargets, agentLabel, } from './agents.js';
17
19
  import { readManifest, MANIFEST_FILENAME } from './manifest.js';
18
20
  import { getUserAgentsDir } from './state.js';
19
21
  import { installVersion, listInstalledVersions, isVersionIsolated, getGlobalDefault, setGlobalDefault, getVersionHomePath, syncResourcesToVersion, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, promptResourceSelection, resolveConfiguredAgentTargets, } from './versions.js';
@@ -114,7 +116,7 @@ export async function refresh(options = {}) {
114
116
  }
115
117
  // 3. Sync resources to default version homes
116
118
  const cliStates = await getAllCliStates();
117
- const agentsToSync = agentFilter ? [agentFilter] : ALL_AGENT_IDS;
119
+ const agentsToSync = agentFilter ? [agentFilter] : MANAGED_AGENT_IDS;
118
120
  const available = getAvailableResources();
119
121
  for (const agentId of agentsToSync) {
120
122
  if (!cliStates[agentId]?.installed && listInstalledVersions(agentId).length === 0)
@@ -8,7 +8,7 @@
8
8
  import * as fs from 'fs';
9
9
  import * as path from 'path';
10
10
  import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, } from '../state.js';
11
- import { AGENTS, agentConfigDirName } from '../agents.js';
11
+ import { AGENTS, agentConfigDirName, isAgentHardDeprecated } from '../agents.js';
12
12
  import { markdownToToml } from '../convert.js';
13
13
  /**
14
14
  * Get the commands directory for a given layer root.
@@ -162,6 +162,9 @@ export class CommandsHandler {
162
162
  * Copies/transforms commands as needed for the agent's expected format.
163
163
  */
164
164
  sync(agent, versionHome, cwd) {
165
+ if (isAgentHardDeprecated(agent)) {
166
+ return;
167
+ }
165
168
  const agentConfig = AGENTS[agent];
166
169
  const targetFormat = this.format(agent);
167
170
  const targetDir = path.join(versionHome, agentConfigDirName(agent), this.targetDir(agent));
@@ -115,8 +115,6 @@ export function getMcpConfigPath(agent, versionHome) {
115
115
  return path.join(versionHome, '.config', 'opencode', 'opencode.jsonc');
116
116
  case 'cursor':
117
117
  return path.join(versionHome, '.cursor', 'mcp.json');
118
- case 'gemini':
119
- return path.join(versionHome, '.gemini', 'settings.json');
120
118
  case 'openclaw':
121
119
  return path.join(versionHome, '.openclaw', 'openclaw.json');
122
120
  case 'antigravity':
@@ -126,8 +124,6 @@ export function getMcpConfigPath(agent, versionHome) {
126
124
  return path.join(versionHome, '.grok', 'mcp.json');
127
125
  case 'hermes':
128
126
  return path.join(versionHome, '.hermes', 'config.yaml');
129
- case 'forge':
130
- return path.join(versionHome, '.forge', '.mcp.json');
131
127
  default:
132
128
  return null;
133
129
  }
@@ -4,7 +4,7 @@
4
4
  * Permissions are stored as YAML files in permissions/ directories at each layer.
5
5
  * Resolution: project > user > system (higher layer wins on name conflict).
6
6
  * Unlike other resources, permissions merge into agent-specific config files
7
- * (Claude/Gemini: settings.json, Codex: config.toml, OpenCode: opencode.jsonc).
7
+ * (Claude: settings.json, Codex: config.toml, OpenCode: opencode.jsonc).
8
8
  */
9
9
  import type { ResourceHandler } from './types.js';
10
10
  import type { PermissionSet } from '../types.js';
@@ -4,7 +4,7 @@
4
4
  * Permissions are stored as YAML files in permissions/ directories at each layer.
5
5
  * Resolution: project > user > system (higher layer wins on name conflict).
6
6
  * Unlike other resources, permissions merge into agent-specific config files
7
- * (Claude/Gemini: settings.json, Codex: config.toml, OpenCode: opencode.jsonc).
7
+ * (Claude: settings.json, Codex: config.toml, OpenCode: opencode.jsonc).
8
8
  */
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
@@ -65,8 +65,6 @@ function getAgentConfigPath(agent, versionHome) {
65
65
  return path.join(versionHome, '.claude', 'settings.json');
66
66
  case 'codex':
67
67
  return path.join(versionHome, '.codex', 'config.toml');
68
- case 'gemini':
69
- return path.join(versionHome, '.gemini', 'settings.json');
70
68
  case 'opencode':
71
69
  return path.join(versionHome, '.config', 'opencode', 'opencode.jsonc');
72
70
  case 'kimi':
@@ -79,8 +77,6 @@ function getAgentConfigPath(agent, versionHome) {
79
77
  return path.join(versionHome, '.openclaw', 'openclaw.json');
80
78
  case 'copilot':
81
79
  return path.join(versionHome, '.copilot', 'permissions-config.json');
82
- case 'forge':
83
- return path.join(versionHome, '.forge', 'permissions.yaml');
84
80
  case 'hermes':
85
81
  return path.join(versionHome, '.hermes', 'config.yaml');
86
82
  case 'goose':
@@ -9,7 +9,7 @@
9
9
  * - All unique subrules across layers are unioned
10
10
  */
11
11
  import * as fs from 'fs';
12
- import { agentConfigDirName } from '../agents.js';
12
+ import { agentConfigDirName, isAgentHardDeprecated } from '../agents.js';
13
13
  import * as path from 'path';
14
14
  import { getSystemRulesDir, getUserRulesDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../state.js';
15
15
  const SUBRULES_DIR = 'subrules';
@@ -158,6 +158,9 @@ export const RulesHandler = {
158
158
  return resolveRule(name, getLayerDirs(cwd));
159
159
  },
160
160
  sync(agent, versionHome, _cwd) {
161
+ if (isAgentHardDeprecated(agent)) {
162
+ return;
163
+ }
161
164
  // Rules sync is handled by the compose module and syncResourcesToVersion.
162
165
  // This method ensures the agent config directory exists.
163
166
  // The actual composition and write happens in versions.ts via composeRulesFromState().
@@ -5,7 +5,7 @@
5
5
  * Format is the same for all agents. Resolution order: project > user > system.
6
6
  */
7
7
  import * as fs from 'fs';
8
- import { AGENTS, agentConfigDirName } from '../agents.js';
8
+ import { AGENTS, agentConfigDirName, isAgentHardDeprecated } from '../agents.js';
9
9
  import * as path from 'path';
10
10
  import * as yaml from 'yaml';
11
11
  import { getSystemSkillsDir, getUserSkillsDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../state.js';
@@ -201,6 +201,9 @@ export function createSkillsHandler(provider = defaultProvider) {
201
201
  return null;
202
202
  },
203
203
  sync(agent, versionHome, cwd) {
204
+ if (isAgentHardDeprecated(agent)) {
205
+ return;
206
+ }
204
207
  // Agents that read directly from central ~/.agents/skills/ (e.g. Gemini,
205
208
  // Goose via the Summon extension) should not get a per-version copy.
206
209
  if (AGENTS[agent]?.nativeAgentsSkillsDir) {
@@ -8,6 +8,7 @@ import * as fs from 'fs';
8
8
  import * as path from 'path';
9
9
  import * as yaml from 'yaml';
10
10
  import { getProjectAgentsDir, getUserSubagentsDir, getSystemSubagentsDir, getEnabledExtraRepos, } from '../state.js';
11
+ import { isAgentHardDeprecated } from '../agents.js';
11
12
  /** Get layer directories for subagent resolution. */
12
13
  function getLayerDirs(cwd) {
13
14
  const projectDir = getProjectAgentsDir(cwd);
@@ -155,6 +156,9 @@ export class SubagentsHandler {
155
156
  * Copies YAML files to the target directory.
156
157
  */
157
158
  sync(agent, versionHome, cwd) {
159
+ if (isAgentHardDeprecated(agent)) {
160
+ return;
161
+ }
158
162
  const targetDir = path.join(versionHome, this.targetDir(agent));
159
163
  // Ensure target directory exists
160
164
  if (!fs.existsSync(targetDir)) {
@@ -5,7 +5,7 @@
5
5
  * - Union: All resources from all layers are combined
6
6
  * - Override on name conflict: Higher layer wins (project > user > system)
7
7
  */
8
- export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'forge';
8
+ export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes';
9
9
  export type Layer = 'system' | 'user' | 'project';
10
10
  export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent' | 'workflow' | 'memory';
11
11
  /** A resolved resource with its origin layer. */
@@ -187,6 +187,14 @@ export interface JobConfig {
187
187
  resume?: string;
188
188
  /** When set, executeJob runs this job through the loop driver instead of once. */
189
189
  loop?: LoopConfig;
190
+ /**
191
+ * Actor id of whoever CREATED this routine (`resolveActor().id`, stamped by
192
+ * `writeJob` at creation and preserved across edits). Propagated into each
193
+ * fired run's env and RunMeta so an unattended cron traces back to the person
194
+ * who scheduled it, not the `UNRESOLVED@<host>` a live resolve would give.
195
+ * RUSH-2020.
196
+ */
197
+ actor?: string;
190
198
  }
191
199
  /** Metadata for a single job execution, persisted as JSON in the run directory. */
192
200
  export interface RunMeta {
@@ -216,6 +224,18 @@ export interface RunMeta {
216
224
  cloudTaskId?: string;
217
225
  /** Cloud provider id when the run was cloud-dispatched. */
218
226
  cloudProvider?: string;
227
+ /**
228
+ * Actor id of the routine's CREATOR (stamped at creation, carried from the job
229
+ * config). Answers "whose scheduled run is this" for an unattended cron fire,
230
+ * where resolving the actor live would only yield `UNRESOLVED@<host>`. RUSH-2020.
231
+ */
232
+ actor?: string;
233
+ /**
234
+ * Actor id that TRIGGERED this particular run (`resolveActor().id` at fire
235
+ * time): a person for a manual `agents routines run`, `UNRESOLVED@<host>` for
236
+ * an unattended scheduled fire. Distinct from {@link actor} (the creator).
237
+ */
238
+ triggeredBy?: string;
219
239
  }
220
240
  /**
221
241
  * Finalize a run record with a terminal status, computing `duration` from
@@ -327,6 +347,19 @@ export declare function validateJob(config: Partial<JobConfig>): string[];
327
347
  export declare function validateTrigger(trigger: unknown): string[];
328
348
  /** True when a job's endAt has already elapsed. False when endAt is unset or in the future. */
329
349
  export declare function isPastEndAt(config: Pick<JobConfig, 'endAt'>, now?: Date): boolean;
350
+ export interface OneShotScheduleParts {
351
+ minute: number;
352
+ hour: number;
353
+ day: number;
354
+ month: number;
355
+ }
356
+ export declare function parseOneShotLikeSchedule(schedule: string | undefined | null): OneShotScheduleParts | null;
357
+ export declare function isOneShotLikeSchedule(schedule: string | undefined | null): boolean;
358
+ export declare function isOneShotRoutine(config: Pick<JobConfig, 'schedule' | 'runOnce'>): boolean;
359
+ export declare function oneShotScheduleFireDate(schedule: string | undefined | null, now?: Date, timezone?: string): Date | null;
360
+ export declare function isPastOneShotRoutine(config: Pick<JobConfig, 'schedule' | 'runOnce' | 'timezone'>, now?: Date): boolean;
361
+ export declare function hasCompletedOneShotRun(config: Pick<JobConfig, 'name' | 'schedule' | 'runOnce' | 'timezone'>, now?: Date): boolean;
362
+ export declare function shouldPurgeCompletedOneShotRoutine(config: Pick<JobConfig, 'name' | 'schedule' | 'runOnce' | 'timezone'>, now?: Date): boolean;
330
363
  /** Expand built-in and user-defined template variables in a job's prompt string. */
331
364
  export declare function resolveJobPrompt(config: JobConfig): string;
332
365
  /** Parse a human-readable timeout string (e.g. "10m", "2h", "1h30m", "3d", "1w") into milliseconds.
@@ -15,6 +15,7 @@ import { safeJoin, isSafeSegmentName } from './paths.js';
15
15
  import { atomicWriteFileSync } from './fs-atomic.js';
16
16
  import { ALL_AGENT_IDS } from './agents.js';
17
17
  import { machineId, normalizeHost } from './machine-id.js';
18
+ import { resolveActor } from './actor.js';
18
19
  export const HOST_STRATEGIES = ['local', 'host', 'fleet', 'cloud'];
19
20
  /** Canonical set of accepted GitHub trigger events — single source for validation. */
20
21
  export const GITHUB_TRIGGER_EVENTS = [
@@ -268,6 +269,11 @@ function readJobFile(filePath) {
268
269
  */
269
270
  export function writeJob(config) {
270
271
  ensureAgentsDir();
272
+ // Stamp the creator once (RUSH-2020). An edit re-writes a config loaded from
273
+ // disk, which already carries `actor`, so this preserves the original creator;
274
+ // only a brand-new routine (no actor yet) gets the current resolver.
275
+ if (!config.actor)
276
+ config.actor = resolveActor().id;
271
277
  const jobsDir = getRoutinesDir();
272
278
  const ymlPath = safeJoin(jobsDir, config.name + '.yml');
273
279
  const yamlPath = safeJoin(jobsDir, config.name + '.yaml');
@@ -580,6 +586,122 @@ export function isPastEndAt(config, now = new Date()) {
580
586
  return false;
581
587
  return now.getTime() >= end;
582
588
  }
589
+ export function parseOneShotLikeSchedule(schedule) {
590
+ if (!schedule)
591
+ return null;
592
+ const parts = schedule.trim().split(/\s+/);
593
+ if (parts.length !== 5)
594
+ return null;
595
+ const [minuteRaw, hourRaw, dayRaw, monthRaw, weekdayRaw] = parts;
596
+ if (weekdayRaw !== '*')
597
+ return null;
598
+ if (![minuteRaw, hourRaw, dayRaw, monthRaw].every((p) => /^\d+$/.test(p)))
599
+ return null;
600
+ const minute = parseInt(minuteRaw, 10);
601
+ const hour = parseInt(hourRaw, 10);
602
+ const day = parseInt(dayRaw, 10);
603
+ const month = parseInt(monthRaw, 10);
604
+ if (minute < 0 || minute > 59)
605
+ return null;
606
+ if (hour < 0 || hour > 23)
607
+ return null;
608
+ if (month < 1 || month > 12)
609
+ return null;
610
+ if (day < 1 || day > 31)
611
+ return null;
612
+ return { minute, hour, day, month };
613
+ }
614
+ export function isOneShotLikeSchedule(schedule) {
615
+ return parseOneShotLikeSchedule(schedule) !== null;
616
+ }
617
+ export function isOneShotRoutine(config) {
618
+ return Boolean(config.runOnce || isOneShotLikeSchedule(config.schedule));
619
+ }
620
+ function zonedParts(date, timezone) {
621
+ const parts = new Intl.DateTimeFormat('en-US', {
622
+ timeZone: timezone,
623
+ year: 'numeric',
624
+ month: 'numeric',
625
+ day: 'numeric',
626
+ hour: 'numeric',
627
+ minute: 'numeric',
628
+ hour12: false,
629
+ hourCycle: 'h23',
630
+ }).formatToParts(date);
631
+ const get = (type) => parseInt(parts.find((p) => p.type === type)?.value ?? '0', 10);
632
+ return {
633
+ year: get('year'),
634
+ month: get('month'),
635
+ day: get('day'),
636
+ hour: get('hour'),
637
+ minute: get('minute'),
638
+ };
639
+ }
640
+ function zonedDateToUtc(year, parts, timezone) {
641
+ const targetUtc = Date.UTC(year, parts.month - 1, parts.day, parts.hour, parts.minute, 0, 0);
642
+ let candidate = new Date(targetUtc);
643
+ for (let i = 0; i < 4; i++) {
644
+ const actual = zonedParts(candidate, timezone);
645
+ const actualUtc = Date.UTC(actual.year, actual.month - 1, actual.day, actual.hour, actual.minute, 0, 0);
646
+ const delta = actualUtc - targetUtc;
647
+ if (delta === 0)
648
+ break;
649
+ candidate = new Date(candidate.getTime() - delta);
650
+ }
651
+ const verify = zonedParts(candidate, timezone);
652
+ if (verify.year !== year ||
653
+ verify.month !== parts.month ||
654
+ verify.day !== parts.day ||
655
+ verify.hour !== parts.hour ||
656
+ verify.minute !== parts.minute) {
657
+ return null;
658
+ }
659
+ return candidate;
660
+ }
661
+ export function oneShotScheduleFireDate(schedule, now = new Date(), timezone) {
662
+ const parts = parseOneShotLikeSchedule(schedule);
663
+ if (!parts)
664
+ return null;
665
+ if (timezone) {
666
+ try {
667
+ const year = zonedParts(now, timezone).year;
668
+ return zonedDateToUtc(year, parts, timezone);
669
+ }
670
+ catch {
671
+ return null;
672
+ }
673
+ }
674
+ const fireAt = new Date(now.getFullYear(), parts.month - 1, parts.day, parts.hour, parts.minute, 0, 0);
675
+ if (fireAt.getFullYear() !== now.getFullYear() ||
676
+ fireAt.getMonth() !== parts.month - 1 ||
677
+ fireAt.getDate() !== parts.day ||
678
+ fireAt.getHours() !== parts.hour ||
679
+ fireAt.getMinutes() !== parts.minute) {
680
+ return null;
681
+ }
682
+ return fireAt;
683
+ }
684
+ export function isPastOneShotRoutine(config, now = new Date()) {
685
+ if (!isOneShotRoutine(config))
686
+ return false;
687
+ const fireAt = oneShotScheduleFireDate(config.schedule, now, config.timezone);
688
+ return Boolean(fireAt && now.getTime() >= fireAt.getTime());
689
+ }
690
+ export function hasCompletedOneShotRun(config, now = new Date()) {
691
+ if (!isPastOneShotRoutine(config, now))
692
+ return false;
693
+ const fireAt = oneShotScheduleFireDate(config.schedule, now, config.timezone);
694
+ if (!fireAt)
695
+ return false;
696
+ const latest = getLatestRun(config.name);
697
+ if (!latest || latest.status === 'running')
698
+ return false;
699
+ const startedAt = Date.parse(latest.startedAt);
700
+ return Number.isFinite(startedAt) && startedAt >= fireAt.getTime() - 60_000;
701
+ }
702
+ export function shouldPurgeCompletedOneShotRoutine(config, now = new Date()) {
703
+ return hasCompletedOneShotRun(config, now);
704
+ }
583
705
  /** Expand built-in and user-defined template variables in a job's prompt string. */
584
706
  export function resolveJobPrompt(config) {
585
707
  const now = new Date();
@@ -36,6 +36,7 @@ export declare function parseRunDefaultSelector(input: string): ParsedRunDefault
36
36
  export declare function resolveRunDefaultsFromConfig(runConfig: RunConfig | undefined, agent: AgentId, version?: string | null): ResolvedRunDefaults;
37
37
  export declare function resolveRunDefaultsFromConfigs(runConfigs: Array<RunConfig | undefined>, agent: AgentId, version?: string | null): ResolvedRunDefaults;
38
38
  export declare function resolveRunDefaults(agent: AgentId, version?: string | null, startPath?: string): ResolvedRunDefaults;
39
+ export declare function formatRunDefaultEntry(entry: RunDefaultEntry): string;
39
40
  export declare function listRunDefaults(): RunDefaultEntry[];
40
41
  export declare function setRunDefault(selectorInput: string, defaultsInput: RunDefaultsInput): RunDefaultEntry;
41
42
  export declare function unsetRunDefault(selectorInput: string): boolean;
@@ -15,6 +15,7 @@ import { ALL_MODES } from './types.js';
15
15
  import { AGENTS } from './agents.js';
16
16
  import { readMeta, updateMeta } from './state.js';
17
17
  import { getProjectRunConfigs } from './run-config.js';
18
+ import chalk from 'chalk';
18
19
  const VERSION_RE = /^(?:\*|latest|(?!.*\.\.)[A-Za-z0-9._+-]{1,64})$/;
19
20
  function isAgentId(value) {
20
21
  return value in AGENTS;
@@ -128,6 +129,14 @@ export function resolveRunDefaults(agent, version, startPath = process.cwd()) {
128
129
  const projectRunConfigs = getProjectRunConfigs(startPath).reverse();
129
130
  return resolveRunDefaultsFromConfigs([readMeta().run, ...projectRunConfigs], agent, version);
130
131
  }
132
+ export function formatRunDefaultEntry(entry) {
133
+ const parts = [];
134
+ if (entry.defaults.mode)
135
+ parts.push(`mode ${chalk.white(entry.defaults.mode)}`);
136
+ if (entry.defaults.model)
137
+ parts.push(`model ${chalk.white(entry.defaults.model)}`);
138
+ return `${chalk.cyan(entry.selector.padEnd(22))} ${parts.join(' ')}`;
139
+ }
131
140
  export function listRunDefaults() {
132
141
  const defaults = readMeta().run?.defaults ?? {};
133
142
  return Object.entries(defaults)
@@ -64,18 +64,6 @@ export declare function dispatchesViaAgentsRun(config: Pick<JobConfig, 'workflow
64
64
  * with `agents run`.
65
65
  */
66
66
  export declare function buildRoutineSpawnEnv(baseEnv: Record<string, string>, agent: AgentId, version: string | undefined, timezone?: string): Record<string, string>;
67
- /**
68
- * Execute a job synchronously (waits for completion or timeout before resolving).
69
- *
70
- * When `config.loop` is set the job is routed through the loop driver (`runLoop`
71
- * from loop.ts) instead of a single spawn — same driver as `agents run --loop` and
72
- * workflow `loop:` blocks (issue #400). The optional `deps` parameter provides
73
- * injectable seams (runIteration, sleep, writeCheckpoint) used by tests; production
74
- * callers omit it and get the defaults.
75
- *
76
- * Single-shot path: pre-flight version/account selection + mid-run rate-limit
77
- * failover across healthy same-agent accounts (RUSH-1016).
78
- */
79
67
  export declare function executeJob(config: JobConfig, deps?: LoopDeps): Promise<RunResult>;
80
68
  /**
81
69
  * Optional lifecycle callbacks for a detached routine run. The daemon passes an
@@ -23,6 +23,7 @@ import { prepareJobHome, buildSpawnEnv, getJobHomePath } from './sandbox.js';
23
23
  import { resolveModel, buildReasoningFlags } from './models.js';
24
24
  import { createTimer, maybeRotate, redactPrompt } from './events.js';
25
25
  import { normalizeMode, resolveHeadlessMode, buildExecEnv, detectRateLimit, detectAuthFailure, isAuthFailureFromLog, authFailureReason, } from './exec.js';
26
+ import { resolveActor } from './actor.js';
26
27
  import { loadTask as loadHostTask } from './hosts/tasks.js';
27
28
  import { reconcileTask as reconcileHostTask } from './hosts/reconcile.js';
28
29
  import { backgroundSpawnOptions } from './platform/process.js';
@@ -398,12 +399,22 @@ export function buildRoutineSpawnEnv(baseEnv, agent, version, timezone) {
398
399
  if (v !== undefined)
399
400
  out[k] = v;
400
401
  }
401
- // No Claude token is injected here. A routine authenticates through the pinned
402
- // account's own CLAUDE_CONFIG_DIR login (buildExecEnv points it at the
403
- // per-account version home), identical to interactive `agents run`. Claude
404
- // Code's interactive session refreshes itself per-device; keeping the daemon
405
- // out of the credential entirely is what avoids the fleet-wide rotation logout
406
- // — a shared/rotating token was the cause, not the fix.
402
+ // A routine authenticates through the pinned account's own CLAUDE_CONFIG_DIR
403
+ // login on THIS box (buildExecEnv points it at the per-account version home).
404
+ // Claude Code's interactive session refreshes itself per-device; keeping the
405
+ // daemon out of the credential entirely is what avoids the fleet-wide rotation
406
+ // logout a shared/rotating token was the cause, not the fix.
407
+ //
408
+ // Injecting a token was already ruled out, but INHERITING one was not:
409
+ // buildExecEnv spreads the ambient process.env (exec.ts) and sanitizeProcessEnv
410
+ // only strips loader/interpreter vars, never credentials. So on any box whose
411
+ // daemon environment happens to carry CLAUDE_CODE_OAUTH_TOKEN, every routine
412
+ // spawn silently ran on that one shared rotating token instead of the host's
413
+ // own login — the exact fleet-wide-logout path, arriving by inheritance rather
414
+ // than injection. CI never caught it because CI has no token to inherit; a
415
+ // provisioned box does. Drop it here so a routine always uses the login of the
416
+ // machine it runs on.
417
+ delete out.CLAUDE_CODE_OAUTH_TOKEN;
407
418
  if (timezone)
408
419
  out.TZ = timezone;
409
420
  return out;
@@ -499,6 +510,28 @@ function spawnJobAttempt(cmd, env, attemptLogPath, timeoutMs, combinedLogPath) {
499
510
  * Single-shot path: pre-flight version/account selection + mid-run rate-limit
500
511
  * failover across healthy same-agent accounts (RUSH-1016).
501
512
  */
513
+ /**
514
+ * Actor provenance for a routine run (RUSH-2020): `actor` is the routine's
515
+ * CREATOR (carried from the job config), `triggeredBy` is whoever kicked off THIS
516
+ * run (`resolveActor().id` — a person for a manual run, `UNRESOLVED@<host>` for an
517
+ * unattended scheduled fire). Spread into every RunMeta so a fired cron traces
518
+ * back to the person who scheduled it.
519
+ */
520
+ function runProvenance(config) {
521
+ return { ...(config.actor ? { actor: config.actor } : {}), triggeredBy: resolveActor().id };
522
+ }
523
+ /**
524
+ * Inject the routine creator's actor into a run's base env so the fired agent
525
+ * INHERITS it (the `AGENTS_ACTOR` path in actor.ts) instead of re-resolving to
526
+ * `UNRESOLVED@<host>` on an unattended fire — so its session, events, and commits
527
+ * all attribute to the person who scheduled the routine (RUSH-2020). Mutates and
528
+ * returns the same env object for call-site brevity.
529
+ */
530
+ function injectRoutineActor(env, config) {
531
+ if (config.actor && !env.AGENTS_ACTOR)
532
+ env.AGENTS_ACTOR = config.actor;
533
+ return env;
534
+ }
502
535
  export async function executeJob(config, deps) {
503
536
  const eligibility = checkJobDeviceEligibility(config);
504
537
  if (eligibility) {
@@ -545,9 +578,9 @@ export async function executeJob(config, deps) {
545
578
  const runId = generateRunId();
546
579
  const runDir = getRunDir(config.name, runId);
547
580
  fs.mkdirSync(runDir, { recursive: true });
548
- const baseEnv = useSandbox
581
+ const baseEnv = injectRoutineActor(useSandbox
549
582
  ? buildSpawnEnv(overlayHome)
550
- : { ...process.env };
583
+ : { ...process.env }, config);
551
584
  // Workflows run via `agents run <workflow>` which delegates to claude under the hood.
552
585
  // Use 'claude' as the effective agent for report extraction and metadata when workflow is set.
553
586
  // (command jobs branched out earlier, so config.agent is set on the non-workflow path.)
@@ -555,6 +588,7 @@ export async function executeJob(config, deps) {
555
588
  const meta = {
556
589
  jobName: config.name,
557
590
  runId,
591
+ ...runProvenance(config),
558
592
  agent: effectiveAgent,
559
593
  ...(config.workflow ? { workflow: config.workflow } : {}),
560
594
  pid: null,
@@ -770,6 +804,7 @@ async function executeJobOnCloud(config, opts) {
770
804
  const meta = {
771
805
  jobName: config.name,
772
806
  runId,
807
+ ...runProvenance(config),
773
808
  agent: config.agent,
774
809
  pid: null,
775
810
  spawnedAt: Date.now(),
@@ -841,6 +876,7 @@ async function executeJobOnHost(config, opts) {
841
876
  const meta = {
842
877
  jobName: config.name,
843
878
  runId,
879
+ ...runProvenance(config),
844
880
  agent: config.agent,
845
881
  pid: null, // no local process — the run lives on the host
846
882
  spawnedAt: Date.now(),
@@ -888,6 +924,7 @@ async function executeCommandJobForeground(config) {
888
924
  const meta = {
889
925
  jobName: config.name,
890
926
  runId,
927
+ ...runProvenance(config),
891
928
  command: config.command,
892
929
  pid: null,
893
930
  spawnedAt: Date.now(),
@@ -1019,9 +1056,9 @@ export async function executeJobDetached(config, hooks) {
1019
1056
  fs.mkdirSync(runDir, { recursive: true });
1020
1057
  const stdoutPath = path.join(runDir, 'stdout.log');
1021
1058
  const stdoutFd = fs.openSync(stdoutPath, 'w', 0o600);
1022
- const baseEnv = useSandbox
1059
+ const baseEnv = injectRoutineActor(useSandbox
1023
1060
  ? buildSpawnEnv(overlayHome)
1024
- : { ...process.env };
1061
+ : { ...process.env }, config);
1025
1062
  const spawnEnv = dispatchesViaAgentsRun(config)
1026
1063
  ? (() => {
1027
1064
  const e = { ...baseEnv };
@@ -1035,6 +1072,7 @@ export async function executeJobDetached(config, hooks) {
1035
1072
  const meta = {
1036
1073
  jobName: config.name,
1037
1074
  runId,
1075
+ ...runProvenance(config),
1038
1076
  agent: effectiveAgent,
1039
1077
  ...(config.workflow ? { workflow: config.workflow } : {}),
1040
1078
  pid: null,
@@ -1155,6 +1193,7 @@ function executeCommandJobDetached(config, hooks) {
1155
1193
  const meta = {
1156
1194
  jobName: config.name,
1157
1195
  runId,
1196
+ ...runProvenance(config),
1158
1197
  command: config.command,
1159
1198
  pid: null,
1160
1199
  spawnedAt: Date.now(),
@@ -6,7 +6,7 @@
6
6
  * on startup and reloads them on SIGHUP.
7
7
  */
8
8
  import { Cron } from 'croner';
9
- import { listJobs, deleteJob, isPastEndAt, setJobEnabled, jobRunsOnThisDevice } from './routines.js';
9
+ import { listJobs, deleteJob, isPastEndAt, isPastOneShotRoutine, isOneShotRoutine, setJobEnabled, shouldPurgeCompletedOneShotRoutine, jobRunsOnThisDevice, } from './routines.js';
10
10
  /** In-memory cron scheduler that triggers a callback when jobs fire. */
11
11
  export class JobScheduler {
12
12
  jobs = new Map();
@@ -20,9 +20,15 @@ export class JobScheduler {
20
20
  // Trigger-only jobs (no cron schedule) fire via the webhook receiver,
21
21
  // not the cron loop — skip them here. Jobs pinned to another device
22
22
  // (routines are fleet-synced) never enter this machine's cron loop.
23
- if (config.enabled && config.schedule && jobRunsOnThisDevice(config)) {
24
- this.schedule(config);
23
+ if (!config.enabled || !config.schedule || !jobRunsOnThisDevice(config))
24
+ continue;
25
+ if (shouldPurgeCompletedOneShotRoutine(config)) {
26
+ deleteJob(config.name);
27
+ continue;
25
28
  }
29
+ if (isPastOneShotRoutine(config))
30
+ continue;
31
+ this.schedule(config);
26
32
  }
27
33
  }
28
34
  schedule(config) {
@@ -30,6 +36,12 @@ export class JobScheduler {
30
36
  if (!config.schedule)
31
37
  return;
32
38
  this.unschedule(config.name);
39
+ if (shouldPurgeCompletedOneShotRoutine(config)) {
40
+ deleteJob(config.name);
41
+ return;
42
+ }
43
+ if (isPastOneShotRoutine(config))
44
+ return;
33
45
  // catch: true — a throw from one job's callback should not kill the
34
46
  // whole cron loop. Each invocation of onTrigger is already wrapped in
35
47
  // try/catch, but a synchronous throw before the await would otherwise
@@ -59,7 +71,7 @@ export class JobScheduler {
59
71
  console.error(`Job '${config.name}' failed:`, err.message);
60
72
  }
61
73
  // One-shot jobs: remove after first execution
62
- if (config.runOnce) {
74
+ if (isOneShotRoutine(config)) {
63
75
  this.unschedule(config.name);
64
76
  deleteJob(config.name);
65
77
  }