@phnx-labs/agents-cli 1.14.1 → 1.14.3

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 (102) hide show
  1. package/README.md +31 -3
  2. package/dist/commands/browser.d.ts +2 -0
  3. package/dist/commands/browser.js +388 -0
  4. package/dist/commands/daemon.js +1 -1
  5. package/dist/commands/doctor.d.ts +16 -9
  6. package/dist/commands/doctor.js +248 -12
  7. package/dist/commands/exec.js +17 -17
  8. package/dist/commands/prune.js +9 -3
  9. package/dist/commands/refresh-rules.d.ts +15 -0
  10. package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
  11. package/dist/commands/routines.js +1 -1
  12. package/dist/commands/rules.js +100 -4
  13. package/dist/commands/secrets.js +206 -12
  14. package/dist/commands/sync.js +19 -0
  15. package/dist/commands/teams.js +162 -22
  16. package/dist/commands/trash.d.ts +10 -0
  17. package/dist/commands/trash.js +187 -0
  18. package/dist/commands/view.js +46 -13
  19. package/dist/index.js +62 -4
  20. package/dist/lib/agents.js +2 -2
  21. package/dist/lib/browser/cdp.d.ts +24 -0
  22. package/dist/lib/browser/cdp.js +94 -0
  23. package/dist/lib/browser/chrome.d.ts +16 -0
  24. package/dist/lib/browser/chrome.js +157 -0
  25. package/dist/lib/browser/drivers/local.d.ts +8 -0
  26. package/dist/lib/browser/drivers/local.js +22 -0
  27. package/dist/lib/browser/drivers/ssh.d.ts +9 -0
  28. package/dist/lib/browser/drivers/ssh.js +129 -0
  29. package/dist/lib/browser/index.d.ts +5 -0
  30. package/dist/lib/browser/index.js +5 -0
  31. package/dist/lib/browser/input.d.ts +6 -0
  32. package/dist/lib/browser/input.js +52 -0
  33. package/dist/lib/browser/ipc.d.ts +12 -0
  34. package/dist/lib/browser/ipc.js +223 -0
  35. package/dist/lib/browser/profiles.d.ts +11 -0
  36. package/dist/lib/browser/profiles.js +61 -0
  37. package/dist/lib/browser/refs.d.ts +21 -0
  38. package/dist/lib/browser/refs.js +88 -0
  39. package/dist/lib/browser/service.d.ts +45 -0
  40. package/dist/lib/browser/service.js +404 -0
  41. package/dist/lib/browser/types.d.ts +73 -0
  42. package/dist/lib/browser/types.js +7 -0
  43. package/dist/lib/cloud/codex.js +1 -1
  44. package/dist/lib/cloud/registry.js +2 -2
  45. package/dist/lib/cloud/rush.js +2 -2
  46. package/dist/lib/cloud/store.js +2 -2
  47. package/dist/lib/daemon.d.ts +1 -1
  48. package/dist/lib/daemon.js +47 -11
  49. package/dist/lib/diff-text.d.ts +25 -0
  50. package/dist/lib/diff-text.js +47 -0
  51. package/dist/lib/doctor-diff.d.ts +64 -0
  52. package/dist/lib/doctor-diff.js +497 -0
  53. package/dist/lib/git.js +3 -3
  54. package/dist/lib/hooks.d.ts +6 -0
  55. package/dist/lib/hooks.js +6 -1
  56. package/dist/lib/migrate.js +77 -0
  57. package/dist/lib/pty-client.js +3 -3
  58. package/dist/lib/pty-server.js +36 -7
  59. package/dist/lib/resources.js +1 -1
  60. package/dist/lib/rotate.d.ts +43 -26
  61. package/dist/lib/rotate.js +99 -44
  62. package/dist/lib/rules/compile.d.ts +104 -0
  63. package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
  64. package/dist/lib/rules/compose.d.ts +78 -0
  65. package/dist/lib/rules/compose.js +170 -0
  66. package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
  67. package/dist/lib/{memory.js → rules/rules.js} +10 -10
  68. package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
  69. package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
  70. package/dist/lib/secrets/bundles.d.ts +61 -4
  71. package/dist/lib/secrets/bundles.js +222 -54
  72. package/dist/lib/secrets/index.d.ts +24 -5
  73. package/dist/lib/secrets/index.js +70 -41
  74. package/dist/lib/session/active.js +5 -5
  75. package/dist/lib/session/db.js +4 -4
  76. package/dist/lib/session/discover.js +2 -2
  77. package/dist/lib/session/render.js +21 -7
  78. package/dist/lib/shims.d.ts +28 -4
  79. package/dist/lib/shims.js +72 -14
  80. package/dist/lib/state.d.ts +22 -28
  81. package/dist/lib/state.js +83 -76
  82. package/dist/lib/sync-manifest.d.ts +2 -2
  83. package/dist/lib/sync-manifest.js +5 -5
  84. package/dist/lib/teams/agents.d.ts +4 -2
  85. package/dist/lib/teams/agents.js +11 -4
  86. package/dist/lib/teams/api.d.ts +1 -1
  87. package/dist/lib/teams/api.js +2 -2
  88. package/dist/lib/teams/index.d.ts +1 -0
  89. package/dist/lib/teams/index.js +1 -0
  90. package/dist/lib/teams/persistence.js +3 -3
  91. package/dist/lib/teams/registry.d.ts +8 -1
  92. package/dist/lib/teams/registry.js +8 -2
  93. package/dist/lib/teams/worktree.d.ts +30 -0
  94. package/dist/lib/teams/worktree.js +96 -0
  95. package/dist/lib/types.d.ts +13 -7
  96. package/dist/lib/types.js +3 -3
  97. package/dist/lib/versions.d.ts +30 -2
  98. package/dist/lib/versions.js +127 -105
  99. package/package.json +1 -1
  100. package/scripts/postinstall.js +29 -0
  101. package/dist/commands/refresh-memory.d.ts +0 -15
  102. package/dist/lib/memory-compile.d.ts +0 -66
