@phnx-labs/agents-cli 1.15.0 → 1.17.0

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 (111) hide show
  1. package/CHANGELOG.md +143 -39
  2. package/README.md +6 -6
  3. package/dist/commands/alias.js +2 -2
  4. package/dist/commands/browser-picker.d.ts +21 -0
  5. package/dist/commands/browser-picker.js +114 -0
  6. package/dist/commands/browser.js +793 -83
  7. package/dist/commands/cloud.js +8 -0
  8. package/dist/commands/commands.js +72 -22
  9. package/dist/commands/daemon.js +2 -2
  10. package/dist/commands/exec.js +70 -1
  11. package/dist/commands/hooks.js +71 -26
  12. package/dist/commands/mcp.js +81 -39
  13. package/dist/commands/plugins.js +224 -17
  14. package/dist/commands/prune.js +29 -1
  15. package/dist/commands/pull.js +3 -3
  16. package/dist/commands/repo.js +1 -1
  17. package/dist/commands/routines.js +2 -2
  18. package/dist/commands/secrets.js +154 -20
  19. package/dist/commands/sessions.js +62 -19
  20. package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
  21. package/dist/commands/{init.js → setup.js} +22 -21
  22. package/dist/commands/skills.js +60 -19
  23. package/dist/commands/subagents.js +41 -13
  24. package/dist/commands/utils.d.ts +16 -0
  25. package/dist/commands/utils.js +32 -0
  26. package/dist/commands/view.js +78 -20
  27. package/dist/commands/workflows.d.ts +10 -0
  28. package/dist/commands/workflows.js +457 -0
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.js +48 -36
  31. package/dist/lib/agents.js +2 -2
  32. package/dist/lib/auto-pull-worker.js +2 -3
  33. package/dist/lib/auto-pull.js +2 -2
  34. package/dist/lib/browser/cdp.d.ts +7 -1
  35. package/dist/lib/browser/cdp.js +32 -1
  36. package/dist/lib/browser/chrome.d.ts +10 -0
  37. package/dist/lib/browser/chrome.js +41 -3
  38. package/dist/lib/browser/devices.d.ts +4 -0
  39. package/dist/lib/browser/devices.js +27 -0
  40. package/dist/lib/browser/drivers/local.js +22 -6
  41. package/dist/lib/browser/drivers/ssh.js +9 -2
  42. package/dist/lib/browser/input.d.ts +1 -0
  43. package/dist/lib/browser/input.js +3 -0
  44. package/dist/lib/browser/ipc.js +158 -23
  45. package/dist/lib/browser/profiles.d.ts +10 -2
  46. package/dist/lib/browser/profiles.js +122 -37
  47. package/dist/lib/browser/service.d.ts +91 -13
  48. package/dist/lib/browser/service.js +767 -132
  49. package/dist/lib/browser/types.d.ts +91 -3
  50. package/dist/lib/browser/types.js +16 -0
  51. package/dist/lib/cloud/rush.d.ts +28 -1
  52. package/dist/lib/cloud/rush.js +69 -14
  53. package/dist/lib/cloud/store.js +2 -2
  54. package/dist/lib/commands.d.ts +1 -15
  55. package/dist/lib/commands.js +11 -7
  56. package/dist/lib/daemon.js +2 -3
  57. package/dist/lib/doctor-diff.js +4 -4
  58. package/dist/lib/events.js +2 -2
  59. package/dist/lib/hooks.d.ts +11 -7
  60. package/dist/lib/hooks.js +138 -49
  61. package/dist/lib/migrate.d.ts +1 -1
  62. package/dist/lib/migrate.js +1237 -22
  63. package/dist/lib/models.js +2 -2
  64. package/dist/lib/permissions.d.ts +8 -66
  65. package/dist/lib/permissions.js +18 -18
  66. package/dist/lib/plugins.d.ts +94 -24
  67. package/dist/lib/plugins.js +702 -123
  68. package/dist/lib/pty-server.js +9 -10
  69. package/dist/lib/resource-patterns.d.ts +41 -0
  70. package/dist/lib/resource-patterns.js +82 -0
  71. package/dist/lib/resources/hooks.d.ts +5 -1
  72. package/dist/lib/resources/hooks.js +21 -4
  73. package/dist/lib/resources/index.d.ts +17 -0
  74. package/dist/lib/resources/index.js +7 -0
  75. package/dist/lib/resources/types.d.ts +1 -1
  76. package/dist/lib/resources/workflows.d.ts +24 -0
  77. package/dist/lib/resources/workflows.js +110 -0
  78. package/dist/lib/resources.d.ts +6 -1
  79. package/dist/lib/resources.js +12 -2
  80. package/dist/lib/rotate.js +3 -4
  81. package/dist/lib/session/active.d.ts +3 -0
  82. package/dist/lib/session/active.js +92 -6
  83. package/dist/lib/session/cloud.js +2 -2
  84. package/dist/lib/session/db.d.ts +18 -0
  85. package/dist/lib/session/db.js +109 -5
  86. package/dist/lib/session/discover.d.ts +6 -0
  87. package/dist/lib/session/discover.js +55 -29
  88. package/dist/lib/session/team-filter.js +2 -2
  89. package/dist/lib/shims.d.ts +4 -52
  90. package/dist/lib/shims.js +23 -15
  91. package/dist/lib/skills.js +6 -2
  92. package/dist/lib/sqlite.js +10 -4
  93. package/dist/lib/state.d.ts +101 -16
  94. package/dist/lib/state.js +179 -31
  95. package/dist/lib/subagents.d.ts +28 -0
  96. package/dist/lib/subagents.js +98 -1
  97. package/dist/lib/sync-manifest.d.ts +1 -1
  98. package/dist/lib/sync-manifest.js +3 -3
  99. package/dist/lib/teams/persistence.js +15 -5
  100. package/dist/lib/teams/registry.js +2 -2
  101. package/dist/lib/types.d.ts +75 -17
  102. package/dist/lib/types.js +3 -3
  103. package/dist/lib/usage.js +2 -2
  104. package/dist/lib/versions.d.ts +3 -0
  105. package/dist/lib/versions.js +158 -47
  106. package/dist/lib/workflows.d.ts +79 -0
  107. package/dist/lib/workflows.js +233 -0
  108. package/package.json +1 -5
  109. package/scripts/postinstall.js +60 -59
  110. package/dist/commands/fork.d.ts +0 -10
  111. package/dist/commands/fork.js +0 -146
