@phnx-labs/agents-cli 1.22.43 → 1.22.45

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 (164) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +14 -12
  3. package/dist/bootstrap.js +3 -6
  4. package/dist/cli/command-registry.d.ts +0 -7
  5. package/dist/cli/command-registry.js +1 -21
  6. package/dist/commands/accounts.d.ts +1 -9
  7. package/dist/commands/accounts.js +12 -90
  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/beta.d.ts +7 -1
  14. package/dist/commands/beta.js +16 -9
  15. package/dist/commands/commands.js +1 -1
  16. package/dist/commands/doctor.js +1 -4
  17. package/dist/commands/events.d.ts +2 -2
  18. package/dist/commands/events.js +6 -5
  19. package/dist/commands/exec.d.ts +1 -1
  20. package/dist/commands/exec.js +8 -4
  21. package/dist/commands/factory.d.ts +1 -1
  22. package/dist/commands/fleet-capture.d.ts +1 -1
  23. package/dist/commands/fleet-capture.js +4 -5
  24. package/dist/commands/harness.js +1 -49
  25. package/dist/commands/hooks.js +1 -1
  26. package/dist/commands/insights.d.ts +1 -1
  27. package/dist/commands/insights.js +85 -156
  28. package/dist/commands/menubar.js +58 -1
  29. package/dist/commands/output.js +2 -2
  30. package/dist/commands/prune.js +5 -4
  31. package/dist/commands/routines.test-fixture.d.ts +86 -0
  32. package/dist/commands/routines.test-fixture.js +345 -0
  33. package/dist/commands/sessions.test-fixture.d.ts +24 -0
  34. package/dist/commands/sessions.test-fixture.js +224 -0
  35. package/dist/commands/setup.js +5 -1
  36. package/dist/commands/share.d.ts +9 -10
  37. package/dist/commands/share.js +68 -50
  38. package/dist/commands/skills.js +1 -1
  39. package/dist/commands/ssh.js +15 -62
  40. package/dist/commands/subagents.js +1 -1
  41. package/dist/commands/view.js +5 -2
  42. package/dist/lib/analytics/mix-commands.d.ts +6 -28
  43. package/dist/lib/analytics/mix-commands.js +14 -40
  44. package/dist/lib/audit/log.d.ts +2 -2
  45. package/dist/lib/audit/log.js +2 -2
  46. package/dist/lib/beta.js +1 -1
  47. package/dist/lib/browser/domain-skills.d.ts +26 -6
  48. package/dist/lib/browser/domain-skills.js +81 -43
  49. package/dist/lib/daemon/daemon.js +11 -0
  50. package/dist/lib/daemon/daemon.test-fixture.d.ts +24 -0
  51. package/dist/lib/daemon/daemon.test-fixture.js +71 -0
  52. package/dist/lib/daemon-ticks.d.ts +38 -1
  53. package/dist/lib/daemon-ticks.js +52 -5
  54. package/dist/lib/device-config.js +1 -2
  55. package/dist/lib/devices/discovery-policy.d.ts +3 -3
  56. package/dist/lib/devices/discovery-policy.js +3 -3
  57. package/dist/lib/devices/fleet.d.ts +4 -8
  58. package/dist/lib/devices/fleet.js +3 -15
  59. package/dist/lib/devices/health-report.js +3 -3
  60. package/dist/lib/devices/pool.d.ts +0 -6
  61. package/dist/lib/devices/pool.js +0 -6
  62. package/dist/lib/devices/registry.d.ts +0 -15
  63. package/dist/lib/devices/registry.js +0 -9
  64. package/dist/lib/devices/stats-cache.d.ts +12 -0
  65. package/dist/lib/devices/stats-cache.js +24 -5
  66. package/dist/lib/fleet/capture.d.ts +1 -1
  67. package/dist/lib/fleet/manifest.js +1 -1
  68. package/dist/lib/fleet/remote-login.d.ts +2 -2
  69. package/dist/lib/fleet/remote-login.js +3 -4
  70. package/dist/lib/hosts/dispatch.d.ts +2 -0
  71. package/dist/lib/hosts/dispatch.js +8 -1
  72. package/dist/lib/hosts/passthrough.js +0 -1
  73. package/dist/lib/hosts/providers/devices.js +1 -13
  74. package/dist/lib/hosts/ready.d.ts +12 -3
  75. package/dist/lib/hosts/ready.js +27 -12
  76. package/dist/lib/hosts/registry.d.ts +4 -5
  77. package/dist/lib/hosts/registry.js +3 -7
  78. package/dist/lib/installations/versions.js +21 -2
  79. package/dist/lib/menubar/install-menubar.d.ts +109 -0
  80. package/dist/lib/menubar/install-menubar.js +199 -4
  81. package/dist/lib/remote-agents-json.js +1 -7
  82. package/dist/lib/routines-placement.d.ts +1 -1
  83. package/dist/lib/routines-placement.js +1 -1
  84. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  85. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
  86. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  87. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
  88. package/dist/lib/session/active.d.ts +4 -16
  89. package/dist/lib/session/active.js +2 -6
  90. package/dist/lib/session/db.d.ts +13 -98
  91. package/dist/lib/session/db.js +4 -11
  92. package/dist/lib/session/discover.d.ts +5 -81
  93. package/dist/lib/session/discover.js +5 -5
  94. package/dist/lib/session/remote/remote-bundle.d.ts +12 -0
  95. package/dist/lib/session/remote/remote-bundle.js +61 -0
  96. package/dist/lib/session/remote/remote-list.d.ts +148 -0
  97. package/dist/lib/session/remote/remote-list.js +607 -0
  98. package/dist/lib/session/remote/remote.d.ts +132 -0
  99. package/dist/lib/session/remote/remote.js +314 -0
  100. package/dist/lib/session/remote/watch.d.ts +101 -0
  101. package/dist/lib/session/remote/watch.js +241 -0
  102. package/dist/lib/session/remote-bundle.d.ts +1 -12
  103. package/dist/lib/session/remote-bundle.js +3 -61
  104. package/dist/lib/session/remote-list.d.ts +1 -148
  105. package/dist/lib/session/remote-list.js +3 -607
  106. package/dist/lib/session/remote.d.ts +1 -132
  107. package/dist/lib/session/remote.js +3 -314
  108. package/dist/lib/session/watch.d.ts +1 -101
  109. package/dist/lib/session/watch.js +3 -242
  110. package/dist/lib/share/config.d.ts +12 -2
  111. package/dist/lib/share/config.js +47 -11
  112. package/dist/lib/share/delete.js +1 -1
  113. package/dist/lib/share/publish.js +1 -1
  114. package/dist/lib/share/worker-template.js +2 -2
  115. package/dist/lib/smart-launch.d.ts +4 -4
  116. package/dist/lib/smart-launch.js +8 -18
  117. package/dist/lib/staleness/index.d.ts +3 -1
  118. package/dist/lib/staleness/index.js +21 -1
  119. package/dist/lib/staleness/types.d.ts +11 -0
  120. package/dist/lib/staleness/writers/commands.js +13 -4
  121. package/dist/lib/staleness/writers/hooks.js +3 -1
  122. package/dist/lib/staleness/writers/mcp.d.ts +0 -7
  123. package/dist/lib/staleness/writers/mcp.js +16 -1
  124. package/dist/lib/staleness/writers/rules.js +1 -1
  125. package/dist/lib/staleness/writers/skills.js +3 -1
  126. package/dist/lib/staleness/writers/subagents.d.ts +0 -9
  127. package/dist/lib/staleness/writers/subagents.js +18 -1
  128. package/dist/lib/staleness/writers/types.d.ts +10 -0
  129. package/dist/lib/startup/command-registry.d.ts +8 -1
  130. package/dist/lib/startup/command-registry.js +21 -6
  131. package/dist/lib/startup/root-command.d.ts +18 -1
  132. package/dist/lib/startup/root-command.js +18 -1
  133. package/dist/lib/state.js +7 -0
  134. package/dist/lib/teams/placement-probe.js +27 -12
  135. package/dist/lib/teams/scheduler.d.ts +3 -1
  136. package/dist/lib/types.d.ts +1 -1
  137. package/dist/lib/view-types.d.ts +3 -0
  138. package/package.json +2 -3
  139. package/dist/bin/agents +0 -0
  140. package/dist/commands/auth.d.ts +0 -9
  141. package/dist/commands/auth.js +0 -108
  142. package/dist/commands/org.d.ts +0 -6
  143. package/dist/commands/org.js +0 -175
  144. package/dist/commands/serve.d.ts +0 -10
  145. package/dist/commands/serve.js +0 -68
  146. package/dist/commands/trends.d.ts +0 -10
  147. package/dist/commands/trends.js +0 -12
  148. package/dist/lib/entitlement.d.ts +0 -31
  149. package/dist/lib/entitlement.js +0 -137
  150. package/dist/lib/prix-account.d.ts +0 -158
  151. package/dist/lib/prix-account.js +0 -214
  152. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  153. package/dist/lib/serve/control.d.ts +0 -95
  154. package/dist/lib/serve/control.js +0 -260
  155. package/dist/lib/serve/data.d.ts +0 -81
  156. package/dist/lib/serve/data.js +0 -91
  157. package/dist/lib/serve/page.d.ts +0 -7
  158. package/dist/lib/serve/page.js +0 -140
  159. package/dist/lib/serve/server.d.ts +0 -80
  160. package/dist/lib/serve/server.js +0 -145
  161. package/dist/lib/serve/stream.d.ts +0 -43
  162. package/dist/lib/serve/stream.js +0 -116
  163. package/dist/lib/serve/token.d.ts +0 -35
  164. package/dist/lib/serve/token.js +0 -85
