contextos-agents 2.1.1 → 2.3.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 (109) hide show
  1. package/.agents/AGENTS.md +53 -396
  2. package/.agents/adapters/aider/export.js +11 -16
  3. package/.agents/adapters/claude/export.js +13 -13
  4. package/.agents/adapters/copilot/export.js +29 -8
  5. package/.agents/adapters/cursor/export.js +9 -18
  6. package/.agents/adapters/gemini/export.js +11 -46
  7. package/.agents/adapters/pure-compiler.js +65 -42
  8. package/.agents/adapters/shared.js +35 -1
  9. package/.agents/adapters/zed/export.js +2 -2
  10. package/.agents/compiled/registry.v2.json +30 -18
  11. package/.agents/compiled/registry.v2.sha256 +1 -1
  12. package/.agents/compiler/manifest-compiler.js +5 -29
  13. package/.agents/core/skills/context-os/references/project-graph.md +3 -3
  14. package/.agents/core/skills/engineering-workflow/SKILL.md +11 -316
  15. package/.agents/core/skills/engineering-workflow/references/workflow.md +336 -0
  16. package/.agents/core/skills/engineering-workflow/skill.yaml +2 -4
  17. package/.agents/core/skills/gstack-roles/SKILL.md +11 -128
  18. package/.agents/core/skills/gstack-roles/references/roles.md +149 -0
  19. package/.agents/core/skills/gstack-roles/skill.yaml +2 -4
  20. package/.agents/core/skills/ponytail-mindset/SKILL.md +13 -165
  21. package/.agents/core/skills/ponytail-mindset/references/minimalism.md +186 -0
  22. package/.agents/core/skills/ponytail-mindset/skill.yaml +2 -5
  23. package/.agents/core/skills/security/skill.yaml +1 -0
  24. package/.agents/ctx.js +22 -17
  25. package/.agents/customization-dx.js +13 -9
  26. package/.agents/doctor.js +2 -2
  27. package/.agents/generated/claude/skills/context-manager/EXAMPLES.md +19 -0
  28. package/.agents/generated/claude/skills/context-manager/SKILL.md +0 -29
  29. package/.agents/generated/claude/skills/context-manager/TROUBLESHOOTING.md +7 -0
  30. package/.agents/generated/claude/skills/context-manager/VALIDATION.json +12 -0
  31. package/.agents/generated/claude/skills/context-manager/references/context-rules.md +59 -0
  32. package/.agents/generated/claude/skills/context-os/EXAMPLES.md +21 -0
  33. package/.agents/generated/claude/skills/context-os/SKILL.md +0 -31
  34. package/.agents/generated/claude/skills/context-os/TROUBLESHOOTING.md +7 -0
  35. package/.agents/generated/claude/skills/context-os/VALIDATION.json +12 -0
  36. package/.agents/generated/claude/skills/context-os/packs.yaml +59 -0
  37. package/.agents/generated/claude/skills/context-os/references/context-rules.md +68 -0
  38. package/.agents/generated/claude/skills/context-os/references/pipeline.md +119 -0
  39. package/.agents/generated/claude/skills/context-os/references/project-graph.md +103 -0
  40. package/.agents/generated/claude/skills/context-os/rules.yaml +135 -0
  41. package/.agents/generated/claude/skills/engineering-workflow/EXAMPLES.md +57 -0
  42. package/.agents/generated/claude/skills/engineering-workflow/SKILL.md +10 -391
  43. package/.agents/generated/claude/skills/engineering-workflow/TROUBLESHOOTING.md +19 -0
  44. package/.agents/generated/claude/skills/engineering-workflow/VALIDATION.json +12 -0
  45. package/.agents/generated/claude/skills/engineering-workflow/references/workflow.md +336 -0
  46. package/.agents/generated/claude/skills/gemini-precision/EXAMPLES.md +72 -0
  47. package/.agents/generated/claude/skills/gemini-precision/SKILL.md +0 -100
  48. package/.agents/generated/claude/skills/gemini-precision/TROUBLESHOOTING.md +25 -0
  49. package/.agents/generated/claude/skills/gemini-precision/VALIDATION.json +12 -0
  50. package/.agents/generated/claude/skills/gstack-roles/EXAMPLES.md +23 -0
  51. package/.agents/generated/claude/skills/gstack-roles/SKILL.md +10 -164
  52. package/.agents/generated/claude/skills/gstack-roles/TROUBLESHOOTING.md +13 -0
  53. package/.agents/generated/claude/skills/gstack-roles/VALIDATION.json +12 -0
  54. package/.agents/generated/claude/skills/gstack-roles/references/roles.md +149 -0
  55. package/.agents/generated/claude/skills/ponytail-mindset/EXAMPLES.md +45 -0
  56. package/.agents/generated/claude/skills/ponytail-mindset/SKILL.md +12 -228
  57. package/.agents/generated/claude/skills/ponytail-mindset/TROUBLESHOOTING.md +19 -0
  58. package/.agents/generated/claude/skills/ponytail-mindset/VALIDATION.json +12 -0
  59. package/.agents/generated/claude/skills/ponytail-mindset/references/minimalism.md +186 -0
  60. package/.agents/generated/claude/skills/security/EXAMPLES.md +64 -0
  61. package/.agents/generated/claude/skills/security/SKILL.md +0 -86
  62. package/.agents/generated/claude/skills/security/TROUBLESHOOTING.md +19 -0
  63. package/.agents/generated/claude/skills/security/VALIDATION.json +12 -0
  64. package/.agents/generated/gemini/skills/context-manager/EXAMPLES.md +19 -0
  65. package/.agents/generated/gemini/skills/context-manager/SKILL.md +1 -33
  66. package/.agents/generated/gemini/skills/context-manager/TROUBLESHOOTING.md +7 -0
  67. package/.agents/generated/gemini/skills/context-manager/VALIDATION.json +12 -0
  68. package/.agents/generated/gemini/skills/context-manager/references/context-rules.md +59 -0
  69. package/.agents/generated/gemini/skills/context-os/EXAMPLES.md +21 -0
  70. package/.agents/generated/gemini/skills/context-os/SKILL.md +0 -35
  71. package/.agents/generated/gemini/skills/context-os/TROUBLESHOOTING.md +7 -0
  72. package/.agents/generated/gemini/skills/context-os/VALIDATION.json +12 -0
  73. package/.agents/generated/gemini/skills/context-os/packs.yaml +59 -0
  74. package/.agents/generated/gemini/skills/context-os/references/context-rules.md +68 -0
  75. package/.agents/generated/gemini/skills/context-os/references/pipeline.md +119 -0
  76. package/.agents/generated/gemini/skills/context-os/references/project-graph.md +103 -0
  77. package/.agents/generated/gemini/skills/context-os/rules.yaml +135 -0
  78. package/.agents/generated/gemini/skills/engineering-workflow/EXAMPLES.md +57 -0
  79. package/.agents/generated/gemini/skills/engineering-workflow/SKILL.md +11 -396
  80. package/.agents/generated/gemini/skills/engineering-workflow/TROUBLESHOOTING.md +19 -0
  81. package/.agents/generated/gemini/skills/engineering-workflow/VALIDATION.json +12 -0
  82. package/.agents/generated/gemini/skills/engineering-workflow/references/workflow.md +336 -0
  83. package/.agents/generated/gemini/skills/gemini-precision/EXAMPLES.md +72 -0
  84. package/.agents/generated/gemini/skills/gemini-precision/SKILL.md +0 -104
  85. package/.agents/generated/gemini/skills/gemini-precision/TROUBLESHOOTING.md +25 -0
  86. package/.agents/generated/gemini/skills/gemini-precision/VALIDATION.json +12 -0
  87. package/.agents/generated/gemini/skills/gstack-roles/EXAMPLES.md +23 -0
  88. package/.agents/generated/gemini/skills/gstack-roles/SKILL.md +11 -169
  89. package/.agents/generated/gemini/skills/gstack-roles/TROUBLESHOOTING.md +13 -0
  90. package/.agents/generated/gemini/skills/gstack-roles/VALIDATION.json +12 -0
  91. package/.agents/generated/gemini/skills/gstack-roles/references/roles.md +149 -0
  92. package/.agents/generated/gemini/skills/ponytail-mindset/EXAMPLES.md +45 -0
  93. package/.agents/generated/gemini/skills/ponytail-mindset/SKILL.md +13 -233
  94. package/.agents/generated/gemini/skills/ponytail-mindset/TROUBLESHOOTING.md +19 -0
  95. package/.agents/generated/gemini/skills/ponytail-mindset/VALIDATION.json +12 -0
  96. package/.agents/generated/gemini/skills/ponytail-mindset/references/minimalism.md +186 -0
  97. package/.agents/generated/gemini/skills/security/EXAMPLES.md +64 -0
  98. package/.agents/generated/gemini/skills/security/SKILL.md +2 -92
  99. package/.agents/generated/gemini/skills/security/TROUBLESHOOTING.md +19 -0
  100. package/.agents/generated/gemini/skills/security/VALIDATION.json +12 -0
  101. package/.agents/plugins.js +272 -28
  102. package/.agents/resolver/canonical-resolver.js +43 -7
  103. package/.agents/resolver/resolve-args.js +31 -0
  104. package/.agents/stats.js +8 -11
  105. package/.agents/workspace/workspace-graph.js +16 -6
  106. package/README.md +61 -6
  107. package/bin/index.js +157 -51
  108. package/bin/lib/ui.js +140 -0
  109. package/package.json +5 -2
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "properties": {
5
+ "rules_followed": {
6
+ "type": "boolean"
7
+ }
8
+ },
9
+ "required": [
10
+ "rules_followed"
11
+ ]
12
+ }
@@ -47,6 +47,25 @@ const REGISTRY_URL = 'https://raw.githubusercontent.com/kok-o/contextos-agents
47
47
  const MAX_DOWNLOAD_BYTES = 5 * 1024 * 1024;