@@ -23,14 +23,16 @@ import { promisify } from 'util';
23
23
  import chalk from 'chalk';
24
24
  import * as TOML from 'smol-toml';
25
25
  import { checkbox, select } from '@inquirer/prompts';
26
- import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getCommandsDir, getSkillsDir, getHooksDir, getResolvedRulesDir, getUserRulesDir, clearVersionResources, recordVersionResources, getProjectAgentsDir, getPromptcutsPath, getEnabledExtraRepos, getAgentsDir, getUserAgentsDir, getTrashVersionsDir, getActiveRulesPreset } from './state.js';
27
- import { resolveResource } from './resources.js';
26
+ import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getCommandsDir, getSkillsDir, getHooksDir, getResolvedRulesDir, getUserRulesDir, clearVersionResources, getVersionResources, ensureVersionResourcePatterns, getProjectAgentsDir, getPromptcutsPath, getUserPromptcutsPath, getEnabledExtraRepos, getAgentsDir, getUserAgentsDir, getTrashVersionsDir, getActiveRulesPreset } from './state.js';
27
+ import { defaultPatterns, expandPatterns } from './resource-patterns.js';
28
+ import { resolveResource, listResources } from './resources.js';
28
29
  import { AGENTS, getAccountEmail, MCP_CAPABLE_AGENTS, COMMANDS_CAPABLE_AGENTS, getMcpConfigPathForHome, parseMcpConfig, resolveAgentName, formatAgentError } from './agents.js';
29
- import { applyPermissionsToVersion as applyPermsToVersion, PERMISSIONS_CAPABLE_AGENTS, discoverPermissionGroups, buildPermissionsFromGroups, CODEX_RULES_FILENAME, getActivePermissionSetName, readPermissionSetRecipe, PERMISSION_SET_ENV_VAR } from './permissions.js';
30
+ import { applyPermissionsToVersion as applyPermsToVersion, PERMISSIONS_CAPABLE_AGENTS, discoverPermissionGroups, buildPermissionsFromGroups, CODEX_RULES_FILENAME, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
30
31
  import { installMcpServers, parseMcpServerConfig } from './mcp.js';
31
32
  import { markdownToToml } from './convert.js';
32
33
  import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
33
34
  import { listInstalledSubagents, transformSubagentForClaude, syncSubagentToOpenclaw, SUBAGENT_CAPABLE_AGENTS } from './subagents.js';
35
+ import { WORKFLOW_CAPABLE_AGENTS, listInstalledWorkflows, syncWorkflowToVersion } from './workflows.js';
34
36
  import { registerHooksToSettings } from './hooks.js';
35
37
  import { supports, explainSkip } from './capabilities.js';
36
38
  import { discoverPlugins, syncPluginToVersion, isPluginSynced, pluginSupportsAgent, cleanOrphanedPluginSkills } from './plugins.js';
@@ -62,6 +64,7 @@ export function getAvailableResources(cwd = process.cwd()) {
62
64
  permissions: [],
63
65
  subagents: [],
64
66
  plugins: [],
67
+ workflows: [],
65
68
  promptcuts: false,
66
69
  };
67
70
  const projectAgentsDir = getProjectAgentsDir(cwd);
@@ -189,13 +192,26 @@ export function getAvailableResources(cwd = process.cwd()) {
189
192
  }
190
193
  }