@@ -7,9 +7,8 @@
7
7
  * usage.db) still exists — it is now `agents insights mix` and the recipe
8
8
  * subcommands below. Latency stays on `agents perf`; quota on `agents usage`.
9
9
  *
10
- * `agents trends` remains a thin deprecated alias registered from
11
- * `commands/trends.ts` that shares this same registration no second copy of
12
- * the recipe tree.
10
+ * Former top-level `agents trends` is gone. The nested spelling
11
+ * `agents insights trends` is an alias of `agents insights mix`.
13
12
  */
14
13
  import chalk from 'chalk';
15
14
  import { setHelpSections } from '../help.js';
@@ -56,33 +55,24 @@ export function renderMixDashboard(days, asJson, bannerLabel = 'agents insights
56
55
  for (const section of dash.sections)
57
56
  printMixSection(section);
58
57
  }
59
- /** One-line deprecation for the retired top-level `agents trends` spelling. */
60
- export function printTrendsDeprecation() {
61
- console.error(chalk.yellow('agents trends is deprecated — use `agents insights mix` (or `agents insights <recipe>`).'));
62
- }
63
58
  /**
64
- * Attach counter-mix subcommands to a parent (typically `insights` or the
65
- * deprecated `trends` alias).
59
+ * Attach counter-mix subcommands to a parent (typically `insights`).
66
60
  *
67
61
  * Layout:
68
- * <parent> mix multi-recipe board (default for trends alias)
62
+ * <parent> mix multi-recipe board
63
+ * <parent> trends alias of mix (former top-level `agents trends`)
69
64
  * <parent> recipes list recipe ids
70
65
  * <parent> query raw usage.db rows
71
66
  * <parent> harness-mix|… one baked recipe
72
67
  */