48
48
  const SAFE_SKILL_NAME = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
49
49
 
50
+ function getCatalogDir() {
51
+ if (process.env.CONTEXTOS_CATALOG_DIR && fs.existsSync(process.env.CONTEXTOS_CATALOG_DIR)) {
52
+ return process.env.CONTEXTOS_CATALOG_DIR;
53
+ }
54
+ const localAgentsCatalog = path.join(__dirname, 'catalog', 'skills');
55
+ if (fs.existsSync(localAgentsCatalog)) return localAgentsCatalog;
56
+
57
+ const relativeCatalog = path.join(path.resolve(__dirname, '..'), 'catalog', 'skills');
58
+ if (fs.existsSync(relativeCatalog)) return relativeCatalog;
59
+
60
+ try {
61
+ const pkgPath = require.resolve('contextos-agents/package.json', { paths: [__dirname, process.cwd()] });
62
+ const pkgCatalog = path.join(path.dirname(pkgPath), 'catalog', 'skills');
63
+ if (fs.existsSync(pkgCatalog)) return pkgCatalog;
64
+ } catch {}
65
+
66
+ return null;
67
+ }
68
+
50
69
  // ── ANSI helpers ──────────────────────────────────────────────────────────────
51
70
  const NO_COLOR = process.env.NO_COLOR || !process.stdout.isTTY;