191
194
  result.subagents = Array.from(subagentNames);
195
+ // Workflows (directories with WORKFLOW.md)
196
+ const workflowNames = new Set();
197
+ for (const { base } of resourceBases) {
198
+ const workflowsDir = path.join(base, 'workflows');
199
+ if (!fs.existsSync(workflowsDir))
200
+ continue;
201
+ const names = fs.readdirSync(workflowsDir, { withFileTypes: true })
202
+ .filter(d => d.isDirectory() && fs.existsSync(path.join(workflowsDir, d.name, 'WORKFLOW.md')))
203
+ .map(d => d.name);
204
+ for (const name of names) {
205
+ workflowNames.add(name);
206
+ }
207
+ }
208
+ result.workflows = Array.from(workflowNames);
192
209
  // Plugins (directories with .claude-plugin/plugin.json)
193
210
  const allPlugins = discoverPlugins();
194
211
  result.plugins = allPlugins.map(p => p.name);
195
- // Promptcuts — single file at ~/.agents/promptcuts.yaml, not per-agent.
196
- // Project-scoped .agents/promptcuts.yaml is intentionally not supported
197
- // (user-global shortcuts only — they follow the user, not the repo).
198
- result.promptcuts = fs.existsSync(getPromptcutsPath());
212
+ // Promptcuts — present if either layer exists. Reads merge user + system
213
+ // with user precedence (see readMergedPromptcuts); writes always go to user.
214
+ result.promptcuts = fs.existsSync(getUserPromptcutsPath()) || fs.existsSync(getPromptcutsPath());
199
215
  return result;
200
216
  }
201
217
  // Files/dirs that are never synced into a version home (OS metadata, local tooling).
@@ -251,6 +267,7 @@ export function getActuallySyncedResources(agent, version, options = {}) {
251
267
  permissions: [],
252
268
  subagents: [],
253
269
  plugins: [],
270
+ workflows: [],
254
271
  promptcuts: false,
255
272
  };
256
273
  // Commands - check what files exist in version home
@@ -449,6 +466,15 @@ export function getActuallySyncedResources(agent, version, options = {}) {
449
466
  }
450
467
  }
451
468
  }
469
+ // Workflows - check {versionHome}/workflows/ for synced workflow directories
470
+ if (WORKFLOW_CAPABLE_AGENTS.includes(agent)) {
471
+ const workflowsDir = path.join(versionHome, 'workflows');
472
+ if (fs.existsSync(workflowsDir)) {
473
+ result.workflows = fs.readdirSync(workflowsDir, { withFileTypes: true })
474
+ .filter(d => d.isDirectory() && fs.existsSync(path.join(workflowsDir, d.name, 'WORKFLOW.md')))
475
+ .map(d => d.name);
476
+ }
477
+ }
452
478
  return result;
453
479
  }