73
- export function registerMixCommands(parent, opts = {}) {
74
- const before = () => {
75
- if (opts.deprecated)
76
- printTrendsDeprecation();
77
- };
78
- const banner = opts.dashboardBanner ?? 'agents insights mix';
68
+ export function registerMixCommands(parent) {
69
+ const banner = 'agents insights mix';
79
70
  const mix = parent
80
71
  .command('mix')
81
72
  .description('Counter recipes — harness/model mix, token ratios, resource frequency (sessions index + usage.db)')
82
73
  .option('--days <n>', 'Days of history to include', '7')
83
74
  .option('--json', 'Emit JSON instead of tables')
84
75
  .action(function summary() {
85
- before();
86
76
  const o = this.opts();
87
77
  renderMixDashboard(parseMixDays(o.days), Boolean(o.json), banner);
88
78
  });
@@ -115,7 +105,6 @@ export function registerMixCommands(parent, opts = {}) {
115
105
  .description('List baked mix-recipe ids')
116
106
  .option('--json', 'Emit JSON')
117
107
  .action((o) => {
118
- before();
119
108
  const list = listRecipes();
120
109
  if (o.json) {
121
110
  console.log(JSON.stringify(list, null, 2));
@@ -134,7 +123,6 @@ export function registerMixCommands(parent, opts = {}) {
134
123
  .option('--limit <n>', 'Max rows', '40')
135
124
  .option('--json', 'Emit JSON')
136
125
  .action((o) => {
137
- before();
138
126
  const win = analyticsWindow(parseMixDays(o.days));
139
127
  const kind = o.kind && USAGE_KINDS.includes(o.kind)
140
128
  ? o.kind
@@ -177,7 +165,6 @@ export function registerMixCommands(parent, opts = {}) {
177
165
  .option('--days <n>', 'Days of history', '7')
178
166
  .option('--json', 'Emit JSON')
179
167
  .action(function recipeAction() {
180
- before();
181
168
  const parentOpts = this.parent?.opts?.();
182
169
  const o = { ...parentOpts, ...this.opts() };
183
170
  const win = analyticsWindow(parseMixDays(o.days));
@@ -193,37 +180,24 @@ export function registerMixCommands(parent, opts = {}) {
193
180
  printMixSection(section);
194
181
  });
195
182
  }
196
- }
197
- /**
198
- * Thin top-level `agents trends` alias: same mix tree, deprecation on every path,
199
- * default action = mix board (old `agents trends` with no subcommand).
200
- */
201
- export function registerDeprecatedTrendsAlias(program) {
202
- const trends = program
183
+ // Nested home for the former top-level `agents trends` spelling.
184
+ const trends = parent
203
185
  .command('trends')
204
- .description('Deprecated alias of `agents insights mix` — counter recipes over sessions + usage.db')
186
+ .description('Alias of `mix` — former top-level `agents trends`')
205
187
  .option('--days <n>', 'Days of history to include', '7')
206
188
  .option('--json', 'Emit JSON instead of tables')
207
189
  .action(function summary() {
208
- printTrendsDeprecation();
209
190
  const o = this.opts();
210
- renderMixDashboard(parseMixDays(o.days), Boolean(o.json), 'agents insights mix');
191
+ renderMixDashboard(parseMixDays(o.days), Boolean(o.json), banner);
211
192
  });
212
193
  setHelpSections(trends, {
213
194
  examples: `
214
- # Prefer the canonical spelling
195
+ agents insights trends
215
196
  agents insights mix
216
-
217
- # Still works (prints a deprecation line)
218
- agents trends
219
- agents trends harness-mix --json
220
197
  `,
221
198
  notes: `
222
- \`agents trends\` is a compatibility alias. New code and agent briefs should use
223
- \`agents insights mix\` / \`agents insights <recipe>\`. Behavioural report remains
224
- bare \`agents insights\`. Latency remains \`agents perf\`.
199
+ \`agents insights trends\` is the nested spelling of the former top-level
200
+ \`agents trends\`. Prefer \`agents insights mix\`.
225
201
  `,
226
202
  });
227
- // Same subcommands as insights, with deprecation on every fire.
228
- registerMixCommands(trends, { deprecated: true, dashboardBanner: 'agents insights mix' });
229
203
  }
@@ -6,7 +6,7 @@
6
6
  * NOT append there. Operators list run outcomes with:
7
7
  *
8
8
  * agents events --include runs
9
- * agents audit # alias of the above
9
+ * agents events audit # nested alias of the above
10
10
  */
11
11
  /** First record's `prevHash` — a fixed anchor so the chain has a root. */
12
12
  export declare const GENESIS_HASH = "GENESIS";
@@ -59,7 +59,7 @@ export declare function verifyAuditChain(logPath?: string): {
59
59
  ok: boolean;
60
60
  brokenAt?: number;
61
61
  };
62
- /** Read the whole chain, oldest-first. Exposed for `agents audit list`. */
62
+ /** Read the whole chain, oldest-first. Exposed for `agents events audit verify`. */
63
63
  export declare function readAuditLog(logPath?: string): AuditRecord[];
64
64
  /**
65
65
  * Record ONE dispatched run at the single exec chokepoint into the unified
@@ -6,7 +6,7 @@
6
6
  * NOT append there. Operators list run outcomes with:
7
7
  *
8
8
  * agents events --include runs
9
- * agents audit # alias of the above
9
+ * agents events audit # nested alias of the above
10
10
  */
11
11
  import * as fs from 'fs';
12
12
  import * as path from 'path';
@@ -116,7 +116,7 @@ export function verifyAuditChain(logPath = getAuditLogPath()) {
116
116
  }
117
117
  return { ok: true };
118
118
  }
119
- /** Read the whole chain, oldest-first. Exposed for `agents audit list`. */
119
+ /** Read the whole chain, oldest-first. Exposed for `agents events audit verify`. */
120
120
  export function readAuditLog(logPath = getAuditLogPath()) {
121
121
  return readRecords(logPath);
122
122
  }
package/dist/lib/beta.js CHANGED
@@ -86,5 +86,5 @@ export function setBetaEnabled(features, enabled) {
86
86
  return { ...location, enabledFeatures };
87
87
  }
88
88
  export function betaEnableHint(feature) {
89
- return `Enable it with: agents beta enable ${feature}`;
89
+ return `Enable it with: agents setup beta enable ${feature}`;
90
90
  }
@@ -2,7 +2,9 @@
2
2
  * Domain-skill discovery for `agents browser start`.
3
3
  *
4
4
  * When a browser task opens a URL, look up a site-specific SKILL.md from
5
- * `~/.agents/skills/browser/domain-skills/<dir>/SKILL.md` and surface its
5
+ * `skills/browser/domain-skills/<dir>/SKILL.md` in the layered DotAgents
6
+ * repos — project > user > system, first layer with a match wins, the same
7
+ * precedence `resolveResource` in `../resources.ts` applies — and surface its
6
8
  * contents so the calling agent gets per-site operating instructions
7
9
  * (selectors, gotchas, sign-in quirks) before it starts driving the page.
8
10
  *
@@ -24,8 +26,21 @@ export interface ResolvedDomainSkill {
24
26
  /** Hostname the match was made against (post-www strip). */
25
27
  hostname: string;
26
28
  }
27
- /** Where domain-skills live. Override via $AGENTS_BROWSER_DOMAIN_SKILLS_DIR for tests. */
28
- export declare function domainSkillsRoot(): string;
29
+ /**
30
+ * Where domain-skills live, in precedence order: project > user > system >
31
+ * extra repos — the same layering `resolveResource` applies. A layer whose
32
+ * root does not exist is simply skipped at resolve time.
33
+ *
34
+ * The project layer resolves from `cwd` (default `process.cwd()`). The
35
+ * browser daemon is shared and long-lived, and its IPC request does not yet
36
+ * carry the calling CLI's cwd, so at that call site the project layer follows
37
+ * the daemon's own cwd until the caller's cwd is threaded through
38
+ * (RUSH-2996); user/system/extra-repo layers are cwd-independent.
39
+ *
40
+ * $AGENTS_BROWSER_DOMAIN_SKILLS_DIR overrides the whole list with a single
41
+ * root, for tests.
42
+ */
43
+ export declare function domainSkillsRoots(cwd?: string): string[];
29
44
  /**
30
45
  * Derive match candidates from a hostname. Order matters — earlier candidates
31
46
  * are tried first.
@@ -40,12 +55,17 @@ export declare function hostnameMatchCandidates(hostname: string): string[];
40
55
  /**
41
56
  * Resolve a URL to its matching domain-skill, or null if none.
42
57
  *
43
- * Two-pass strategy:
58
+ * Roots are searched in `domainSkillsRoots` order (project > user > system);
59
+ * the first layer with a match wins outright — an earlier layer's
60
+ * directory-name match beats a later layer's `domains:` pin, mirroring
61
+ * `resolveResource`'s layer precedence. Within one layer, two passes:
44
62
  * 1. Index every SKILL.md in the root and read its `domains:` frontmatter.
45
63
  * If any pinned domain matches a candidate, return that skill.
46
64
  * 2. Fall back to directory-name match against the candidate list.
47
65
  *
48
66
  * Errors (missing root, unreadable file, invalid URL) are swallowed and
49
- * yield null — domain-skill discovery must never break browser start.
67
+ * yield null — domain-skill discovery must never break browser start. A miss
68
+ * logs the roots searched at debug level (AGENTS_DEBUG/DEBUG) so a skill
69
+ * sitting in an unsearched layer is diagnosable rather than silent.
50
70
  */
51
- export declare function resolveDomainSkill(url: string): ResolvedDomainSkill | null;
71
+ export declare function resolveDomainSkill(url: string, cwd?: string): ResolvedDomainSkill | null;
@@ -2,7 +2,9 @@
2
2
  * Domain-skill discovery for `agents browser start`.
3
3
  *
4
4
  * When a browser task opens a URL, look up a site-specific SKILL.md from
5
- * `~/.agents/skills/browser/domain-skills/<dir>/SKILL.md` and surface its
5
+ * `skills/browser/domain-skills/<dir>/SKILL.md` in the layered DotAgents
6
+ * repos — project > user > system, first layer with a match wins, the same
7
+ * precedence `resolveResource` in `../resources.ts` applies — and surface its
6
8
  * contents so the calling agent gets per-site operating instructions
7
9
  * (selectors, gotchas, sign-in quirks) before it starts driving the page.
8
10
  *
@@ -14,14 +16,37 @@
14
16
  * the directory-name match.
15
17
  */
16
18
  import * as fs from 'fs';
17
- import * as os from 'os';
18
19
  import * as path from 'path';
19
- /** Where domain-skills live. Override via $AGENTS_BROWSER_DOMAIN_SKILLS_DIR for tests. */
20
- export function domainSkillsRoot() {
20
+ import { getEnabledExtraRepos, getProjectAgentsDir, getSystemAgentsDir, getUserAgentsDir, } from '../state.js';
21
+ /**
22
+ * Where domain-skills live, in precedence order: project > user > system >
23
+ * extra repos — the same layering `resolveResource` applies. A layer whose
24
+ * root does not exist is simply skipped at resolve time.
25
+ *
26
+ * The project layer resolves from `cwd` (default `process.cwd()`). The
27
+ * browser daemon is shared and long-lived, and its IPC request does not yet
28
+ * carry the calling CLI's cwd, so at that call site the project layer follows
29
+ * the daemon's own cwd until the caller's cwd is threaded through
30
+ * (RUSH-2996); user/system/extra-repo layers are cwd-independent.
31
+ *
32
+ * $AGENTS_BROWSER_DOMAIN_SKILLS_DIR overrides the whole list with a single
33
+ * root, for tests.
34
+ */
35
+ export function domainSkillsRoots(cwd) {
21
36
  const override = process.env.AGENTS_BROWSER_DOMAIN_SKILLS_DIR;
22
37
  if (override)
23
- return override;
24
- return path.join(os.homedir(), '.agents', 'skills', 'browser', 'domain-skills');
38
+ return [override];
39
+ const sub = ['skills', 'browser', 'domain-skills'];
40
+ const roots = [];
41
+ const projectDir = getProjectAgentsDir(cwd);
42
+ if (projectDir)
43
+ roots.push(path.join(projectDir, ...sub));
44
+ roots.push(path.join(getUserAgentsDir(), ...sub));
45
+ roots.push(path.join(getSystemAgentsDir(), ...sub));
46
+ for (const extra of getEnabledExtraRepos()) {
47
+ roots.push(path.join(extra.dir, ...sub));
48
+ }
49
+ return roots;
25
50
  }
26
51
  /**
27
52
  * Derive match candidates from a hostname. Order matters — earlier candidates
@@ -85,18 +110,28 @@ function parseDomainsFrontmatter(content) {
85
110
  }
86
111
  return [];
87
112
  }
113
+ /** Stderr debug line, gated on AGENTS_DEBUG/DEBUG — same convention as `../teams/debug.ts`. */
114
+ function debug(message) {
115
+ if (process.env.AGENTS_DEBUG || process.env.DEBUG)
116
+ console.error(message);
117
+ }
88
118
  /**
89
119
  * Resolve a URL to its matching domain-skill, or null if none.
90
120
  *
91
- * Two-pass strategy:
121
+ * Roots are searched in `domainSkillsRoots` order (project > user > system);
122
+ * the first layer with a match wins outright — an earlier layer's
123
+ * directory-name match beats a later layer's `domains:` pin, mirroring
124
+ * `resolveResource`'s layer precedence. Within one layer, two passes:
92
125
  * 1. Index every SKILL.md in the root and read its `domains:` frontmatter.
93
126
  * If any pinned domain matches a candidate, return that skill.
94
127
  * 2. Fall back to directory-name match against the candidate list.
95
128
  *
96
129
  * Errors (missing root, unreadable file, invalid URL) are swallowed and
97
- * yield null — domain-skill discovery must never break browser start.
130
+ * yield null — domain-skill discovery must never break browser start. A miss
131
+ * logs the roots searched at debug level (AGENTS_DEBUG/DEBUG) so a skill
132
+ * sitting in an unsearched layer is diagnosable rather than silent.
98
133
  */
99
- export function resolveDomainSkill(url) {
134
+ export function resolveDomainSkill(url, cwd) {
100
135
  let hostname;
101
136
  try {
102
137
  hostname = new URL(url).hostname;
@@ -106,52 +141,55 @@ export function resolveDomainSkill(url) {
106
141
  }
107
142
  if (!hostname)
108
143
  return null;
109
- const root = domainSkillsRoot();
110
- let entries;
111
- try {
112
- entries = fs.readdirSync(root, { withFileTypes: true });
113
- }
114
- catch {
115
- return null;
116
- }
117
144
  const candidates = hostnameMatchCandidates(hostname);
118
145
  if (candidates.length === 0)
119
146
  return null;
120
147
  const candidateSet = new Set(candidates);
121
- const indexed = [];
122
- for (const e of entries) {
123
- if (!e.isDirectory())
124
- continue;
125
- const skillPath = path.join(root, e.name, 'SKILL.md');
126
- let content;
148
+ const roots = domainSkillsRoots(cwd);
149
+ for (const root of roots) {
150
+ let entries;
127
151
  try {
128
- content = fs.readFileSync(skillPath, 'utf-8');
152
+ entries = fs.readdirSync(root, { withFileTypes: true });
129
153
  }
130
154
  catch {
131
155
  continue;
132
156
  }
133
- indexed.push({
134
- name: e.name,
135
- skillPath,
136
- content,
137
- pinned: parseDomainsFrontmatter(content),
138
- });
139
- }
140
- // Pass 1: explicit `domains:` overrides.
141
- for (const s of indexed) {
142
- for (const d of s.pinned) {
143
- if (candidateSet.has(d)) {
144
- return { name: s.name, path: s.skillPath, content: s.content, hostname };
157
+ const indexed = [];
158
+ for (const e of entries) {
159
+ if (!e.isDirectory())
160
+ continue;
161
+ const skillPath = path.join(root, e.name, 'SKILL.md');
162
+ let content;
163
+ try {
164
+ content = fs.readFileSync(skillPath, 'utf-8');
165
+ }
166
+ catch {
167
+ continue;
145
168
  }
169
+ indexed.push({
170
+ name: e.name,
171
+ skillPath,
172
+ content,
173
+ pinned: parseDomainsFrontmatter(content),
174
+ });
146
175
  }
147
- }
148
- // Pass 2: directory-name match, walking candidates in priority order.
149
- const byName = new Map(indexed.map((s) => [s.name.toLowerCase(), s]));
150
- for (const c of candidates) {
151
- const hit = byName.get(c);
152
- if (hit) {
153
- return { name: hit.name, path: hit.skillPath, content: hit.content, hostname };
176
+ // Pass 1: explicit `domains:` overrides.
177
+ for (const s of indexed) {
178
+ for (const d of s.pinned) {
179
+ if (candidateSet.has(d)) {
180
+ return { name: s.name, path: s.skillPath, content: s.content, hostname };
181
+ }
182
+ }
183
+ }
184
+ // Pass 2: directory-name match, walking candidates in priority order.
185
+ const byName = new Map(indexed.map((s) => [s.name.toLowerCase(), s]));
186
+ for (const c of candidates) {
187
+ const hit = byName.get(c);
188
+ if (hit) {
189
+ return { name: hit.name, path: hit.skillPath, content: hit.content, hostname };
190
+ }
154
191
  }
155
192
  }
193
+ debug(`[browser] no domain-skill for ${hostname}; searched: ${roots.join(', ')}`);
156
194
  return null;
157
195
  }
@@ -1868,6 +1868,17 @@ export function ensureDaemonStarted() {
1868
1868
  // callers that branch on this return (e.g. secrets/agent.ts).
1869
1869
  if (isDaemonRunning())
1870
1870
  return startDaemon();
1871
+ // RUSH-3021: never LAUNCH a daemon from a redirected (sandbox/test) HOME.
1872
+ // #2860 gated service-manager registration on this signal but left the
1873
+ // detached spawn itself ungated, so a test-spawned CLI could fork a daemon
1874
+ // into the test's temp HOME; the child outlives the test and races its
1875
+ // recursive teardown rm (ENOTEMPTY). Placed after the already-running branch
1876
+ // — reporting a live daemon stays allowed, same as the circuit breaker.
1877
+ // AGENTS_SERVICE_MANAGER_ALLOW_REDIRECTED_HOME=1 is the test seam for suites
1878
+ // that exercise daemon startup deliberately; `agents daemon start` remains
1879
+ // the operator override.
1880
+ if (!serviceManagerRegistrationAllowed().allowed)
1881
+ return null;
1871
1882
  // RUSH-2418: the auto-start circuit breaker. A daemon that dies during
1872
1883
  // startup would otherwise be relaunched by EVERY foreground command that
1873
1884
  // wants one (secrets unlock, browser start, watchdog, ...) — an
@@ -0,0 +1,24 @@
1
+ import { type KeychainBackend } from '../secrets/index.js';
2
+ /**
3
+ * Shared fixture for the daemon.*.test.ts suite slices (RUSH-2819).
4
+ *
5
+ * daemon.test.ts was a single 2201-line file (88 tests, ~112s in CI) — the
6
+ * slowest file in the daemon test-ownership group, serializing an entire
7
+ * vitest fork while every other selected file finished. The suite is split
8
+ * into topical slices so per-file fork parallelism can spread the
9
+ * process-spawning / real-daemon integration tests across workers; the
10
+ * helpers shared by more than one slice live here.
11
+ */
12
+ /** An in-memory KeychainBackend stand-in, so a test never touches the real OS keychain. */
13
+ export declare function makeMemoryBackend(): {
14
+ backend: KeychainBackend;
15
+ store: Map<string, string>;
16
+ };
17
+ /**
18
+ * Registers the keychain-backend swap + AGENTS_SECRETS_NO_AGENT hermeticity
19
+ * beforeEach/afterEach every slice of the original daemon.test.ts ran under,
20
+ * verbatim. Call once at the top level of each slice file.
21
+ */
22
+ export declare function installKeychainHermeticity(): void;
23
+ export declare const REPO_ROOT: string;
24
+ export declare const DIST_ENTRY: string;
@@ -0,0 +1,71 @@
1
+ import { beforeEach, afterEach } from 'vitest';
2
+ import * as path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { setKeychainBackendForTest } from '../secrets/index.js';
5
+ import { deleteBundle } from '../secrets/bundles.js';
6
+ /**
7
+ * Shared fixture for the daemon.*.test.ts suite slices (RUSH-2819).
8
+ *
9
+ * daemon.test.ts was a single 2201-line file (88 tests, ~112s in CI) — the
10
+ * slowest file in the daemon test-ownership group, serializing an entire
11
+ * vitest fork while every other selected file finished. The suite is split
12
+ * into topical slices so per-file fork parallelism can spread the
13
+ * process-spawning / real-daemon integration tests across workers; the
14
+ * helpers shared by more than one slice live here.
15
+ */
16
+ /** An in-memory KeychainBackend stand-in, so a test never touches the real OS keychain. */
17
+ export function makeMemoryBackend() {
18
+ const store = new Map();
19
+ const backend = {
20
+ has: (item) => store.has(item),
21
+ get: (item) => {
22
+ const v = store.get(item);
23
+ if (v === undefined)
24
+ throw new Error(`Keychain item '${item}' not found.`);
25
+ return v;
26
+ },
27
+ set: (item, value) => { store.set(item, value); },
28
+ delete: (item) => store.delete(item),
29
+ list: (prefix) => Array.from(store.keys()).filter((k) => k.startsWith(prefix)),
30
+ };
31
+ return { backend, store };
32
+ }
33
+ /**
34
+ * Registers the keychain-backend swap + AGENTS_SECRETS_NO_AGENT hermeticity
35
+ * beforeEach/afterEach every slice of the original daemon.test.ts ran under,
36
+ * verbatim. Call once at the top level of each slice file.
37
+ */
38
+ export function installKeychainHermeticity() {
39
+ let restore = null;
40
+ let prevNoAgent;
41
+ beforeEach(() => {
42
+ const m = makeMemoryBackend();
43
+ restore = setKeychainBackendForTest(m.backend);
44
+ // Hermeticity: readAndResolveBundleEnv consults the running secrets-agent
45
+ // (bundles.ts agentGetSync fast-path) BEFORE the injected keychain backend.
46
+ // On a dev machine where the agent is live and the real `claude` bundle is
47
+ // unlocked, that returns the machine's real CLAUDE_CODE_OAUTH_TOKEN and this
48
+ // test reads a live credential instead of the seeded value (CI has no agent,
49
+ // so it only bites locally). Disable the agent so the read falls through to
50
+ // the in-memory backend above — hermetic regardless of host state.
51
+ prevNoAgent = process.env.AGENTS_SECRETS_NO_AGENT;
52
+ process.env.AGENTS_SECRETS_NO_AGENT = '1';
53
+ });
54
+ afterEach(() => {
55
+ try {
56
+ deleteBundle('claude');
57
+ }
58
+ catch { /* not created */ }
59
+ setKeychainBackendForTest(restore);
60
+ if (prevNoAgent === undefined)
61
+ delete process.env.AGENTS_SECRETS_NO_AGENT;
62
+ else
63
+ process.env.AGENTS_SECRETS_NO_AGENT = prevNoAgent;
64
+ });
65
+ }
66
+ // The real compiled CLI entry, shared by every slice that drives an actual
67
+ // `__daemon-run` subprocess (lifecycle/registry/stop). Computed relative to
68
+ // this file's own location, which stays in src/lib/daemon/ alongside every
69
+ // slice, so the path math is unaffected by the split.
70
+ export const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
71
+ export const DIST_ENTRY = path.join(REPO_ROOT, 'dist', 'index.js');
@@ -18,11 +18,48 @@ export declare function isFreshFleetAuthSnapshot(value: {
18
18
  row: FleetStatusRow;
19
19
  authRows: AuthProbeRow[];
20
20
  }, minimumCapturedAt: number): boolean;
21
+ /**
22
+ * How stale a cached auth verdict may get before the periodic tick re-probes.
23
+ *
24
+ * The auth verdict rides the same rate-limited `/api/oauth/usage` endpoint as
25
+ * the usage probe. Firing it every 3-minute tick on every fleet device drove one
26
+ * per-account request quota to a permanent 429, and the shared backoff then
27
+ * parked usage fleet-wide and froze the usage cache (RUSH-2998). Re-probing at
28
+ * most every 20 minutes cuts that endpoint traffic ~5x while still catching a
29
+ * revocation within one window — and every device keeps a REAL verdict (not a
30
+ * degraded "unverified"), so `agents devices ping --strict` and the run
31
+ * auth-preflight keep working on every host, unlike a publisher/subscriber split
32
+ * that would blind every non-primary box to revocation. Fleet status still
33
+ * publishes every tick — it does not ride that endpoint.
34
+ */
35
+ export declare const AUTH_PROBE_MAX_AGE_MS: number;
36
+ /**
37
+ * True when every cached auth row was probed within {@link AUTH_PROBE_MAX_AGE_MS}
38
+ * — i.e. reusing them would not let a verdict get staler than one probe window.
39
+ * Empty cache is never fresh (nothing to reuse). Pure — unit-tested.
40
+ */
41
+ export declare function isCachedFleetAuthProbeFresh(authRows: AuthProbeRow[], now: number, maxAgeMs?: number): boolean;
42
+ /**
43
+ * Whether the tick may reuse the cached auth verdict instead of re-probing the
44
+ * rate-limited endpoint. `force` (an on-demand `agents devices ping`) ALWAYS
45
+ * re-probes — the whole point of the command is a genuinely live verdict, and
46
+ * missing this `!force` is exactly how a `--strict` check silently passed a
47
+ * revoked account (the second `runFleetPing` call site, RUSH-2998). Pure —
48
+ * unit-tested so that inversion cannot regress unnoticed.
49
+ */
50
+ export declare function shouldReuseCachedAuthProbe(force: boolean, cached: AuthProbeRow[], now: number, maxAgeMs?: number): boolean;
21
51
  /**
22
52
  * Fleet cache warm: publish THIS host's row for the caches `agents fleet
23
53
  * status` / `agents devices list` read (PUBLISH-OWN / READ-UNION, RUSH-2061).
54
+ *
55
+ * `force` is set by on-demand callers (`agents devices ping`) that must return a
56
+ * genuinely live verdict; the periodic daemon tick leaves it unset so it reuses a
57
+ * recent verdict per {@link AUTH_PROBE_MAX_AGE_MS} instead of hammering the
58
+ * rate-limited endpoint every 3 minutes (RUSH-2998).
24
59
  */
25
- export declare function refreshLocalFleetAuthState(): Promise<{
60
+ export declare function refreshLocalFleetAuthState(opts?: {
61
+ force?: boolean;
62
+ }): Promise<{
26
63
  row: FleetStatusRow;
27
64
  authRows: import('./auth-health.js').AuthProbeRow[];
28
65
  }>;
@@ -17,11 +17,51 @@ export function isFreshFleetAuthSnapshot(value, minimumCapturedAt) {
17
17
  && value.authRows.length > 0
18
18
  && value.authRows.every(authRow => authRow.health.checkedAt >= minimumCapturedAt);
19
19
  }
20
+ /**
21
+ * How stale a cached auth verdict may get before the periodic tick re-probes.
22
+ *
23
+ * The auth verdict rides the same rate-limited `/api/oauth/usage` endpoint as
24
+ * the usage probe. Firing it every 3-minute tick on every fleet device drove one
25
+ * per-account request quota to a permanent 429, and the shared backoff then
26
+ * parked usage fleet-wide and froze the usage cache (RUSH-2998). Re-probing at
27
+ * most every 20 minutes cuts that endpoint traffic ~5x while still catching a
28
+ * revocation within one window — and every device keeps a REAL verdict (not a
29
+ * degraded "unverified"), so `agents devices ping --strict` and the run
30
+ * auth-preflight keep working on every host, unlike a publisher/subscriber split
31
+ * that would blind every non-primary box to revocation. Fleet status still
32
+ * publishes every tick — it does not ride that endpoint.
33
+ */
34
+ export const AUTH_PROBE_MAX_AGE_MS = 20 * 60_000;
35
+ /**
36
+ * True when every cached auth row was probed within {@link AUTH_PROBE_MAX_AGE_MS}
37
+ * — i.e. reusing them would not let a verdict get staler than one probe window.
38
+ * Empty cache is never fresh (nothing to reuse). Pure — unit-tested.
39
+ */
40
+ export function isCachedFleetAuthProbeFresh(authRows, now, maxAgeMs = AUTH_PROBE_MAX_AGE_MS) {
41
+ return authRows.length > 0 && authRows.every((r) => now - r.health.checkedAt < maxAgeMs);
42
+ }
43
+ /**
44
+ * Whether the tick may reuse the cached auth verdict instead of re-probing the
45
+ * rate-limited endpoint. `force` (an on-demand `agents devices ping`) ALWAYS
46
+ * re-probes — the whole point of the command is a genuinely live verdict, and
47
+ * missing this `!force` is exactly how a `--strict` check silently passed a
48
+ * revoked account (the second `runFleetPing` call site, RUSH-2998). Pure —
49
+ * unit-tested so that inversion cannot regress unnoticed.
50
+ */
51
+ export function shouldReuseCachedAuthProbe(force, cached, now, maxAgeMs = AUTH_PROBE_MAX_AGE_MS) {
52
+ return !force && isCachedFleetAuthProbeFresh(cached, now, maxAgeMs);
53
+ }
20
54
  /**
21
55
  * Fleet cache warm: publish THIS host's row for the caches `agents fleet
22
56
  * status` / `agents devices list` read (PUBLISH-OWN / READ-UNION, RUSH-2061).
57
+ *
58
+ * `force` is set by on-demand callers (`agents devices ping`) that must return a
59
+ * genuinely live verdict; the periodic daemon tick leaves it unset so it reuses a
60
+ * recent verdict per {@link AUTH_PROBE_MAX_AGE_MS} instead of hammering the
61
+ * rate-limited endpoint every 3 minutes (RUSH-2998).
23
62
  */
24
- export async function refreshLocalFleetAuthState() {
63
+ export async function refreshLocalFleetAuthState(opts) {
64
+ const force = opts?.force === true;
25
65
  const { machineId } = await import('./machine-id.js');
26
66
  const { probeLocalFleetAuth, readFleetAuthRows, writeFleetAuthRows } = await import('./auth-health.js');
27
67
  const { getCliVersion } = await import('./version.js');
@@ -40,11 +80,18 @@ export async function refreshLocalFleetAuthState() {
40
80
  return { row, authRows: readFleetAuthRows(self) };
41
81
  },
42
82
  // A recent daemon publication is the completed result, not a reason to probe
43
- // every provider a second time.
44
- isCompleted: (value) => isFreshFleetAuthSnapshot(value, minimumCapturedAt),
83
+ // every provider a second time. An on-demand caller (force) never accepts a
84
+ // cached snapshot it must return a genuinely live verdict.
85
+ isCompleted: (value) => !force && isFreshFleetAuthSnapshot(value, minimumCapturedAt),
45
86
  refresh: async () => {
46
- const authRows = await probeLocalFleetAuth({ cliVersion: getCliVersion() });
47
- writeFleetAuthRows(self, authRows);
87
+ // Re-probe the rate-limited /oauth/usage endpoint at most every
88
+ // AUTH_PROBE_MAX_AGE_MS; reuse the last real verdict in between (RUSH-2998).
89
+ // Fleet status publishes every tick regardless — it does not ride that endpoint.
90
+ const cached = readFleetAuthRows(self);
91
+ const reuse = shouldReuseCachedAuthProbe(force, cached, requestedAt);
92
+ const authRows = reuse ? cached : await probeLocalFleetAuth({ cliVersion: getCliVersion() });
93
+ if (!reuse)
94
+ writeFleetAuthRows(self, authRows);
48
95
  const row = await publishLocalFleetStatus(self);
49
96
  return { row, authRows };
50
97
  },
@@ -243,8 +243,7 @@ export const CONFIG_KEYS = [
243
243
  type: 'string',
244
244
  description: "What this device is for, fleet-wide: 'worker' (a box agents run on) or 'personal' (a machine you sit at — never " +
245
245
  'picked automatically). Marking ANY device worker turns automatic placement into an allowlist: `--device auto` then ' +
246
- 'picks only from the marked workers. (A paired iPhone/iPad cockpit is marked control by `agents devices pair-ios` ' +
247
- 'and is excluded from placement by that role, not this key.)',
246
+ 'picks only from the marked workers.',
248
247
  validate: (v) => DEVICE_ROLES.includes(v)
249
248
  ? null
250
249
  : `role must be one of ${DEVICE_ROLES.join(' | ')}.`,