52
71
  const c = {
@@ -291,7 +310,7 @@ function parseRef(ref, projectRoot = null) {
291
310
 
292
311
  // 1. Built-in package catalog skills take strict precedence for catalog names.
293
312
  // Consumer project local files can never spoof or hijack package catalog skills.
294
- const pkgCatalogPath = path.join(path.resolve(__dirname, '..'), 'catalog', 'skills', ref);
313
+ const pkgCatalogPath = path.join(getCatalogDir(), ref);
295
314
  if (fs.existsSync(pkgCatalogPath) && fs.existsSync(path.join(pkgCatalogPath, 'SKILL.md'))) {
296
315
  return {
297
316
  type: 'catalog',
@@ -636,9 +655,137 @@ function isSafeSkillName(skillName) {
636
655
  return typeof skillName === 'string' && SAFE_SKILL_NAME.test(skillName);
637
656
  }
638
657
 
639
- // ═════════════════════════════════════════════════════════════════════════════
640
- // COMMANDS
641
- // ═════════════════════════════════════════════════════════════════════════════
658
+ function findClosestSkill(target, candidates) {
659
+ let closest = null;
660
+ let minDistance = Infinity;
661
+ for (const item of candidates) {
662
+ if (item === target) return item;
663
+ if (item.includes(target) || target.includes(item)) return item;
664
+ const dist = levenshtein(target, item);
665
+ if (dist < minDistance && dist <= 3) {
666
+ minDistance = dist;
667
+ closest = item;
668
+ }
669
+ }
670
+ return closest;
671
+ }
672
+
673
+ function levenshtein(a, b) {
674
+ const m = a.length, n = b.length;
675
+ const d = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
676
+ for (let i = 0; i <= m; i++) d[i][0] = i;
677
+ for (let j = 0; j <= n; j++) d[0][j] = j;
678
+ for (let i = 1; i <= m; i++) {
679
+ for (let j = 1; j <= n; j++) {
680
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
681
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
682
+ }
683
+ }
684
+ return d[m][n];
685
+ }
686
+
687
+ /**
688
+ * Batch install all available catalog skills.
689
+ */
690
+ async function addAllFromCatalog(options = {}) {
691
+ const dryRun = Boolean(options.dryRun || options['dry-run']);
692
+ const force = Boolean(options.force);
693
+ const catalogDir = getCatalogDir();
694
+ if (!fs.existsSync(catalogDir)) {
695
+ throw new Error('Catalog directory not found in package');
696
+ }
697
+
698
+ const catalogSkills = fs.readdirSync(catalogDir).filter(n => {
699
+ const full = path.join(catalogDir, n);
700
+ return fs.statSync(full).isDirectory() && fs.existsSync(path.join(full, 'SKILL.md'));
701
+ });
702
+
703
+ console.log('');
704
+ console.log(c.bold(`Batch installing ${catalogSkills.length} catalog skills...`));
705
+
706
+ const lock = readLock();
707
+ const existingNames = new Set(lock.plugins.map(p => p.name));
708
+
709
+ const projectLock = new ProjectMutationLock(ROOT);
710
+ const tx = new JournaledTransaction(ROOT);
711
+ let lockToken = null;
712
+
713
+ let installedCount = 0;
714
+ let skippedCount = 0;
715
+ const newlyInstalled = [];
716
+
717
+ try {
718
+ lockToken = projectLock.acquire({ command: 'skill:add-all' });
719
+
720
+ for (const skillName of catalogSkills) {
721
+ if (existingNames.has(skillName) && !force) {
722
+ skippedCount++;
723
+ continue;
724
+ }
725
+
726
+ const descriptor = {
727
+ type: 'catalog',
728
+ name: skillName,
729
+ path: path.join(catalogDir, skillName),
730
+ isPinned: true,
731
+ raw: skillName,
732
+ };
733
+
734
+ const installedSha256 = installFromCatalog(descriptor, skillName, dryRun, null, false, tx);
735
+
736
+ if (!dryRun) {
737
+ const idx = lock.plugins.findIndex(p => p.name === skillName);
738
+ const entry = {
739
+ name: skillName,
740
+ ref: skillName,
741
+ type: 'catalog',
742
+ sha256: installedSha256,
743
+ installedAt: new Date().toISOString(),
744
+ };
745
+ if (idx >= 0) lock.plugins[idx] = entry;
746
+ else lock.plugins.push(entry);
747
+ newlyInstalled.push(skillName);
748
+ }
749
+ installedCount++;
750
+ }
751
+
752
+ if (!dryRun) {
753
+ writeLock(lock, tx);
754
+ tx.commit();
755
+
756
+ for (const skillName of newlyInstalled) {
757
+ const targetDir = path.join(PLUGINS_DIR, skillName);
758
+ if (fs.existsSync(targetDir)) {
759
+ const digestResult = calculateTreeDigest(targetDir);
760
+ const entry = lock.plugins.find(p => p.name === skillName);
761
+ if (entry) entry.treeDigest = digestResult.treeDigest;
762
+ }
763
+ }
764
+ writeLock(lock);
765
+ }
766
+
767
+ console.log('');
768
+ if (dryRun) {
769
+ console.log(c.dim(`[DRY-RUN] Would install ${installedCount} skills (${skippedCount} already installed - skipped).`));
770
+ } else {
771
+ console.log(c.green(c.bold(`✓ Batch install complete: ${installedCount} catalog skills installed.`)));
772
+ if (skippedCount > 0) {
773
+ console.log(c.dim(` (${skippedCount} skills already installed - skipped)`));
774
+ }
775
+ console.log('');
776
+ console.log(' Next steps:');
777
+ console.log(' node .agents/ctx.js export all # recompile with new skills');
778
+ console.log(' node .agents/ctx.js validate # verify consistency');
779
+ }
780
+
781
+ return { installedCount, skippedCount };
782
+ } catch (err) {
783
+ console.error(c.red(`\n[ERROR] Batch installation failed: ${err.message}`));
784
+ process.exit(1);
785
+ } finally {
786
+ if (lockToken) projectLock.release(lockToken);
787
+ }
788
+ }
642
789
 
643
790
  /**
644
791
  * skill add <ref> [--dry-run] [--checksum <sha256>] [--force-unsafe-prompts]
@@ -650,6 +797,10 @@ async function add(ref, options = {}) {
650
797
  const allowFloating = Boolean(options.allowFloating || options['allow-floating']);
651
798
  const force = Boolean(options.force);
652
799
 
800
+ if (ref === '--all' || ref === 'all' || options.all) {
801
+ return await addAllFromCatalog(options);
802
+ }
803
+
653
804
  if (!ref) {
654
805
  console.error(c.red('Usage: ctx.js skill add <ref> [--checksum <sha256>]'));
655
806
  console.error(' ref can be: username/repo, username/repo/path/to/skill, or npm-package-name');
@@ -663,7 +814,37 @@ async function add(ref, options = {}) {
663
814
  throw new Error(`Invalid plugin name derived from reference: '${skillName}'`);
664
815
  }
665
816
 
666
- // Supply-chain source pinning check (Wave 6)
817
+ // 1. Check for conflict with builtin skills first
818
+ const builtinPath = path.join(CORE_SKILLS, skillName);
819
+ if (fs.existsSync(builtinPath)) {
820
+ console.error(c.red(`\n[ERROR] '${skillName}' conflicts with a built-in skill.`));
821
+ console.error(c.red(' Built-in skills cannot be overridden via --add-skill.'));
822
+ console.error(c.dim(` To customize a built-in skill for your project, run: contextos skill override ${skillName}`));
823
+ process.exit(1);
824
+ }
825
+
826
+ // 2. If single word not in catalog, check for typo suggestion
827
+ if (descriptor.type !== 'catalog' && !ref.includes('/') && !ref.includes('@')) {
828
+ const catalogDir = getCatalogDir();
829
+ if (fs.existsSync(catalogDir)) {
830
+ const catalogSkills = fs.readdirSync(catalogDir).filter(n => {
831
+ try {
832
+ return fs.statSync(path.join(catalogDir, n)).isDirectory();
833
+ } catch {
834
+ return false;
835
+ }
836
+ });
837
+ const suggestion = findClosestSkill(skillName, catalogSkills);
838
+ if (suggestion) {
839
+ console.error(c.red(`\n[ERROR] Skill '${skillName}' not found in catalog.`));
840
+ console.log(c.yellow(` Did you mean: "${suggestion}"?`));
841
+ console.log(c.dim(' Run "contextos skill list --available" to see all catalog skills.'));
842
+ process.exit(1);
843
+ }
844
+ }
845
+ }
846
+
847
+ // 3. Supply-chain source pinning check (Wave 6)
667
848
  if (!dryRun && descriptor.type !== 'catalog') {
668
849
  const pinning = validatePluginPinning(
669
850
  {
@@ -688,15 +869,6 @@ async function add(ref, options = {}) {
688
869
  console.log(c.bold(`Installing skill: ${c.cyan(skillName)}`));
689
870
  console.log(c.dim(` Source: ${ref} (${descriptor.type})`));
690
871
 
691
- // Check for conflict with builtin skills
692
- const builtinPath = path.join(CORE_SKILLS, skillName);
693
- if (fs.existsSync(builtinPath)) {
694
- console.error(c.red(`\n[ERROR] '${skillName}' conflicts with a built-in skill.`));
695
- console.error(c.red(' Built-in skills cannot be overridden via --add-skill.'));
696
- console.error(c.dim(' To customize a built-in skill, fork the repo and submit a PR.'));
697
- process.exit(1);
698
- }
699
-
700
872
  // Check if already installed
701
873
  const lock = readLock();
702
874
  const existing = lock.plugins.find(p => p.name === skillName);
@@ -779,6 +951,21 @@ async function add(ref, options = {}) {
779
951
  // We don't need to do anything since it wasn't committed
780
952
  }
781
953
  console.error(c.red(`\n[ERROR] Failed to install '${skillName}': ${err.message}`));
954
+ const catalogDir = getCatalogDir();
955
+ if (fs.existsSync(catalogDir)) {
956
+ const catalogSkills = fs.readdirSync(catalogDir).filter(n => {
957
+ try {
958
+ return fs.statSync(path.join(catalogDir, n)).isDirectory();
959
+ } catch {
960
+ return false;
961
+ }
962
+ });
963
+ const suggestion = findClosestSkill(skillName, catalogSkills);
964
+ if (suggestion) {
965
+ console.log(c.yellow(` Did you mean: "${suggestion}"?`));
966
+ console.log(c.dim(' Run "contextos skill list --available" to see all catalog skills.'));
967
+ }
968
+ }
782
969
  process.exit(1);
783
970
  } finally {
784
971
  if (lockToken) projectLock.release(lockToken);
@@ -898,25 +1085,46 @@ function remove(skillName) {
898
1085
  }
899
1086
 
900
1087
  /**
901
- * skill list
1088
+ * skill list [options]
902
1089
  */
903
- function list() {
1090
+ function list(options = {}) {
1091
+ const showAvailable = Boolean(options.available || options['--available'] || options.all);
904
1092
  const lock = readLock();
905
1093
 
906
1094
  // Builtin skills
907
1095
  const builtins = fs.existsSync(CORE_SKILLS)
908
- ? fs.readdirSync(CORE_SKILLS).filter(n => fs.statSync(path.join(CORE_SKILLS, n)).isDirectory())
1096
+ ? fs.readdirSync(CORE_SKILLS).filter(n => {
1097
+ try {
1098
+ return fs.statSync(path.join(CORE_SKILLS, n)).isDirectory();
1099
+ } catch {
1100
+ return false;
1101
+ }
1102
+ })
909
1103
  : [];
910
1104
 
911
1105
  // Plugin skills
912
1106
  const plugins = lock.plugins;
913
1107
 
1108
+ // Catalog skills
1109
+ const catalogDir = getCatalogDir();
1110
+ const catalogSkills = fs.existsSync(catalogDir)
1111
+ ? fs.readdirSync(catalogDir).filter(n => {
1112
+ try {
1113
+ return fs.statSync(path.join(catalogDir, n)).isDirectory();
1114
+ } catch {
1115
+ return false;
1116
+ }
1117
+ })
1118
+ : [];
1119
+ const installedNames = new Set(plugins.map(p => p.name));
1120
+ const availableCatalog = catalogSkills.filter(n => !installedNames.has(n));
1121
+
914
1122
  console.log('');
915
1123
  console.log(c.bold('ContextOS Skills'));
916
1124
  console.log('');
917
1125
 
918
1126
  // Built-ins
919
- console.log(c.bold(` Built-in (${builtins.length})`));
1127
+ console.log(c.bold(` Built-in Core (${builtins.length})`));
920
1128
  for (const name of builtins) {
921
1129
  console.log(` ${c.cyan('●')} ${name} ${c.dim('(core)')}`);
922
1130
  }
@@ -924,22 +1132,39 @@ function list() {
924
1132
  // Plugins
925
1133
  console.log('');
926
1134
  if (plugins.length === 0) {
927
- console.log(c.bold(' Plugins (0)'));
928
- console.log(c.dim(' No plugins installed yet.'));
929
- console.log(c.dim(' Install one: node .agents/ctx.js skill add username/my-skill'));
1135
+ console.log(c.bold(' Installed Plugins & Domain Skills (0)'));
1136
+ console.log(c.dim(' No domain skills or plugins installed yet.'));
1137
+ console.log(c.dim(' Install one: contextos skill add <name> (or --all)'));
930
1138
  } else {
931
- console.log(c.bold(` Plugins (${plugins.length})`));
1139
+ console.log(c.bold(` Installed Plugins & Domain Skills (${plugins.length})`));
932
1140
  for (const p of plugins) {
933
1141
  const when = p.installedAt ? new Date(p.installedAt).toLocaleDateString() : '?';
934
1142
  console.log(` ${c.green('●')} ${p.name} ${c.dim(`← ${p.ref} (installed ${when})`)}`);
935
1143
  }
936
1144
  }
937
1145
 
1146
+ // Available in Catalog
1147
+ console.log('');
1148
+ if (showAvailable || availableCatalog.length > 0) {
1149
+ console.log(c.bold(` Available in Catalog (${availableCatalog.length})`));
1150
+ if (showAvailable) {
1151
+ for (const name of availableCatalog) {
1152
+ console.log(` ${c.dim('○')} ${name}`);
1153
+ }
1154
+ console.log('');
1155
+ console.log(c.dim(' Install with: contextos skill add <name> (or contextos skill add --all)'));
1156
+ } else {
1157
+ console.log(c.dim(` ${availableCatalog.length} domain skills available in local catalog.`));
1158
+ console.log(c.dim(' Run "contextos skill list --available" to see all, or install with:'));
1159
+ console.log(c.dim(' contextos skill add <name> | contextos skill add --all'));
1160
+ }
1161
+ }
1162
+
938
1163
  console.log('');
939
1164
  }
940
1165
 
941
1166
  /**
942
- * skill search <query> — searches the hosted registry.json
1167
+ * skill search <query> - searches the hosted registry.json
943
1168
  */
944
1169
  async function search(query) {
945
1170
  console.log(c.dim(`\nFetching registry from: ${REGISTRY_URL}\n`));
@@ -996,11 +1221,11 @@ async function search(query) {
996
1221
  * both core (builtin) and plugins.
997
1222
  * Adapters call this instead of reading CORE_SKILLS directly.
998
1223
  */
999
- function collectAllSkillDirs(targetRoot) {
1224
+ function collectAllSkillDirs(targetRoot, options = {}) {
1000
1225
  const isTargetExplicit = Boolean(targetRoot);
1001
1226
  const root = targetRoot || process.cwd();
1002
- const agentsDir = path.join(root, '.agents');
1003
- const localCore = path.join(agentsDir, 'core', 'skills');
1227
+ const agentsDir = options.agentsDir || path.join(root, '.agents');
1228
+ const localCore = options.coreSkillsDir || path.join(agentsDir, 'core', 'skills');
1004
1229
  const localPlugins = path.join(agentsDir, 'plugins');
1005
1230
 
1006
1231
  const coreDir = fs.existsSync(localCore) ? localCore : (isTargetExplicit ? null : CORE_SKILLS);
@@ -1027,13 +1252,32 @@ function collectAllSkillDirs(targetRoot) {
1027
1252
  const subDir = path.join(nestedSkills, sub);
1028
1253
  if (fs.statSync(subDir).isDirectory()) dirs.push(subDir);
1029
1254
  }
1030
- } else if (fs.existsSync(path.join(d, 'SKILL.md')) || fs.existsSync(path.join(d, 'skill.yaml'))) {
1255
+ } else if (fs.existsSync(path.join(d, 'SKILL.md')) || fs.existsSync(path.join(d, 'skill.yaml')) || fs.existsSync(path.join(d, 'skill.v2.yaml'))) {
1031
1256
  dirs.push(d);
1032
1257
  }
1033
1258
  }
1034
1259
  }
1035
1260
 
1036
- return dirs;
1261
+ // Higher ownership tiers replace an upstream skill with the same ID. Preserve
1262
+ // upstream duplicates so the compiler can still diagnose conflicting installs.
1263
+ for (const tier of ['vendor', 'project']) {
1264
+ const tierRoot = path.join(agentsDir, tier, 'skills');
1265
+ if (!fs.existsSync(tierRoot)) continue;
1266
+ for (const name of fs.readdirSync(tierRoot).sort()) {
1267
+ const dir = path.join(tierRoot, name);
1268
+ const { resolvedPath } = require('./filesystem/index.js').resolveManagedPath(root, path.relative(root, dir));
1269
+ if (!fs.statSync(resolvedPath).isDirectory()) continue;
1270
+ if (!fs.existsSync(path.join(dir, 'SKILL.md')) && !fs.existsSync(path.join(dir, 'skill.yaml')) && !fs.existsSync(path.join(dir, 'skill.v2.yaml'))) continue;
1271
+ for (let i = dirs.length - 1; i >= 0; i--) {
1272
+ if (path.basename(dirs[i]) === name) dirs.splice(i, 1);
1273
+ }
1274
+ dirs.push(dir);
1275
+ }
1276
+ }
1277
+ if (isTargetExplicit) {
1278
+ for (const dir of dirs) require('./filesystem/index.js').resolveManagedPath(root, path.relative(root, dir));
1279
+ }
1280
+ return dirs.sort();
1037
1281
  }
1038
1282
 
1039
1283
  module.exports = {
@@ -650,6 +650,12 @@ class CanonicalResolver {
650
650
  }
651
651
  }
652
652
 
653
+ if (resolvedId === 'security' && ['keyword', 'file_glob', 'explicit', 'safety_required'].includes(kind)) {
654
+ if (profileExcluded.delete(resolvedId)) {
655
+ warnings.push({ code: 'CTX_REQUIRED_SECURITY_PROFILE_CONFLICT', message: 'Task safety guidance overrides the profile exclusion of security.' });
656
+ }
657
+ kind = 'safety_required';
658
+ }
653
659
  if (profileExcluded.has(resolvedId)) return;
654
660
  if (!evidenceBySkill.has(resolvedId)) evidenceBySkill.set(resolvedId, []);
655
661
  evidenceBySkill.get(resolvedId).push({ kind, weight, reason });
@@ -836,6 +842,10 @@ class CanonicalResolver {
836
842
  }
837
843
  }
838
844
 
845
+ if (risk.value === 'high' || risk.value === 'destructive') {
846
+ addEvidence('security', 'safety_required', 100, `Required safety guidance for ${risk.value} risk task`);
847
+ }
848
+
839
849
  // 3. Compute Candidate Scores & Intent Precedence
840
850
  const candidates = [];
841
851
  for (const [id, evList] of evidenceBySkill.entries()) {
@@ -850,7 +860,7 @@ class CanonicalResolver {
850
860
  if ((e.kind === 'keyword' || e.kind === 'alias') && e.weight >= 20) {
851
861
  hasDirectTaskIntent = true;
852
862
  }
853
- if (e.kind === 'explicit' || e.kind === 'profile_required') {
863
+ if (e.kind === 'explicit' || e.kind === 'profile_required' || e.kind === 'safety_required') {
854
864
  isMandatory = true;
855
865
  hasDirectTaskIntent = true;
856
866
  }
@@ -929,7 +939,7 @@ class CanonicalResolver {
929
939
  const closureMap = new Map();
930
940
  for (const item of initialSelection) {
931
941
  const rawEst = skillsDict[item.id]?.estimatedTokens || 1000;
932
- const estTokens = Math.min(rawEst, BUDGET_TIERS.SKILL_BODY);
942
+ const estTokens = rawEst;
933
943
  closureMap.set(item.id, {
934
944
  id: item.id,
935
945
  displayName: skillsDict[item.id]?.displayName || item.id,
@@ -953,7 +963,7 @@ class CanonicalResolver {
953
963
  }
954
964
  } else {
955
965
  const rawEst = skillsDict[reqId]?.estimatedTokens || 1000;
956
- const estTokens = Math.min(rawEst, BUDGET_TIERS.SKILL_BODY);
966
+ const estTokens = rawEst;
957
967
  closureMap.set(reqId, {
958
968
  id: reqId,
959
969
  displayName: skillsDict[reqId]?.displayName || reqId,
@@ -1000,7 +1010,12 @@ class CanonicalResolver {
1000
1010
  }
1001
1011
 
1002
1012
  // 7. Token Budget Planner & Capability Clustering
1003
- const BASE_SKILLS = ['ponytail-mindset', 'engineering-workflow'];
1013
+ const plannedPhase = resolvePhaseAndRole(explicitPhase, task, []).phase.value;
1014
+ const BASE_SKILLS = ['engineering-workflow'];
1015
+ if (risk.value !== 'routine' && plannedPhase === 'Build') BASE_SKILLS.push('ponytail-mindset');
1016
+ for (let i = BASE_SKILLS.length - 1; i >= 0; i--) {
1017
+ if (!skillsDict[BASE_SKILLS[i]]) BASE_SKILLS.splice(i, 1);
1018
+ }
1004
1019
  let baseTokens = 0;
1005
1020
 
1006
1021
  for (const baseId of BASE_SKILLS) {
@@ -1009,7 +1024,8 @@ class CanonicalResolver {
1009
1024
  }
1010
1025
  }
1011
1026
 
1012
- let allocatedDynamicTokens = 0;
1027
+ // Foundation bodies occupy the same budget as task-specific skills.
1028
+ let allocatedDynamicTokens = baseTokens;
1013
1029
  const admitted = new Map(); // id -> candidate entry
1014
1030
 
1015
1031
  // Separate mandatory vs optional candidates
@@ -1017,7 +1033,7 @@ class CanonicalResolver {
1017
1033
  const optionalCandidates = [];
1018
1034
 
1019
1035
  for (const cand of closureMap.values()) {
1020
- const isMandatory = cand.reasons.some(r => r.kind === 'explicit' || r.kind === 'profile_required');
1036
+ const isMandatory = cand.reasons.some(r => r.kind === 'explicit' || r.kind === 'profile_required' || r.kind === 'safety_required');
1021
1037
  if (isMandatory) {
1022
1038
  mandatoryCandidates.push(cand);
1023
1039
  } else {
@@ -1131,6 +1147,25 @@ class CanonicalResolver {
1131
1147
  }
1132
1148
  }
1133
1149
 
1150
+ const totalSelectedTokens = finalSelected.reduce((sum, skill) => sum + skill.estimatedTokens, 0);
1151
+ for (const skill of finalSelected) {
1152
+ if (!skillsDict[skill.id]) {
1153
+ warnings.push({ code: 'CTX_SELECTED_SKILL_UNAVAILABLE', message: `Selected skill "${skill.id}" has no installed manifest. Install it before assembling runtime context.` });
1154
+ }
1155
+ }
1156
+ if (finalSelected.length > maxSkillsLimit) {
1157
+ warnings.push({ code: 'CTX_RESOLVER_SKILL_LIMIT_EXCEEDED', message: `Required skills and dependencies occupy ${finalSelected.length} slots; soft maxSkills limit is ${maxSkillsLimit}.` });
1158
+ }
1159
+ if (finalSelected.length > maxSkillsLimit) {
1160
+ warnings.push({ code: 'CTX_RESOLVER_SKILL_LIMIT_EXCEEDED', message: `Required skills and dependencies occupy ${finalSelected.length} slots; soft maxSkills limit is ${maxSkillsLimit}.` });
1161
+ }
1162
+ if (totalSelectedTokens > budgetTokens && !warnings.some(w => w.code === 'CTX_RESOLVER_BUDGET_EXCEEDED')) {
1163
+ warnings.push({
1164
+ code: 'CTX_RESOLVER_BUDGET_EXCEEDED',
1165
+ message: `Soft context budget (${budgetTokens} tokens) exceeded by required/foundation skill bodies (total: ${totalSelectedTokens} tokens). Client instructions and chat history are additional.`,
1166
+ });
1167
+ }
1168
+
1134
1169
  // 9. Domain, Phase & Role
1135
1170
  let domain = request.domain || '';
1136
1171
  if (!domain) {
@@ -1208,7 +1243,7 @@ class CanonicalResolver {
1208
1243
  excluded,
1209
1244
  conflicts,
1210
1245
  warnings,
1211
- totalEstimatedTokens: baseTokens + allocatedDynamicTokens,
1246
+ totalEstimatedTokens: totalSelectedTokens,
1212
1247
  // Legacy compatibility properties
1213
1248
  skills: allSelectedIds,
1214
1249
  };
@@ -1229,6 +1264,7 @@ class CanonicalResolver {
1229
1264
  const lines = [
1230
1265
  `[DOMAIN: ${res.domain}] [PHASE: ${phaseStr}] [ROLE: ${res.role}] [MODE: ${modeStr}] [LENSES: ${lensesStr}] [RISK: ${riskStr}]`,
1231
1266
  `Skills loaded: ${skillsList}`,
1267
+ 'Selection declaration only; skill bodies are read by the consuming client or prompt assembler.',
1232
1268
  ];
1233
1269
  if (res.workflow && res.workflow.steps && res.workflow.steps.length > 0) {
1234
1270
  lines.push(`Workflow (${res.workflow.name}): ${res.workflow.steps[0]}`);
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ function parseResolveArgs(args) {
4
+ const result = { task: '', files: [], explicitPhase: undefined, contextBudgetTokens: undefined, json: false, explain: false };
5
+ const task = [];
6
+ for (let i = 0; i < args.length; i++) {
7
+ const arg = args[i];
8
+ if (arg === '--json') result.json = true;
9
+ else if (arg === '--explain') result.explain = true;
10
+ else if (['--files', '--phase', '--budget'].includes(arg)) {
11
+ const value = args[++i];
12
+ if (!value || value.startsWith('--')) throw new Error(`Missing value for ${arg}`);
13
+ if (arg === '--files') result.files = value.split(',').map(file => file.trim()).filter(Boolean);
14
+ else if (arg === '--phase') result.explicitPhase = value;
15
+ else {
16
+ if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value))) {
17
+ throw new Error('--budget must be a positive integer');
18
+ }
19
+ result.contextBudgetTokens = Number(value);
20
+ }
21
+ } else if (arg === '--') {
22
+ task.push(...args.slice(i + 1));
23
+ break;
24
+ } else if (arg.startsWith('-')) throw new Error(`Unknown resolve option: ${arg}`);
25
+ else task.push(arg);
26
+ }
27
+ result.task = task.join(' ');
28
+ return result;
29
+ }
30
+
31
+ module.exports = { parseResolveArgs };
package/.agents/stats.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * 1. Full context payload (all skills + AGENTS.md)
7
7
  * 2. Profiled context payload (after applying active profile exclusions)
8
8
  * 3. Dynamically resolved context (lean on-demand skill set for typical tasks)
9
- * 4. Concrete token savings percentage and cost reductions
9
+ * 4. Character-based payload estimates, not provider billing or quality results
10
10
  */
11
11
 
12
12
  'use strict';
@@ -22,7 +22,6 @@ function estimateTokens(text) {
22
22
 
23
23
  function calculateContextStats(projectDir = process.cwd()) {
24
24
  const agentsDir = path.join(projectDir, '.agents');
25
- const skillsDir = path.join(agentsDir, 'core', 'skills');
26
25
  const agentsMdPath = path.join(agentsDir, 'AGENTS.md');
27
26
 
28
27
  let agentsMdChars = 0;
@@ -31,15 +30,13 @@ function calculateContextStats(projectDir = process.cwd()) {
31
30
  }
32
31
 
33
32
  const skillCharMap = new Map();
34
- if (fs.existsSync(skillsDir)) {
35
- const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
36
- for (const entry of entries) {
37
- if (!entry.isDirectory()) continue;
38
- const skillMd = path.join(skillsDir, entry.name, 'SKILL.md');
33
+ const { collectSkillDirectories } = require('./adapters/shared.js');
34
+ const allDirs = collectSkillDirectories({ exclude_skills: [] }, projectDir);
35
+ for (const directory of allDirs) {
36
+ const skillMd = path.join(directory, 'SKILL.md');
39
37
  if (fs.existsSync(skillMd)) {
40
- skillCharMap.set(entry.name, fs.readFileSync(skillMd, 'utf8').length);
38
+ skillCharMap.set(path.basename(directory), fs.readFileSync(skillMd, 'utf8').length);
41
39
  }
42
- }
43
40
  }
44
41
 
45
42
  const totalSkillCount = skillCharMap.size;
@@ -50,9 +47,9 @@ function calculateContextStats(projectDir = process.cwd()) {
50
47
 
51
48
  // Active profile
52
49
  const activeProfile = profiles.getActiveProfile(projectDir);
53
- const profileName = activeProfile ? (activeProfile.name || activeProfile.profile) : 'startup (recommended)';
50
+ const profileName = activeProfile ? (activeProfile.name || activeProfile.profile) : 'none';
54
51
  const excludedSkills = new Set(
55
- activeProfile ? activeProfile.exclude_skills || [] : (profiles.getProfile('startup')?.exclude_skills || [])
52
+ activeProfile ? activeProfile.exclude_skills || [] : []
56
53
  );
57
54
 
58
55
  let profileChars = agentsMdChars;
@@ -23,6 +23,8 @@ const DEFAULT_IGNORED_DIRS = new Set([
23
23
  '.git',
24
24
  'node_modules',
25
25
  '.contextos-worktrees',
26
+ '.swarm-worktrees',
27
+ '.external-skills',
26
28
  '.agents',
27
29
  'dist',
28
30
  'build',
@@ -121,6 +123,7 @@ class WorkspaceGraphBuilder {
121
123
  this.maxDepth = options.maxDepth || 8;
122
124
  this.maxPackages = options.maxPackages || 500;
123
125
  this.ignoredDirs = new Set([...DEFAULT_IGNORED_DIRS, ...(options.ignoredDirs || [])]);
126
+ this.includeNestedRepositories = options.includeNestedRepositories === true;
124
127
  }
125
128
 
126
129
  /**
@@ -223,7 +226,11 @@ class WorkspaceGraphBuilder {
223
226
  return;
224
227
  }
225
228
 
226
- for (const entry of entries) {
229
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
230
+ if (packages.length >= this.maxPackages) {
231
+ hitLimit = true;
232
+ break;
233
+ }
227
234
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
228
235
  if (this.ignoredDirs.has(entry.name)) continue;
229
236
 
@@ -236,10 +243,8 @@ class WorkspaceGraphBuilder {
236
243
  }
237
244
 
238
245
  // Symlink escape guard: must remain within root realpath
239
- if (
240
- !realEntryPath.startsWith(visitedDirs.values().next().value) &&
241
- !realEntryPath.startsWith(realRoot)
242
- ) {
246
+ const realRelative = path.relative(realRoot, realEntryPath);
247
+ if (realRelative === '..' || realRelative.startsWith(`..${path.sep}`) || path.isAbsolute(realRelative)) {
243
248
  continue;
244
249
  }
245
250
 
@@ -253,7 +258,12 @@ class WorkspaceGraphBuilder {
253
258
  workspaceGlobs.length === 0 ||
254
259
  workspaceGlobs.some((g) => g.test(relPath));
255
260
 
256
- const pkg = this._inspectDirectoryForPackage(fullPath, relPath, realRoot);
261
+ if (fs.existsSync(path.join(fullPath, '.git')) &&
262
+ !this.includeNestedRepositories && !(workspaceGlobs.length > 0 && isDeclared)) {
263
+ continue;
264
+ }
265
+
266
+ const pkg = isDeclared ? this._inspectDirectoryForPackage(fullPath, relPath, realRoot) : null;
257
267
  if (pkg) {
258
268
  packages.push(pkg);
259
269
  packageIdSet.add(pkg.id);