454
480
  /**
@@ -470,6 +496,7 @@ export function getNewResources(available, actuallySynced) {
470
496
  permissions: available.permissions.filter(p => !actuallySynced.permissions.includes(p)),
471
497
  subagents: available.subagents.filter(s => !actuallySynced.subagents.includes(s)),
472
498
  plugins: available.plugins.filter(p => !actuallySynced.plugins.includes(p)),
499
+ workflows: available.workflows.filter(w => !actuallySynced.workflows.includes(w)),
473
500
  // Promptcuts aren't version-scoped — the hook reads ~/.agents/promptcuts.yaml
474
501
  // directly, so there is never a "new" per-version state to reconcile.
475
502
  promptcuts: false,
@@ -486,6 +513,7 @@ export function hasNewResources(diff, agent, version) {
486
513
  const permsApply = agent ? supports(agent, 'allowlist', version).ok : true;
487
514
  const subagentsApply = agent ? SUBAGENT_CAPABLE_AGENTS.includes(agent) : true;
488
515
  const pluginsApply = agent ? supports(agent, 'plugins', version).ok : true;
516
+ const workflowsApply = agent ? WORKFLOW_CAPABLE_AGENTS.includes(agent) : true;
489
517
  return ((diff.commands.length > 0 && commandsApply) ||
490
518
  diff.skills.length > 0 ||
491
519
  (diff.hooks.length > 0 && hooksApply) ||
@@ -493,7 +521,8 @@ export function hasNewResources(diff, agent, version) {
493
521
  (diff.mcp.length > 0 && mcpApply) ||
494
522
  (diff.permissions.length > 0 && permsApply) ||
495
523
  (diff.subagents.length > 0 && subagentsApply) ||
496
- (diff.plugins.length > 0 && pluginsApply));
524
+ (diff.plugins.length > 0 && pluginsApply) ||
525
+ (diff.workflows.length > 0 && workflowsApply));
497
526
  }
498
527
  /**
499
528
  * Build a summary string of new resources.
@@ -526,6 +555,9 @@ function buildNewResourcesSummary(newResources, agent) {
526
555
  if (newResources.plugins.length > 0 && PLUGINS_CAPABLE_AGENTS.includes(agent)) {
527
556
  parts.push(`${newResources.plugins.length} plugin${newResources.plugins.length === 1 ? '' : 's'}`);
528
557
  }
558
+ if (newResources.workflows.length > 0 && WORKFLOW_CAPABLE_AGENTS.includes(agent)) {
559
+ parts.push(`${newResources.workflows.length} workflow${newResources.workflows.length === 1 ? '' : 's'}`);
560
+ }
529
561
  return parts.join(', ');
530
562
  }
531
563
  /**
@@ -574,6 +606,8 @@ export async function promptNewResourceSelection(agent, newResources) {
574
606
  selection.subagents = newResources.subagents;
575
607
  if (newResources.plugins.length > 0 && PLUGINS_CAPABLE_AGENTS.includes(agent))
576
608
  selection.plugins = newResources.plugins;
609
+ if (newResources.workflows.length > 0 && WORKFLOW_CAPABLE_AGENTS.includes(agent))
610
+ selection.workflows = newResources.workflows;
577
611
  return selection;
578
612
  }
579
613
  // Select specific items for each category
@@ -651,6 +685,14 @@ export async function promptNewResourceSelection(agent, newResources) {
651
685
  if (selected.length > 0)
652
686
  selection.plugins = selected;
653
687
  }
688
+ if (newResources.workflows.length > 0 && WORKFLOW_CAPABLE_AGENTS.includes(agent)) {
689
+ const selected = await checkbox({
690
+ message: 'Select new workflows to sync:',
691
+ choices: newResources.workflows.map(w => ({ name: w, value: w, checked: true })),
692
+ });
693
+ if (selected.length > 0)
694
+ selection.workflows = selected;
695
+ }
654
696
  return selection;
655
697
  }
656
698
  /**
@@ -676,14 +718,14 @@ export async function promptResourceSelection(agent) {
676
718
  ];
677
719
  const availableCategories = categories.filter(c => c.available);
678
720
  if (availableCategories.length === 0) {
679
- console.log(chalk.gray('No resources available in ~/.agents/'));
721
+ console.log(chalk.gray('No resources available to sync.'));
680
722
  return {};
681
723
  }
682
724
  // Step 1: Select categories (with "Select All" shortcut at the top)
683
725
  console.log();
684
726
  const SELECT_ALL_KEY = '__select_all__';
685
727
  const selectedCategories = await checkbox({
686
- message: 'Which resources from ~/.agents/ would you like to sync?',
728
+ message: 'Which resources would you like to sync?',
687
729
  choices: [
688
730
  { name: chalk.bold('Select All (sync everything)'), value: SELECT_ALL_KEY, checked: false },
689
731
  ...availableCategories.map(c => ({
@@ -1189,7 +1231,7 @@ export function resolveVersionAliasLoose(agent, raw) {
1189
1231
  * Get version specified in a project-root agents.yaml (not the user ~/.agents-system/agents.yaml).
1190
1232
  */