@@ -1,12 +1,16 @@
1
1
  import chalk from 'chalk';
2
2
  import { checkAllClis } from '../lib/teams/agents.js';
3
- import { AGENTS, ALL_AGENT_IDS } from '../lib/agents.js';
4
- import { getAvailableResources, getGlobalDefault, } from '../lib/versions.js';
3
+ import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError } from '../lib/agents.js';
4
+ import { getAvailableResources, getGlobalDefault, getVersionHomePath, isVersionInstalled, listInstalledVersions, parseAgentSpec, } from '../lib/versions.js';
5
5
  import { loadSyncManifest, isSyncStale } from '../lib/sync-manifest.js';
6
6
  import { diffVersionCommands, iterCommandsCapableVersions } from '../lib/commands.js';
7
7
  import { diffVersionSkills, iterSkillsCapableVersions } from '../lib/skills.js';
8
8
  import { diffVersionHooks, iterHooksCapableVersions } from '../lib/hooks.js';
9
+ import { diffVersionResources, DOCTOR_ALL_KINDS, } from '../lib/doctor-diff.js';
10
+ import { unifiedDiff, colorizeUnifiedDiff } from '../lib/diff-text.js';
11
+ import * as fs from 'fs';
9
12
  const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
13
+ // ─── overview mode (no target) ────────────────────────────────────────────────
10
14
  function checkSyncStatus(cwd) {
11
15
  const rows = [];
12
16
  for (const agent of ALL_AGENT_IDS) {
@@ -58,7 +62,7 @@ function countOrphans() {
58
62
  }
59
63
  return Array.from(byKey.values()).filter((r) => r.commands + r.skills + r.hooks > 0);
60
64
  }
61
- function renderText(clis, syncRows, orphanRows) {
65
+ function renderOverviewText(clis, syncRows, orphanRows) {
62
66
  console.log(chalk.bold('Agent CLIs'));
63
67
  if (Object.keys(clis).length === 0) {
64
68
  console.log(chalk.gray(' (no agents reported)'));
@@ -113,20 +117,252 @@ function renderText(clis, syncRows, orphanRows) {
113
117
  console.log(chalk.gray(' Run `agents prune` to remove.'));
114
118
  }
115
119
  }
120
+ function parseTargetArg(arg) {
121
+ const at = arg.indexOf('@');
122
+ const agentPart = at === -1 ? arg : arg.slice(0, at);
123
+ const versionPart = at === -1 ? '' : arg.slice(at + 1);
124
+ const agent = resolveAgentName(agentPart);
125
+ if (!agent)
126
+ return { error: formatAgentError(agentPart) };
127
+ if (!versionPart) {
128
+ const versions = listInstalledVersions(agent);
129
+ if (versions.length === 0)
130
+ return { error: `${AGENTS[agent].name} has no installed versions. Run \`agents add ${agent}@<version>\` first.` };
131
+ return { agent, versions };
132
+ }
133
+ if (versionPart === 'default') {
134
+ const def = getGlobalDefault(agent);
135
+ if (!def)
136
+ return { error: `${AGENTS[agent].name} has no default version pinned. Run \`agents use ${agent}@<version>\`.` };
137
+ return { agent, versions: [def] };
138
+ }
139
+ const spec = parseAgentSpec(`${agent}@${versionPart}`);
140
+ if (!spec)
141
+ return { error: `Invalid version: ${versionPart}` };
142
+ if (!isVersionInstalled(agent, versionPart)) {
143
+ return { error: `${AGENTS[agent].name}@${versionPart} is not installed. Installed: ${listInstalledVersions(agent).join(', ') || '(none)'}` };
144
+ }
145
+ return { agent, versions: [versionPart] };
146
+ }
147
+ function parseKindFilter(arg) {
148
+ if (!arg)
149
+ return DOCTOR_ALL_KINDS;
150
+ const requested = arg.split(',').map((s) => s.trim()).filter(Boolean);
151
+ const valid = new Set(DOCTOR_ALL_KINDS);
152
+ const out = [];
153
+ for (const k of requested) {
154
+ if (!valid.has(k)) {
155
+ return { error: `Unknown kind: ${k}. Valid: ${DOCTOR_ALL_KINDS.join(', ')}` };
156
+ }
157
+ out.push(k);
158
+ }
159
+ return out;
160
+ }
161
+ function statusLabel(status) {
162
+ switch (status) {
163
+ case 'ok': return chalk.green('ok ');
164
+ case 'diff': return chalk.yellow('DIFF ');
165
+ case 'missing': return chalk.red('MISS ');
166
+ case 'extra': return chalk.magenta('EXTRA');
167
+ }
168
+ }
169
+ function sourceLabel(diff, layers) {
170
+ if (!diff.source)
171
+ return '';
172
+ if (diff.source === 'extra') {
173
+ // Find which extra repo this came from.
174
+ const sourcePath = diff.sourcePath;
175
+ if (sourcePath) {
176
+ for (const e of layers.extras) {
177
+ if (sourcePath.startsWith(e.dir + '/') || sourcePath === e.dir) {
178
+ return chalk.gray(`source=extra:${e.alias}`);
179
+ }
180
+ }
181
+ }
182
+ return chalk.gray('source=extra');
183
+ }
184
+ return chalk.gray(`source=${diff.source}`);
185
+ }
186
+ function countByStatus(rows) {
187
+ let ok = 0, diff = 0, missing = 0, extra = 0;
188
+ for (const r of rows) {
189
+ if (r.status === 'ok')
190
+ ok++;
191
+ else if (r.status === 'diff')
192
+ diff++;
193
+ else if (r.status === 'missing')
194
+ missing++;
195
+ else if (r.status === 'extra')
196
+ extra++;
197
+ }
198
+ return { ok, diff, missing, extra };
199
+ }
200
+ function renderKindSection(kind, rows, layers, options) {
201
+ const counts = countByStatus(rows);
202
+ const total = rows.length;
203
+ const summaryParts = [];
204
+ if (counts.ok)
205
+ summaryParts.push(`${counts.ok} ok`);
206
+ if (counts.diff)
207
+ summaryParts.push(chalk.yellow(`${counts.diff} diff`));
208
+ if (counts.missing)
209
+ summaryParts.push(chalk.red(`${counts.missing} missing`));
210
+ if (counts.extra)
211
+ summaryParts.push(chalk.magenta(`${counts.extra} extra`));
212
+ const summary = total === 0 ? chalk.gray('(none)') : summaryParts.join(', ');
213
+ console.log(` ${chalk.bold(kind.padEnd(11))} ${chalk.gray(`${total} item${total === 1 ? '' : 's'}`)} ${summary}`);
214
+ if (total === 0)
215
+ return;
216
+ // Hide ok rows by default for big lists; show them only with --diff so the
217
+ // operator can verify presence; otherwise keep output focused on problems.
218
+ const visible = options.showDiff ? rows : rows.filter((r) => r.status !== 'ok');
219
+ if (visible.length === 0) {
220
+ console.log(` ${chalk.gray('all ok')}`);
221
+ return;
222
+ }
223
+ for (const r of visible) {
224
+ const src = sourceLabel(r, layers);
225
+ console.log(` ${statusLabel(r.status)} ${r.name.padEnd(28)} ${src}`);
226
+ if (options.showDiff && r.status === 'diff' && r.sourcePath && r.homePath) {
227
+ const expected = readExpectedForDiff(kind, r);
228
+ const actual = safeRead(r.homePath);
229
+ if (expected != null && actual != null) {
230
+ const patch = unifiedDiff(expected, actual, {
231
+ fromLabel: r.sourcePath,
232
+ toLabel: r.homePath,
233
+ context: 2,
234
+ });
235
+ if (patch)
236
+ console.log(colorizeUnifiedDiff(patch, ' '));
237
+ }
238
+ }
239
+ }
240
+ }
241
+ function safeRead(p) {
242
+ try {
243
+ return fs.readFileSync(p, 'utf-8');
244
+ }
245
+ catch {
246
+ return null;
247
+ }
248
+ }
249
+ function readExpectedForDiff(kind, row) {
250
+ // Skills are directories; per-file diffs would need recursive walking.
251
+ // Keep the v1 behaviour minimal: the row already says DIFF, the user can
252
+ // open the source path to inspect.
253
+ if (kind === 'skills')
254
+ return null;
255
+ if (!row.sourcePath)
256
+ return null;
257
+ return safeRead(row.sourcePath);
258
+ }
259
+ function renderTargetText(report, options) {
260
+ const label = `${AGENT_NAMES[report.agent] || report.agent}@${report.version}`;
261
+ console.log(chalk.bold(label));
262
+ console.log(chalk.gray(` home: ${report.home}`));
263
+ console.log(chalk.gray(` cwd: ${report.cwd}`));
264
+ const layerStr = [
265
+ report.layers.project ? `project=${report.layers.project}` : null,
266
+ `user=${report.layers.user}`,
267
+ `system=${report.layers.system}`,
268
+ report.layers.extras.length > 0
269
+ ? `extras=[${report.layers.extras.map((e) => e.alias).join(',')}]`
270
+ : null,
271
+ ].filter(Boolean).join(' ');
272
+ console.log(chalk.gray(` layers: ${layerStr}`));
273
+ console.log();
274
+ for (const kind of DOCTOR_ALL_KINDS) {
275
+ const rows = report.kinds[kind];
276
+ // Skip kinds that weren't requested (kind filter narrowed the report).
277
+ // We detect this by checking whether the kind has any rows AND was in
278
+ // scope; absent kinds with empty arrays still render so the operator
279
+ // sees what was checked. options.requestedKinds drives this.
280
+ if (options.requestedKinds && !options.requestedKinds.has(kind))
281
+ continue;
282
+ renderKindSection(kind, rows, report.layers, options);
283
+ }
284
+ console.log();
285
+ const { ok, diff, missing, extra } = report.summary;
286
+ const verdictParts = [];
287
+ if (diff)
288
+ verdictParts.push(chalk.yellow(`${diff} divergent`));
289
+ if (missing)
290
+ verdictParts.push(chalk.red(`${missing} missing`));
291
+ if (extra)
292
+ verdictParts.push(chalk.magenta(`${extra} extra`));
293
+ if (verdictParts.length === 0) {
294
+ console.log(chalk.green(` Verdict: ${ok} resource${ok === 1 ? '' : 's'} reconciled. Version home matches resolved sources.`));
295
+ }
296
+ else {
297
+ console.log(` Verdict: ${verdictParts.join(', ')}.`);
298
+ console.log(chalk.gray(` Run \`agents sync --agent ${report.agent} --agent-version ${report.version}\` to reconcile, or \`agents prune\` to drop extras.`));
299
+ }
300
+ }
301
+ // ─── command registration ────────────────────────────────────────────────────
116
302
  export function registerDoctorCommand(program) {
117
303
  program
118
- .command('doctor')
119
- .description('Diagnose CLI availability, sync status, and orphan resources')
304
+ .command('doctor [target]')
305
+ .description('Diagnose CLI availability, sync status, and resource divergence (optionally for a specific agent[@version])')
120
306
  .option('--json', 'Output machine-readable JSON')
121
- .action((opts) => {
122
- const cwd = process.cwd();
123
- const clis = checkAllClis();
124
- const syncRows = checkSyncStatus(cwd);
125
- const orphanRows = countOrphans();
307
+ .option('--diff', 'In target mode, include unified diffs for divergent files')
308
+ .option('--kind <kinds>', 'Restrict to comma-separated resource kinds (commands,skills,hooks,rules,mcp,permissions,subagents,plugins,promptcuts)')
309
+ .option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())')
310
+ .addHelpText('after', `
311
+ Examples:
312
+ # Overview across default versions (CLI availability + sync + orphans)
313
+ agents doctor
314
+
315
+ # Full per-resource report for the active default
316
+ agents doctor claude@default
317
+
318
+ # Pin to a specific installed version
319
+ agents doctor codex@0.117.0
320
+
321
+ # All installed versions for one agent
322
+ agents doctor gemini
323
+
324
+ # Only inspect rules and hooks, with full diffs
325
+ agents doctor claude@default --kind rules,hooks --diff
326
+ `)
327
+ .action((target, opts) => {
328
+ const cwd = opts.cwd ? opts.cwd : process.cwd();
329
+ if (!target) {
330
+ const clis = checkAllClis();
331
+ const syncRows = checkSyncStatus(cwd);
332
+ const orphanRows = countOrphans();
333
+ if (opts.json) {
334
+ console.log(JSON.stringify({ clis, sync: syncRows, orphans: orphanRows }, null, 2));
335
+ return;
336
+ }
337
+ renderOverviewText(clis, syncRows, orphanRows);
338
+ return;
339
+ }
340
+ const parsed = parseTargetArg(target);
341
+ if ('error' in parsed) {
342
+ console.error(chalk.red(parsed.error));
343
+ process.exit(1);
344
+ }
345
+ const kinds = parseKindFilter(opts.kind);
346
+ if (!Array.isArray(kinds)) {
347
+ console.error(chalk.red(kinds.error));
348
+ process.exit(1);
349
+ }
350
+ const reports = parsed.versions.map((v) => diffVersionResources(parsed.agent, v, { cwd, kinds }));
126
351
  if (opts.json) {
127
- console.log(JSON.stringify({ clis, sync: syncRows, orphans: orphanRows }, null, 2));
352
+ console.log(JSON.stringify(reports.length === 1 ? reports[0] : reports, null, 2));
128
353
  return;
129
354
  }
130
- renderText(clis, syncRows, orphanRows);
355
+ const showDiff = !!opts.diff;
356
+ const requestedKinds = opts.kind ? new Set(kinds) : undefined;
357
+ reports.forEach((r, i) => {
358
+ if (i > 0)
359
+ console.log();
360
+ const home = getVersionHomePath(r.agent, r.version);
361
+ if (!fs.existsSync(home)) {
362
+ console.log(chalk.red(`${AGENT_NAMES[r.agent] || r.agent}@${r.version}: version home not found at ${home}`));
363
+ return;
364
+ }
365
+ renderTargetText(r, { showDiff, requestedKinds });
366
+ });
131
367
  });
132
368
  }
@@ -16,12 +16,12 @@ const VALID_AGENTS = Object.keys(AGENT_COMMANDS);
16
16
  function isValidAgent(agent) {
17
17
  return VALID_AGENTS.includes(agent);
18
18
  }
19
- /** Build a one-line banner describing which version the rotation picked. */
20
- function formatRotationBanner(result) {
19
+ /** Build a one-line banner describing which version the strategy picked. */
20
+ function formatRotationBanner(result, verb = 'balanced') {
21
21
  const { picked, healthy, excluded } = result;
22
22
  const label = picked.email ? `${picked.email} · ${picked.agent}@${picked.version}` : `${picked.agent}@${picked.version}`;
23
23
  const ratio = `${healthy.length} of ${healthy.length + excluded.length} healthy`;
24
- return `[agents] rotation picked ${label} (${ratio})`;
24
+ return `[agents] ${verb} picked ${label} (${ratio})`;
25
25
  }
26
26
  /** Register the `agents run <agent> [prompt]` command. */
27
27
  export function registerRunCommand(program) {
@@ -42,8 +42,8 @@ export function registerRunCommand(program) {
42
42
  .option('--verbose', 'Show detailed execution logs')
43
43
  .option('--timeout <duration>', 'Kill the agent after this duration (e.g., 30m, 1h, 2h30m)')
44
44
  .option('--fallback <agents>', 'Comma-separated agents to try on rate-limit failure. Each entry accepts an optional @version pin (e.g., codex@0.116.0,gemini). The primary runs first; if it exits with a rate-limit error, the next agent picks up via /continue handoff.')
45
- .option('-r, --rotate', 'Shortcut for --strategy rotate. Ignored when @version is pinned.')
46
- .option('--strategy <strategy>', 'Version/account selection strategy: pinned | available | rotate. Defaults to run.<agent>.strategy, then pinned.')
45
+ .option('-b, --balanced', 'Shortcut for --strategy balanced. Ignored when @version is pinned.')
46
+ .option('--strategy <strategy>', 'Version/account selection strategy: pinned | available | balanced. Defaults to run.<agent>.strategy, then pinned. (Legacy `rotate` accepted as alias for `balanced`.)')
47
47
  .option('--acp', 'Route through the Agent Client Protocol instead of direct exec. Supported for gemini, claude (via @zed-industries/claude-code-acp adapter). Unified event stream; emits ndjson when --json.')
48
48
  .addHelpText('after', `
49
49
  Modes:
@@ -54,18 +54,20 @@ Run strategy:
54
54
  pinned Use the workspace/global pinned version from agents.yaml.
55
55
  available Use the pinned version if it has usage available; otherwise switch
56
56
  to another signed-in version with usage available.
57
- rotate Pick the signed-in account with usage available and the most
58
- headroom; last-active breaks ties.
57
+ balanced Distribute traffic across healthy accounts using weighted random
58
+ by remaining capacity — fresher accounts get more, near-exhausted
59
+ ones get less. Avoids bursting any single account.
59
60
  Configure with run.<agent>.strategy in agents.yaml, or override with
60
- --strategy. --rotate is kept as a shortcut for --strategy rotate.
61
+ --strategy. --balanced is kept as a shortcut for --strategy balanced.
62
+ Legacy "rotate" is accepted as an alias for "balanced".
61
63
  Ignored when @version is pinned, when a profile is used, or with --fallback.
62
64
 
63
65
  Examples:
64
66
  # Interactive with the pinned default version
65
67
  agents run claude
66
68
 
67
- # Interactive, rotate to the least-used healthy account
68
- agents run claude --strategy rotate
69
+ # Interactive, distribute load across healthy accounts
70
+ agents run claude --strategy balanced
69
71
 
70
72
  # Headless, switch away from the pinned version when usage is unavailable
71
73
  agents run claude "summarize recent git commits" --mode plan --strategy available
@@ -134,14 +136,14 @@ Examples:
134
136
  console.error(chalk.red(`Invalid strategy: ${options.strategy}. Use ${RUN_STRATEGIES.join(', ')}.`));
135
137
  process.exit(1);
136
138
  }
137
- if (options.rotate && explicitStrategy && explicitStrategy !== 'rotate') {
138
- console.error(chalk.red('--rotate conflicts with --strategy. Use one strategy override.'));
139
+ if (options.balanced && explicitStrategy && explicitStrategy !== 'balanced') {
140
+ console.error(chalk.red('--balanced conflicts with --strategy. Use one strategy override.'));
139
141
  process.exit(1);
140
142
  }
141
- const strategy = options.rotate ? 'rotate' : explicitStrategy ?? configuredStrategy;
143
+ const strategy = options.balanced ? 'balanced' : explicitStrategy ?? configuredStrategy;
142
144
  // Strategy only applies to bare agent invocations. Explicit @version,
143
145
  // profiles, and fallback chains already define their execution target.
144
- if (strategy !== 'pinned' || options.rotate || explicitStrategy) {
146
+ if (strategy !== 'pinned' || options.balanced || explicitStrategy) {
145
147
  if (version) {
146
148
  process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: version ${version} is pinned\n`));
147
149
  }
@@ -157,9 +159,7 @@ Examples:
157
159
  if (resolved.version) {
158
160
  version = resolved.version;
159
161
  if (resolved.rotation) {
160
- const banner = strategy === 'available'
161
- ? formatRotationBanner(resolved.rotation).replace('rotation picked', 'available picked')
162
- : formatRotationBanner(resolved.rotation);
162
+ const banner = formatRotationBanner(resolved.rotation, strategy);
163
163
  process.stderr.write(chalk.gray(banner + '\n'));
164
164
  }
165
165
  }
@@ -133,7 +133,7 @@ async function runOrphanPrune(resourceTypes, options) {
133
133
  export function registerPruneCommand(program) {
134
134
  program
135
135
  .command('prune [target]')
136
- .description('Remove orphan resources (commands/skills/hooks) and/or older duplicate version installs')
136
+ .description('Remove orphan resources (commands/skills/hooks) and/or older duplicate version installs (versions soft-deleted to ~/.agents-system/trash/)')
137
137
  .option('--all', 'For orphan cleanup: sweep every installed version (default: current default version per agent)')
138
138
  .option('--dry-run', 'Show what would be removed without deleting')
139
139
  .option('-y, --yes', 'Skip confirmation prompt')
@@ -172,8 +172,14 @@ What's an orphan?
172
172
  reconciled into the version install.
173
173
 
174
174
  What this does NOT do:
175
- This is destructive cleanup only. Adds and updates flow through the auto-sync
176
- that runs when you launch the agent — there is no manual sync verb.
175
+ Adds and updates flow through the auto-sync that runs when you launch the
176
+ agent — there is no manual sync verb.
177
+
178
+ Soft-delete:
179
+ Version directories are NEVER hard-deleted. \`prune\` moves them to
180
+ ~/.agents-system/trash/versions/<agent>/<version>/<timestamp>/. Recover
181
+ with \`agents trash list\` and \`agents trash restore <agent>@<version>\`.
182
+ The trash never auto-expires; \`rm -rf\` it manually when you're sure.
177
183
  `)
178
184
  .action(async (target, options) => {
179
185
  const parsed = parseTarget(target);
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Internal rules refresh command.
3
+ *
4
+ * Registers the hidden `agents refresh-rules` command invoked by shims for
5
+ * agents that do not natively resolve @-imports in their rules file.
6
+ * Recompiles only when source files have changed.
7
+ */
8
+ import { Command } from 'commander';
9
+ /**
10
+ * Hidden command invoked by shims for agents that don't natively resolve
11
+ * @-imports in their rules file. Fast-path check first (sha256 of tracked
12
+ * source files); only recompiles if a source has changed since the last
13
+ * sync. Typical cost: 10-20ms when rules are fresh.
14
+ */
15
+ export declare function registerRefreshRulesCommand(program: Command): void;
@@ -1,26 +1,26 @@
1
1
  /**
2
- * Internal memory refresh command.
2
+ * Internal rules refresh command.
3
3
  *
4
- * Registers the hidden `agents refresh-memory` command invoked by
5
- * shims for agents that do not natively resolve @-imports in their
6
- * memory file. Recompiles memory only when source files have changed.
4
+ * Registers the hidden `agents refresh-rules` command invoked by shims for
5
+ * agents that do not natively resolve @-imports in their rules file.
6
+ * Recompiles only when source files have changed.
7
7
  */
8
8
  import chalk from 'chalk';
9
9
  import { AGENTS } from '../lib/agents.js';
10
10
  import { isVersionInstalled } from '../lib/versions.js';
11
- import { ensureMemoryFresh, supportsMemoryImports } from '../lib/memory-compile.js';
11
+ import { ensureRulesFresh, supportsRulesImports } from '../lib/rules/compile.js';
12
12
  /**
13
13
  * Hidden command invoked by shims for agents that don't natively resolve
14
- * @-imports in their memory file. Fast-path check first (sha256 of tracked
14
+ * @-imports in their rules file. Fast-path check first (sha256 of tracked
15
15
  * source files); only recompiles if a source has changed since the last
16
- * sync. Typical cost: 10-20ms when memory is fresh.
16
+ * sync. Typical cost: 10-20ms when rules are fresh.
17
17
  */
18
- export function registerRefreshMemoryCommand(program) {
18
+ export function registerRefreshRulesCommand(program) {
19
19
  program
20
- .command('refresh-memory', { hidden: true })
21
- .description('Internal: recompile memory for an agent if sources have changed. Called by shims.')
20
+ .command('refresh-rules', { hidden: true })
21
+ .description('Internal: recompile rules for an agent if sources have changed. Called by shims.')
22
22
  .requiredOption('--agent <agent>', 'Agent identifier (codex, opencode, cursor, etc.)')
23
- .requiredOption('--agent-version <version>', 'Installed version whose memory file should be refreshed')
23
+ .requiredOption('--agent-version <version>', 'Installed version whose rules file should be refreshed')
24
24
  .option('--quiet', 'Suppress all output (exit code indicates success)', false)
25
25
  .action((opts) => {
26
26
  const agentId = opts.agent;
@@ -32,7 +32,7 @@ export function registerRefreshMemoryCommand(program) {
32
32
  process.exitCode = 1;
33
33
  return;
34
34
  }
35
- if (supportsMemoryImports(agentId)) {
35
+ if (supportsRulesImports(agentId)) {
36
36
  // Nothing to do — agent resolves @-imports natively.
37
37
  return;
38
38
  }
@@ -43,9 +43,9 @@ export function registerRefreshMemoryCommand(program) {
43
43
  process.exitCode = 1;
44
44
  return;
45
45
  }
46
- const recompiled = ensureMemoryFresh(agentId, version);
46
+ const recompiled = ensureRulesFresh(agentId, version);
47
47
  if (!quiet && recompiled) {
48
- console.log(chalk.gray(`Refreshed memory for ${agentId}@${version}`));
48
+ console.log(chalk.gray(`Refreshed rules for ${agentId}@${version}`));
49
49
  }
50
50
  });
51
51
  }
@@ -572,7 +572,7 @@ Examples:
572
572
  if (options.follow) {
573
573
  const { exec: execCb } = await import('child_process');
574
574
  const { getAgentsDir } = await import('../lib/state.js');
575
- const logPath = path.join(getAgentsDir(), 'daemon.log');
575
+ const logPath = path.join(getAgentsDir(), 'helpers/daemon/logs.jsonl');
576
576
  const child = execCb(`tail -f "${logPath}"`);
577
577
  child.stdout?.pipe(process.stdout);
578
578
  child.stderr?.pipe(process.stderr);
@@ -6,9 +6,11 @@ import * as path from 'path';
6
6
  import { select, checkbox } from '@inquirer/prompts';
7
7
  import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
8
8
  import { cloneRepo } from '../lib/git.js';
9
- import { discoverInstructionsFromRepo, discoverMemoryFilesFromRepo, installInstructionsCentrally, uninstallInstructions, listInstalledInstructionsWithScope, instructionsExists, getInstructionsContent, listCentralMemory, } from '../lib/memory.js';
9
+ import { discoverInstructionsFromRepo, discoverRuleFilesFromRepo, installInstructionsCentrally, uninstallInstructions, listInstalledInstructionsWithScope, instructionsExists, getInstructionsContent, listCentralRules, } from '../lib/rules/rules.js';
10
10
  import { listInstalledVersions, getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, resolveAgentVersionTargets, } from '../lib/versions.js';
11
- import { recordVersionResources } from '../lib/state.js';
11
+ import { recordVersionResources, getActiveRulesPreset, setActiveRulesPreset } from '../lib/state.js';
12
+ import { discoverRulesLayers } from '../lib/rules/compose.js';
13
+ import * as yaml from 'yaml';
12
14
  import { isPromptCancelled, formatPath, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, requireDestructiveArg, } from './utils.js';
13
15
  /** Register the `agents rules` command tree (list, add, view, remove). */
14
16
  export function registerRulesCommands(program) {
@@ -205,7 +207,7 @@ Examples:
205
207
  try {
206
208
  let ruleNames;
207
209
  if (!source) {
208
- const centralRules = listCentralMemory();
210
+ const centralRules = listCentralRules();
209
211
  if (centralRules.length === 0) {
210
212
  console.log(chalk.yellow('No rule files in ~/.agents/rules/'));
211
213
  console.log(chalk.gray('\nTo add rule files from a repo:'));
@@ -271,7 +273,7 @@ Examples:
271
273
  spinner.succeed('Using local path');
272
274
  }
273
275
  const agentInstructions = discoverInstructionsFromRepo(localPath);
274
- const ruleFiles = discoverMemoryFilesFromRepo(localPath);
276
+ const ruleFiles = discoverRuleFilesFromRepo(localPath);
275
277
  const totalFiles = agentInstructions.length + ruleFiles.length;
276
278
  console.log(chalk.bold(`\nFound ${totalFiles} rule file(s):`));
277
279
  if (totalFiles === 0) {
@@ -484,6 +486,100 @@ Examples:
484
486
  console.log(chalk.yellow(`No rule file found for ${agentLabel(agentId)}`));
485
487
  }
486
488
  });
489
+ rulesCmd
490
+ .command('switch <target>')
491
+ .description('Choose the active rule preset for an agent version (persists in agents.yaml)')
492
+ .option('-p, --preset <name>', 'Preset to activate; omit for an interactive picker')
493
+ .addHelpText('after', `
494
+ Targets are <agent>@<version>. Use 'default' for the alias of the global default version.
495
+
496
+ Examples:
497
+ # Persist the cautious preset for claude 2.1.111
498
+ agents rules switch claude@2.1.111 --preset cautious
499
+
500
+ # Reset back to the literal default preset
501
+ agents rules switch claude@default --preset default
502
+
503
+ # Pick interactively
504
+ agents rules switch codex@0.116.0
505
+ `)
506
+ .action(async (target, options) => {
507
+ try {
508
+ const [rawAgent, rawVersion] = target.split('@');
509
+ const agentId = resolveAgentName(rawAgent);
510
+ if (!agentId) {
511
+ console.log(chalk.red(formatAgentError(rawAgent)));
512
+ process.exit(1);
513
+ }
514
+ const version = resolveVersionAlias(agentId, rawVersion);
515
+ if (!version) {
516
+ console.log(chalk.red(`Pass a version: ${agentId}@<version>. Try 'default' or 'agents list ${agentId}'.`));
517
+ process.exit(1);
518
+ }
519
+ const installed = listInstalledVersions(agentId);
520
+ if (!installed.includes(version)) {
521
+ console.log(chalk.red(`Version ${version} not installed for ${agentLabel(agentId)}.`));
522
+ console.log(chalk.gray(`Installed: ${installed.join(', ') || 'none'}`));
523
+ process.exit(1);
524
+ }
525
+ // Discover available presets across layers (highest-priority defines, lowers union in).
526
+ const layers = discoverRulesLayers();
527
+ const presetSet = new Set();
528
+ for (const layer of layers) {
529
+ const yamlPath = path.join(layer.rulesDir, 'rules.yaml');
530
+ if (!fs.existsSync(yamlPath))
531
+ continue;
532
+ try {
533
+ const parsed = yaml.parse(fs.readFileSync(yamlPath, 'utf-8'));
534
+ for (const name of Object.keys(parsed?.presets || {}))
535
+ presetSet.add(name);
536
+ }
537
+ catch { /* malformed yaml — skip */ }
538
+ }
539
+ const presets = Array.from(presetSet).sort();
540
+ if (presets.length === 0) {
541
+ console.log(chalk.red('No presets found. Define presets in ~/.agents-system/rules/rules.yaml or ~/.agents/rules/rules.yaml.'));
542
+ process.exit(1);
543
+ }
544
+ let chosen = options.preset;
545
+ if (chosen) {
546
+ if (!presets.includes(chosen)) {
547
+ console.log(chalk.red(`Unknown preset: ${chosen}`));
548
+ console.log(chalk.gray(`Available: ${presets.join(', ')}`));
549
+ process.exit(1);
550
+ }
551
+ }
552
+ else {
553
+ if (!isInteractiveTerminal()) {
554
+ requireInteractiveSelection('Selecting a rule preset', [
555
+ `agents rules switch ${agentId}@${version} --preset default`,
556
+ `agents rules switch ${agentId}@${version} --preset cautious`,
557
+ ]);
558
+ }
559
+ const current = getActiveRulesPreset(agentId, version);
560
+ chosen = await select({
561
+ message: `Active rule preset for ${agentLabel(agentId)}@${version}`,
562
+ default: current,
563
+ choices: presets.map((p) => ({ name: p === current ? `${p} (current)` : p, value: p })),
564
+ });
565
+ }
566
+ setActiveRulesPreset(agentId, version, chosen);
567
+ const spinner = ora(`Re-syncing ${agentLabel(agentId)}@${version} with preset '${chosen}'`).start();
568
+ const result = syncResourcesToVersion(agentId, version);
569
+ spinner.succeed(`Switched ${agentLabel(agentId)}@${version} to '${chosen}'`);
570
+ if (result.memory.length > 0) {
571
+ console.log(chalk.gray(` Wrote ${result.memory[0]} from preset '${chosen}'.`));
572
+ }
573
+ }
574
+ catch (err) {
575
+ if (isPromptCancelled(err)) {
576
+ console.log(chalk.gray('\nCancelled'));
577
+ return;
578
+ }
579
+ console.error(chalk.red(`Failed to switch preset: ${err.message}`));
580
+ process.exit(1);
581
+ }
582
+ });
487
583
  rulesCmd
488
584
  .command('show [agent]', { hidden: true })
489
585
  .action(async (agentArg) => {