1191
1233
  export function getProjectVersion(agent, startPath) {
1192
- const userAgentsYaml = path.join(getAgentsDir(), 'agents.yaml');
1234
+ const userAgentsYaml = path.join(getUserAgentsDir(), 'agents.yaml');
1193
1235
  let dir = path.resolve(startPath);
1194
1236
  while (dir !== path.dirname(dir)) {
1195
1237
  const manifestPath = path.join(dir, 'agents.yaml');
@@ -1400,7 +1442,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1400
1442
  const versionHome = getVersionHomePath(agent, version);
1401
1443
  const agentDir = path.join(versionHome, `.${agent}`);
1402
1444
  fs.mkdirSync(agentDir, { recursive: true });
1403
- const result = { commands: false, skills: false, hooks: false, memory: [], permissions: false, mcp: [], subagents: [], plugins: [] };
1445
+ const result = { commands: false, skills: false, hooks: false, memory: [], permissions: false, mcp: [], subagents: [], plugins: [], workflows: [] };
1404
1446
  const cwd = options.cwd || process.cwd();
1405
1447
  const projectAgentsDir = options.projectDir || getProjectAgentsDir(cwd);
1406
1448
  const userAgentsDir = getUserAgentsDir();
@@ -1408,13 +1450,80 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1408
1450
  // project/user/system repos win on name collisions.
1409
1451
  const extraRepos = getEnabledExtraRepos();
1410
1452
  const available = getAvailableResources(cwd);
1453
+ // Write default resource selection patterns for this version (idempotent —
1454
+ // only sets fields that aren't already present, preserving user edits).
1455
+ {
1456
+ const extraAliases = extraRepos.map(e => e.alias);
1457
+ const allLayers = defaultPatterns(extraAliases);
1458
+ const noProject = defaultPatterns(extraAliases, false);
1459
+ ensureVersionResourcePatterns(agent, version, {
1460
+ commands: allLayers,
1461
+ skills: allLayers,
1462
+ hooks: noProject, // hooks: no project layer (security)
1463
+ subagents: noProject,
1464
+ plugins: noProject,
1465
+ workflows: noProject,
1466
+ permissions: ['system:*'],
1467
+ mcp: ['user:*'],
1468
+ });
1469
+ }
1470
+ // If no explicit selection was passed, build one from the persisted resource
1471
+ // patterns. This lets users customize agents.yaml to control which resources
1472
+ // are synced (e.g. "skills: [system:brain-scan user:creative]").
1473
+ // When patterns are the default (every layer wildcard), the expanded result
1474
+ // equals the full available set — identical to the old behavior.
1475
+ if (!selection) {
1476
+ const vr = getVersionResources(agent, version);
1477
+ if (vr) {
1478
+ const patternSelection = {};
1479
+ // Listable resource types: use listResources to get name→source maps.
1480
+ const listableTypes = [
1481
+ ['commands', 'commands'],
1482
+ ['skills', 'skills'],
1483
+ ['hooks', 'hooks'],
1484
+ ['subagents', 'subagents'],
1485
+ ];
1486
+ for (const [type, kind] of listableTypes) {
1487
+ const patterns = vr[type];
1488
+ if (!Array.isArray(patterns) || patterns.length === 0)
1489
+ continue;
1490
+ const sourceMap = new Map(listResources(kind, cwd).map(r => [r.name, r.source]));
1491
+ patternSelection[type] = expandPatterns(patterns, sourceMap);
1492
+ }
1493
+ // permissions: all groups are 'system' source.
1494
+ if (Array.isArray(vr.permissions) && vr.permissions.length > 0) {
1495
+ const permMap = new Map(available.permissions.map(n => [n, 'system']));
1496
+ patternSelection.permissions = expandPatterns(vr.permissions, permMap);
1497
+ }
1498
+ // mcp: all declared servers are 'user' source.
1499
+ if (Array.isArray(vr.mcp) && vr.mcp.length > 0) {
1500
+ const mcpMap = new Map(available.mcp.map(n => [n, 'user']));
1501
+ patternSelection.mcp = expandPatterns(vr.mcp, mcpMap);
1502
+ }
1503
+ // plugins: treat all as 'user' source for now.
1504
+ if (Array.isArray(vr.plugins) && vr.plugins.length > 0) {
1505
+ const pluginMap = new Map(available.plugins.map(n => [n, 'user']));
1506
+ patternSelection.plugins = expandPatterns(vr.plugins, pluginMap);
1507
+ }
1508
+ // workflows: treat all as 'user' source.
1509
+ if (Array.isArray(vr.workflows) && vr.workflows.length > 0) {
1510
+ const workflowMap = new Map(available.workflows.map(n => [n, 'user']));
1511
+ patternSelection.workflows = expandPatterns(vr.workflows, workflowMap);
1512
+ }
1513
+ // memory is not pattern-controlled (rulesPreset handles it) — always sync.
1514
+ patternSelection.memory = 'all';
1515
+ if (Object.keys(patternSelection).length > 0) {
1516
+ selection = patternSelection;
1517
+ }
1518
+ }
1519
+ }
1411
1520
  // Fast guard: skip the entire sync when no selection is active and nothing
1412
1521
  // has changed since the last full sync. Drops steady-state cost from ~16s
1413
1522
  // (unconditional file copies) to ~2ms (stat calls + manifest read).
1414
1523
  if (!selection && !options.force) {
1415
1524
  const manifest = loadSyncManifest(agent, version);
1416
1525
  if (manifest && !isSyncStale(manifest, available, agent, version, cwd)) {
1417
- return { commands: false, skills: false, hooks: false, memory: [], permissions: false, mcp: [], subagents: [], plugins: [] };
1526
+ return { commands: false, skills: false, hooks: false, memory: [], permissions: false, mcp: [], subagents: [], plugins: [], workflows: [] };
1418
1527
  }
1419
1528
  }
1420
1529
  // Helper: remove a path (symlink or real) if it exists
@@ -1500,9 +1609,6 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1500
1609
  syncedCommands.push(cmd);
1501
1610
  }
1502
1611
  result.commands = syncedCommands.length > 0;
1503
- if (syncedCommands.length > 0) {
1504
- recordVersionResources(agent, version, 'commands', syncedCommands);
1505
- }
1506
1612
  }
1507
1613
  // Sync skills (skip if agent natively reads ~/.agents/skills/)
1508
1614
  if (agentConfig.nativeAgentsSkillsDir) {
@@ -1531,9 +1637,6 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1531
1637
  syncedSkills.push(skill);
1532
1638
  }
1533
1639
  result.skills = syncedSkills.length > 0;
1534
- if (syncedSkills.length > 0) {
1535
- recordVersionResources(agent, version, 'skills', syncedSkills);
1536
- }
1537
1640
  }
1538
1641
  }
1539
1642
  // Sync hooks (if agent supports them at this version)
@@ -1599,9 +1702,6 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1599
1702
  }
1600
1703
  }
1601
1704
  result.hooks = syncedHooks.length > 0;
1602
- if (syncedHooks.length > 0) {
1603
- recordVersionResources(agent, version, 'hooks', syncedHooks);
1604
- }
1605
1705
  // Register hooks into agent-native settings.json/hooks.json. Gemini
1606
1706
  // shipped hooks in 0.26.0; gate already passed above so this is safe.
1607
1707
  if (agent === 'claude' || agent === 'codex' || agent === 'gemini') {
@@ -1630,8 +1730,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1630
1730
  removePath(destFile);
1631
1731
  fs.writeFileSync(destFile, composed.content);
1632
1732
  result.memory.push(targetName);
1633
- // Track which preset materialized surfaces in `agents rules list`.
1634
- recordVersionResources(agent, version, 'memory', [composed.preset]);
1733
+ // rulesPreset is tracked separately via setActiveRulesPreset.
1635
1734
  }
1636
1735
  catch (err) {
1637
1736
  // No rules.yaml yet, or a typo'd preset name. Don't fail the whole sync —
@@ -1641,39 +1740,39 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1641
1740
  }
1642
1741
  // Apply permissions (if agent supports them).
1643
1742
  // Groups live in ~/.agents/permissions/groups/. Optional recipes in
1644
- // ~/.agents/permissions/sets/<name>.yaml pick a subset via `includes:`.
1645
- // If AGENTS_PERMISSION_SET is set, we resolve that recipe and use its
1743
+ // ~/.agents/permissions/presets/<name>.yaml pick a subset via `includes:`.
1744
+ // If AGENTS_PERMISSION_PRESET is set, we resolve that recipe and use its
1646
1745
  // includes list as the group filter (intersected with groups on disk).
1647
1746
  const permissionGroups = discoverPermissionGroups();
1648
1747
  const allGroupNames = permissionGroups.map(g => g.name);
1649
- const activeSetName = getActivePermissionSetName();
1650
- let setFilteredGroups = null;
1651
- if (activeSetName) {
1652
- const recipe = readPermissionSetRecipe(activeSetName);
1748
+ const activePresetName = getActivePermissionPresetName();
1749
+ let presetFilteredGroups = null;
1750
+ if (activePresetName) {
1751
+ const recipe = readPermissionPresetRecipe(activePresetName);
1653
1752
  if (recipe) {
1654
1753
  const available = new Set(allGroupNames);
1655
- setFilteredGroups = recipe.includes.filter(g => available.has(g));
1754
+ presetFilteredGroups = recipe.includes.filter(g => available.has(g));
1656
1755
  }
1657
1756
  else {
1658
- console.warn(`${PERMISSION_SET_ENV_VAR}=${activeSetName} but no recipe at ~/.agents/permissions/sets/${activeSetName}.yaml — falling back to all groups`);
1757
+ console.warn(`${PERMISSION_PRESET_ENV_VAR}=${activePresetName} but no recipe at ~/.agents/permissions/presets/${activePresetName}.yaml — falling back to all groups`);
1659
1758
  }
1660
1759
  }
1661
1760
  let permsToSync;
1662
1761
  if (selection) {
1663
1762
  permsToSync = resolveSelection(selection.permissions, allGroupNames);
1664
- // If a set recipe is active, the recipe's includes list always wins —
1763
+ // If a preset recipe is active, the recipe's includes list always wins —
1665
1764
  // even when the caller passed an explicit array via selection. Without
1666
1765
  // this intersection, `agents add`'s buildAutomaticSelection would pass
1667
1766
  // every group name discovered on disk (including 99-deny), bypassing
1668
1767
  // the sandbox filter.
1669
- if (setFilteredGroups) {
1670
- const filterSet = new Set(setFilteredGroups);
1768
+ if (presetFilteredGroups) {
1769
+ const filterSet = new Set(presetFilteredGroups);
1671
1770
  permsToSync = permsToSync.filter(g => filterSet.has(g));
1672
1771
  }
1673
1772
  }
1674
1773
  else {
1675
1774
  permsToSync = PERMISSIONS_CAPABLE_AGENTS.includes(agent)
1676
- ? (setFilteredGroups ?? allGroupNames)
1775
+ ? (presetFilteredGroups ?? allGroupNames)
1677
1776
  : [];
1678
1777
  }
1679
1778
  if (permsToSync.length > 0 && PERMISSIONS_CAPABLE_AGENTS.includes(agent)) {
@@ -1682,9 +1781,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1682
1781
  if (builtPerms.allow.length > 0 || (builtPerms.deny && builtPerms.deny.length > 0)) {
1683
1782
  const permResult = applyPermsToVersion(agent, builtPerms, versionHome, true);
1684
1783
  result.permissions = permResult.success;
1685
- if (permResult.success) {
1686
- recordVersionResources(agent, version, 'permissions', permsToSync);
1687
- }
1784
+ // permissions patterns already written via ensureVersionResourcePatterns above.
1688
1785
  }
1689
1786
  }
1690
1787
  // Install MCP servers (if agent supports them)
@@ -1696,9 +1793,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1696
1793
  if (mcpToSync.length > 0 && MCP_CAPABLE_AGENTS.includes(agent)) {
1697
1794
  const mcpResult = installMcpServers(agent, version, versionHome, mcpToSync, { cwd });
1698
1795
  result.mcp = mcpResult.applied;
1699
- if (mcpResult.applied.length > 0) {
1700
- recordVersionResources(agent, version, 'mcp', mcpResult.applied);
1701
- }
1796
+ // mcp patterns already written via ensureVersionResourcePatterns above.
1702
1797
  }
1703
1798
  // Sync subagents (claude and openclaw only)
1704
1799
  const subagentsToSync = selection
@@ -1731,9 +1826,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1731
1826
  }
1732
1827
  catch { /* resource sync failed for this item */ }
1733
1828
  }
1734
- if (result.subagents.length > 0) {
1735
- recordVersionResources(agent, version, 'subagents', result.subagents);
1736
- }
1829
+ // subagent patterns already written via ensureVersionResourcePatterns above.
1737
1830
  }
1738
1831
  // Sync plugins (claude and openclaw)
1739
1832
  const pluginsToSync = selection
@@ -1754,9 +1847,27 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
1754
1847
  result.plugins.push(name);
1755
1848
  }
1756
1849
  }
1757
- if (result.plugins.length > 0) {
1758
- recordVersionResources(agent, version, 'plugins', result.plugins);
1850
+ // plugin patterns already written via ensureVersionResourcePatterns above.
1851
+ }
1852
+ // Sync workflows (claude only)
1853
+ const workflowsToSync = selection
1854
+ ? resolveSelection(selection.workflows, available.workflows)
1855
+ : (WORKFLOW_CAPABLE_AGENTS.includes(agent) ? available.workflows : []);
1856
+ if (workflowsToSync.length > 0 && WORKFLOW_CAPABLE_AGENTS.includes(agent)) {
1857
+ const allWorkflows = listInstalledWorkflows();
1858
+ for (const name of workflowsToSync) {
1859
+ const workflow = allWorkflows.get(name);
1860
+ if (!workflow)
1861
+ continue;
1862
+ try {
1863
+ const syncResult = syncWorkflowToVersion(workflow.path, name, agent, versionHome);
1864
+ if (syncResult.success) {
1865
+ result.workflows.push(name);
1866
+ }
1867
+ }
1868
+ catch { /* resource sync failed for this item */ }
1759
1869
  }
1870
+ // workflow patterns already written via ensureVersionResourcePatterns above.
1760
1871
  }
1761
1872
  // Write manifest after a successful full sync so the next launch can skip this work.
1762
1873
  if (!selection) {
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Workflow management library.
3
+ *
4
+ * Workflows are directory bundles with a WORKFLOW.md containing YAML frontmatter.
5
+ * They optionally contain subagents/, skills/, and plugins/ subdirectories that
6
+ * are composed at runtime by `agents run <workflow>`.
7
+ */
8
+ import type { AgentId } from './types.js';
9
+ /** Agents that support running workflows via `agents run`. */
10
+ export declare const WORKFLOW_CAPABLE_AGENTS: AgentId[];
11
+ /** Parsed WORKFLOW.md frontmatter. */
12
+ export interface WorkflowFrontmatter {
13
+ name: string;
14
+ description: string;
15
+ model?: string;
16
+ tools?: string[];
17
+ skills?: string[];
18
+ mcpServers?: string[];
19
+ allowedAgents?: string[];
20
+ }
21
+ /** A workflow found during repo discovery. */
22
+ export interface DiscoveredWorkflow {
23
+ name: string;
24
+ path: string;
25
+ frontmatter: WorkflowFrontmatter;
26
+ subagentCount: number;
27
+ }
28
+ /** A workflow in central storage (~/.agents/workflows/ or ~/.agents-system/workflows/). */
29
+ export interface InstalledWorkflow {
30
+ name: string;
31
+ path: string;
32
+ frontmatter: WorkflowFrontmatter;
33
+ subagentCount: number;
34
+ }
35
+ /** Parse WORKFLOW.md frontmatter from a workflow directory. Returns null if invalid. */
36
+ export declare function parseWorkflowFrontmatter(workflowDir: string): WorkflowFrontmatter | null;
37
+ /** Count subagent .md files in a workflow's subagents/ directory. */
38
+ export declare function countWorkflowSubagents(workflowDir: string): number;
39
+ /**
40
+ * Discover all workflow directories (those containing WORKFLOW.md) in a local path.
41
+ * Checks if the path itself is a workflow, then scans a top-level workflows/ subdirectory,
42
+ * then falls back to scanning all immediate subdirectories.
43
+ */
44
+ export declare function discoverWorkflowsFromRepo(repoPath: string): DiscoveredWorkflow[];
45
+ /**
46
+ * List all workflows in central storage.
47
+ * User layer (~/.agents/workflows/) wins over system (~/.agents-system/workflows/).
48
+ */
49
+ export declare function listInstalledWorkflows(): Map<string, InstalledWorkflow>;
50
+ /** Copy a workflow directory into user central storage (~/.agents/workflows/<name>/). */
51
+ export declare function installWorkflowCentrally(sourcePath: string, name: string): {
52
+ success: boolean;
53
+ error?: string;
54
+ };
55
+ /** Move a workflow from user central storage to trash. */
56
+ export declare function removeWorkflow(name: string): {
57
+ success: boolean;
58
+ error?: string;
59
+ };
60
+ /** List workflow names synced into a specific agent version home (at {versionHome}/workflows/). */
61
+ export declare function listWorkflowsForAgent(_agent: AgentId, versionHome: string): string[];
62
+ /** Copy a workflow directory into a version home at {versionHome}/workflows/<name>/. */
63
+ export declare function syncWorkflowToVersion(workflowPath: string, name: string, _agent: AgentId, versionHome: string): {
64
+ success: boolean;
65
+ error?: string;
66
+ };
67
+ /** Remove a workflow from a specific agent version home. */
68
+ export declare function removeWorkflowFromVersion(agent: AgentId, version: string, name: string): {
69
+ success: boolean;
70
+ error?: string;
71
+ };
72
+ /** Iterate all installed (agent, version) pairs that support workflows. */
73
+ export declare function iterWorkflowsCapableVersions(filter?: {
74
+ agent?: AgentId;
75
+ version?: string;
76
+ }): Array<{
77
+ agent: AgentId;
78
+ version: string;
79
+ }>;