contextos-agents 2.1.0 → 2.1.1

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 (191) hide show
  1. package/.agents/adapters/aider/export.js +2 -2
  2. package/.agents/adapters/claude/export.js +53 -2
  3. package/.agents/adapters/drift-detector.js +6 -3
  4. package/.agents/adapters/pure-compiler.js +18 -6
  5. package/.agents/ctx.js +4 -4
  6. package/.agents/plugins.js +100 -4
  7. package/.agents/profiles.js +32 -11
  8. package/README.md +2 -2
  9. package/bin/commands/hook.js +50 -12
  10. package/bin/commands/scan.js +10 -3
  11. package/bin/index.js +8 -2
  12. package/bin/lib/git-snapshot.js +70 -43
  13. package/bin/lib/scan.js +108 -27
  14. package/catalog/skills/adapters/EXAMPLES.md +19 -0
  15. package/catalog/skills/adapters/SKILL.md +101 -0
  16. package/catalog/skills/adapters/TROUBLESHOOTING.md +7 -0
  17. package/catalog/skills/adapters/VALIDATION.json +12 -0
  18. package/catalog/skills/adapters/skill.yaml +13 -0
  19. package/catalog/skills/api-design/EXAMPLES.md +91 -0
  20. package/catalog/skills/api-design/SKILL.md +63 -0
  21. package/catalog/skills/api-design/TROUBLESHOOTING.md +54 -0
  22. package/catalog/skills/api-design/VALIDATION.json +11 -0
  23. package/catalog/skills/api-design/skill.yaml +14 -0
  24. package/catalog/skills/architecture-diagrams/SKILL.md +108 -0
  25. package/catalog/skills/architecture-diagrams/VALIDATION.json +12 -0
  26. package/catalog/skills/architecture-diagrams/skill.yaml +9 -0
  27. package/catalog/skills/brutalist-design/EXAMPLES.md +59 -0
  28. package/catalog/skills/brutalist-design/SKILL.md +150 -0
  29. package/catalog/skills/brutalist-design/VALIDATION.json +12 -0
  30. package/catalog/skills/brutalist-design/skill.yaml +10 -0
  31. package/catalog/skills/ci-cd/EXAMPLES.md +79 -0
  32. package/catalog/skills/ci-cd/SKILL.md +69 -0
  33. package/catalog/skills/ci-cd/TROUBLESHOOTING.md +52 -0
  34. package/catalog/skills/ci-cd/VALIDATION.json +11 -0
  35. package/catalog/skills/ci-cd/skill.yaml +13 -0
  36. package/catalog/skills/database/EXAMPLES.md +74 -0
  37. package/catalog/skills/database/SKILL.md +101 -0
  38. package/catalog/skills/database/TROUBLESHOOTING.md +18 -0
  39. package/catalog/skills/database/VALIDATION.json +11 -0
  40. package/catalog/skills/database/skill.yaml +14 -0
  41. package/catalog/skills/ddd/EXAMPLES.md +42 -0
  42. package/catalog/skills/ddd/SKILL.md +247 -0
  43. package/catalog/skills/ddd/TROUBLESHOOTING.md +19 -0
  44. package/catalog/skills/ddd/VALIDATION.json +12 -0
  45. package/catalog/skills/ddd/skill.yaml +14 -0
  46. package/catalog/skills/decisions/EXAMPLES.md +35 -0
  47. package/catalog/skills/decisions/SKILL.md +90 -0
  48. package/catalog/skills/decisions/TROUBLESHOOTING.md +13 -0
  49. package/catalog/skills/decisions/VALIDATION.json +12 -0
  50. package/catalog/skills/decisions/skill.yaml +13 -0
  51. package/catalog/skills/docker/EXAMPLES.md +56 -0
  52. package/catalog/skills/docker/SKILL.md +169 -0
  53. package/catalog/skills/docker/TROUBLESHOOTING.md +18 -0
  54. package/catalog/skills/docker/VALIDATION.json +11 -0
  55. package/catalog/skills/docker/skill.yaml +13 -0
  56. package/catalog/skills/fastapi/EXAMPLES.md +36 -0
  57. package/catalog/skills/fastapi/SKILL.md +171 -0
  58. package/catalog/skills/fastapi/TROUBLESHOOTING.md +19 -0
  59. package/catalog/skills/fastapi/VALIDATION.json +12 -0
  60. package/catalog/skills/fastapi/skill.yaml +14 -0
  61. package/catalog/skills/generators/EXAMPLES.md +19 -0
  62. package/catalog/skills/generators/SKILL.md +110 -0
  63. package/catalog/skills/generators/TROUBLESHOOTING.md +7 -0
  64. package/catalog/skills/generators/VALIDATION.json +12 -0
  65. package/catalog/skills/generators/skill.yaml +22 -0
  66. package/catalog/skills/generators/templates/API.md +77 -0
  67. package/catalog/skills/generators/templates/ARCHITECTURE.md +70 -0
  68. package/catalog/skills/generators/templates/DATABASE.md +42 -0
  69. package/catalog/skills/generators/templates/DECISION.md +46 -0
  70. package/catalog/skills/generators/templates/PRD.md +67 -0
  71. package/catalog/skills/generators/templates/PROJECT_GRAPH.md +56 -0
  72. package/catalog/skills/generators/templates/ROADMAP.md +51 -0
  73. package/catalog/skills/generators/templates/TASKS.md +43 -0
  74. package/catalog/skills/generators/templates/UI.md +73 -0
  75. package/catalog/skills/graphify/EXAMPLES.md +73 -0
  76. package/catalog/skills/graphify/SKILL.md +130 -0
  77. package/catalog/skills/graphify/VALIDATION.json +12 -0
  78. package/catalog/skills/graphify/skill.yaml +13 -0
  79. package/catalog/skills/impeccable-design/EXAMPLES.md +26 -0
  80. package/catalog/skills/impeccable-design/SKILL.md +201 -0
  81. package/catalog/skills/impeccable-design/TROUBLESHOOTING.md +19 -0
  82. package/catalog/skills/impeccable-design/VALIDATION.json +12 -0
  83. package/catalog/skills/impeccable-design/skill.yaml +15 -0
  84. package/catalog/skills/interview-me/SKILL.md +97 -0
  85. package/catalog/skills/interview-me/VALIDATION.json +12 -0
  86. package/catalog/skills/interview-me/skill.yaml +9 -0
  87. package/catalog/skills/microservices/EXAMPLES.md +38 -0
  88. package/catalog/skills/microservices/SKILL.md +164 -0
  89. package/catalog/skills/microservices/TROUBLESHOOTING.md +19 -0
  90. package/catalog/skills/microservices/VALIDATION.json +12 -0
  91. package/catalog/skills/microservices/skill.yaml +14 -0
  92. package/catalog/skills/minimalist-design/EXAMPLES.md +58 -0
  93. package/catalog/skills/minimalist-design/SKILL.md +113 -0
  94. package/catalog/skills/minimalist-design/VALIDATION.json +12 -0
  95. package/catalog/skills/minimalist-design/skill.yaml +10 -0
  96. package/catalog/skills/nestjs/EXAMPLES.md +40 -0
  97. package/catalog/skills/nestjs/SKILL.md +139 -0
  98. package/catalog/skills/nestjs/TROUBLESHOOTING.md +19 -0
  99. package/catalog/skills/nestjs/VALIDATION.json +12 -0
  100. package/catalog/skills/nestjs/skill.yaml +14 -0
  101. package/catalog/skills/nextjs/EXAMPLES.md +40 -0
  102. package/catalog/skills/nextjs/SKILL.md +163 -0
  103. package/catalog/skills/nextjs/TROUBLESHOOTING.md +19 -0
  104. package/catalog/skills/nextjs/VALIDATION.json +12 -0
  105. package/catalog/skills/nextjs/skill.yaml +14 -0
  106. package/catalog/skills/node/EXAMPLES.md +80 -0
  107. package/catalog/skills/node/SKILL.md +128 -0
  108. package/catalog/skills/node/TROUBLESHOOTING.md +19 -0
  109. package/catalog/skills/node/VALIDATION.json +12 -0
  110. package/catalog/skills/node/skill.yaml +14 -0
  111. package/catalog/skills/performance/EXAMPLES.md +30 -0
  112. package/catalog/skills/performance/SKILL.md +75 -0
  113. package/catalog/skills/performance/TROUBLESHOOTING.md +19 -0
  114. package/catalog/skills/performance/VALIDATION.json +12 -0
  115. package/catalog/skills/performance/skill.yaml +14 -0
  116. package/catalog/skills/react/EXAMPLES.md +79 -0
  117. package/catalog/skills/react/SKILL.md +132 -0
  118. package/catalog/skills/react/TROUBLESHOOTING.md +19 -0
  119. package/catalog/skills/react/VALIDATION.json +12 -0
  120. package/catalog/skills/react/skill.yaml +14 -0
  121. package/catalog/skills/react-best-practices/SKILL.md +158 -0
  122. package/catalog/skills/react-best-practices/VALIDATION.json +12 -0
  123. package/catalog/skills/react-best-practices/skill.yaml +13 -0
  124. package/catalog/skills/redesign-audit/SKILL.md +117 -0
  125. package/catalog/skills/redesign-audit/VALIDATION.json +12 -0
  126. package/catalog/skills/redesign-audit/skill.yaml +9 -0
  127. package/catalog/skills/security-audit/EXAMPLES.md +79 -0
  128. package/catalog/skills/security-audit/SKILL.md +91 -0
  129. package/catalog/skills/security-audit/TROUBLESHOOTING.md +46 -0
  130. package/catalog/skills/security-audit/VALIDATION.json +11 -0
  131. package/catalog/skills/security-audit/skill.yaml +14 -0
  132. package/catalog/skills/soft-design/EXAMPLES.md +51 -0
  133. package/catalog/skills/soft-design/SKILL.md +108 -0
  134. package/catalog/skills/soft-design/VALIDATION.json +12 -0
  135. package/catalog/skills/soft-design/skill.yaml +10 -0
  136. package/catalog/skills/state-management/EXAMPLES.md +56 -0
  137. package/catalog/skills/state-management/SKILL.md +168 -0
  138. package/catalog/skills/state-management/TROUBLESHOOTING.md +18 -0
  139. package/catalog/skills/state-management/VALIDATION.json +11 -0
  140. package/catalog/skills/state-management/skill.yaml +14 -0
  141. package/catalog/skills/subagent-orchestrator/SKILL.md +117 -0
  142. package/catalog/skills/subagent-orchestrator/VALIDATION.json +12 -0
  143. package/catalog/skills/subagent-orchestrator/skill.yaml +9 -0
  144. package/catalog/skills/system-design/EXAMPLES.md +75 -0
  145. package/catalog/skills/system-design/SKILL.md +419 -0
  146. package/catalog/skills/system-design/TROUBLESHOOTING.md +19 -0
  147. package/catalog/skills/system-design/VALIDATION.json +12 -0
  148. package/catalog/skills/system-design/skill.yaml +14 -0
  149. package/catalog/skills/terraform/EXAMPLES.md +74 -0
  150. package/catalog/skills/terraform/SKILL.md +55 -0
  151. package/catalog/skills/terraform/TROUBLESHOOTING.md +53 -0
  152. package/catalog/skills/terraform/VALIDATION.json +11 -0
  153. package/catalog/skills/terraform/skill.yaml +14 -0
  154. package/catalog/skills/testing/EXAMPLES.md +122 -0
  155. package/catalog/skills/testing/SKILL.md +70 -0
  156. package/catalog/skills/testing/TROUBLESHOOTING.md +18 -0
  157. package/catalog/skills/testing/VALIDATION.json +11 -0
  158. package/catalog/skills/testing/skill.yaml +14 -0
  159. package/catalog/skills/typescript/EXAMPLES.md +64 -0
  160. package/catalog/skills/typescript/SKILL.md +112 -0
  161. package/catalog/skills/typescript/TROUBLESHOOTING.md +19 -0
  162. package/catalog/skills/typescript/VALIDATION.json +12 -0
  163. package/catalog/skills/typescript/skill.yaml +14 -0
  164. package/catalog/skills/ui-design/EXAMPLES.md +21 -0
  165. package/catalog/skills/ui-design/SKILL.md +124 -0
  166. package/catalog/skills/ui-design/TROUBLESHOOTING.md +19 -0
  167. package/catalog/skills/ui-design/VALIDATION.json +12 -0
  168. package/catalog/skills/ui-design/skill.yaml +16 -0
  169. package/catalog/skills/ui-ux-pro/EXAMPLES.md +62 -0
  170. package/catalog/skills/ui-ux-pro/SKILL.md +418 -0
  171. package/catalog/skills/ui-ux-pro/TROUBLESHOOTING.md +19 -0
  172. package/catalog/skills/ui-ux-pro/VALIDATION.json +12 -0
  173. package/catalog/skills/ui-ux-pro/skill.yaml +14 -0
  174. package/catalog/skills/ux-design/EXAMPLES.md +36 -0
  175. package/catalog/skills/ux-design/SKILL.md +116 -0
  176. package/catalog/skills/ux-design/TROUBLESHOOTING.md +19 -0
  177. package/catalog/skills/ux-design/VALIDATION.json +12 -0
  178. package/catalog/skills/ux-design/skill.yaml +16 -0
  179. package/catalog/skills/vercel-optimize/SKILL.md +83 -0
  180. package/catalog/skills/vercel-optimize/VALIDATION.json +12 -0
  181. package/catalog/skills/vercel-optimize/scripts/collect-signals.mjs +131 -0
  182. package/catalog/skills/vercel-optimize/scripts/gate-investigations.mjs +142 -0
  183. package/catalog/skills/vercel-optimize/scripts/merge-signals.mjs +143 -0
  184. package/catalog/skills/vercel-optimize/scripts/scan-codebase.mjs +174 -0
  185. package/catalog/skills/vercel-optimize/skill.yaml +15 -0
  186. package/catalog/skills/web-accessibility/EXAMPLES.md +39 -0
  187. package/catalog/skills/web-accessibility/SKILL.md +151 -0
  188. package/catalog/skills/web-accessibility/TROUBLESHOOTING.md +19 -0
  189. package/catalog/skills/web-accessibility/VALIDATION.json +12 -0
  190. package/catalog/skills/web-accessibility/skill.yaml +14 -0
  191. package/package.json +3 -2
@@ -90,8 +90,8 @@ function render(context) {
90
90
  }
91
91
 
92
92
  aiderConfContent = yamlProv + '\n' + YAML.stringify(parsed);
93
- } catch {
94
- aiderConfContent = yamlProv + '\nread:\n - CONVENTIONS.md\n';
93
+ } catch (err) {
94
+ throw new Error(`Failed to parse existing .aider.conf.yml: ${err.message}. Aborting to protect existing configuration from data loss.`);
95
95
  }
96
96
  } else {
97
97
  aiderConfContent = [
@@ -9,13 +9,15 @@ const { collectSkillDirectories, readMeaningfulMarkdown } = require('../shared.j
9
9
  const { registerAdapter, applyArtifacts } = require('../pure-compiler.js');
10
10
 
11
11
  const GENERATOR_ID = 'claude@2';
12
+ const CLAUDE_START_MARKER = '<!-- CONTEXTOS:START -->';
13
+ const CLAUDE_END_MARKER = '<!-- CONTEXTOS:END -->';
12
14
 
13
15
  function describe() {
14
16
  return {
15
17
  name: 'claude',
16
18
  version: '2.0.0',
17
- description: 'Compiles clean markdown skills for Claude Code CLI',
18
- targetPattern: '.agents/generated/claude/skills/**/SKILL.md',
19
+ description: 'Compiles clean markdown skills for Claude Code CLI and manages CLAUDE.md entrypoint',
20
+ targetPattern: '{CLAUDE.md,.agents/generated/claude/skills/**/SKILL.md}',
19
21
  };
20
22
  }
21
23
 
@@ -53,6 +55,50 @@ function renderClaudeSkill(skillDir, context) {
53
55
  };
54
56
  }
55
57
 
58
+ function renderClaudeRootIndex(skills, context) {
59
+ const projectRoot = context?.projectRoot || '.';
60
+ const claudePath = path.join(projectRoot, 'CLAUDE.md');
61
+ let existingContent = '';
62
+ if (fs.existsSync(claudePath)) {
63
+ try {
64
+ existingContent = fs.readFileSync(claudePath, 'utf8');
65
+ } catch {}
66
+ }
67
+
68
+ const skillEntries = skills.map(skillDir => {
69
+ const name = path.basename(skillDir);
70
+ return `- [${name}](.agents/generated/claude/skills/${name}/SKILL.md)`;
71
+ }).join('\n');
72
+
73
+ const managedBlock = `${CLAUDE_START_MARKER}
74
+ <!-- Do not edit this section directly. Synchronized by ContextOS. -->
75
+ # ContextOS Agent Governance
76
+
77
+ The following skills are managed by ContextOS:
78
+ ${skillEntries}
79
+ ${CLAUDE_END_MARKER}`;
80
+
81
+ let finalContent;
82
+ if (existingContent.includes(CLAUDE_START_MARKER)) {
83
+ const regex = new RegExp(`${CLAUDE_START_MARKER}[\\s\\S]*?${CLAUDE_END_MARKER}`);
84
+ finalContent = existingContent.replace(regex, managedBlock);
85
+ } else if (existingContent.trim()) {
86
+ finalContent = `${existingContent.trimEnd()}\n\n${managedBlock}\n`;
87
+ } else {
88
+ finalContent = `${managedBlock}\n`;
89
+ }
90
+
91
+ return {
92
+ path: 'CLAUDE.md',
93
+ content: finalContent.replace(/\r\n/g, '\n'),
94
+ mediaType: 'text/markdown',
95
+ kind: 'generated-adapter',
96
+ generator: GENERATOR_ID,
97
+ sourceSkillIds: skills.map(s => path.basename(s)),
98
+ inputsHash: context?.sourceGraphHash || 'none',
99
+ };
100
+ }
101
+
56
102
  function render(context) {
57
103
  const skills = collectSkillDirectories(context?.profile, context?.projectRoot);
58
104
  const artifacts = [];
@@ -62,6 +108,11 @@ function render(context) {
62
108
  if (art) artifacts.push(art);
63
109
  }
64
110
 
111
+ if (skills.length > 0) {
112
+ const rootIndex = renderClaudeRootIndex(skills, context);
113
+ if (rootIndex) artifacts.push(rootIndex);
114
+ }
115
+
65
116
  return artifacts;
66
117
  }
67
118
 
@@ -170,10 +170,13 @@ function detectDrift(projectRoot, adapters = 'all', options = {}) {
170
170
  } else {
171
171
  const diskContent = fs.readFileSync(fullPath, 'utf8');
172
172
  const diskSemanticHash = computeSemanticHash(diskContent);
173
+ const lockRecord = lockfile?.managedFiles?.[art.path];
173
174
 
174
- // Check if disk matches projected render
175
- if (diskSemanticHash !== projectedSemanticHash) {
176
- const lockRecord = lockfile?.managedFiles?.[art.path];
175
+ // Check if disk matches projected render and recorded lockfile
176
+ const matchesProjection = diskSemanticHash === projectedSemanticHash;
177
+ const matchesLock = lockRecord ? diskSemanticHash === lockRecord.semanticTextSha256 : matchesProjection;
178
+
179
+ if (!matchesProjection || !matchesLock) {
177
180
  if (lockRecord && lockRecord.semanticTextSha256 !== diskSemanticHash) {
178
181
  findings[DRIFT_STATES.MODIFIED_MANAGED_OUTPUT].push({
179
182
  path: art.path,
@@ -86,11 +86,22 @@ function loadCompilerContext(projectRoot, options = {}) {
86
86
 
87
87
  // 2. Profile v2
88
88
  let profile = null;
89
- try {
90
- const profiles = require('../profiles.js');
91
- profile = options.profile ? profiles.getProfile(options.profile, absRoot) : profiles.getActiveProfile(absRoot);
92
- } catch {
93
- profile = null;
89
+ const profiles = require('../profiles.js');
90
+ if (options.profile) {
91
+ try {
92
+ profile = profiles.getProfile(options.profile, absRoot);
93
+ } catch (err) {
94
+ throw new Error(`Failed to load profile '${options.profile}': ${err.message}`);
95
+ }
96
+ if (!profile) {
97
+ throw new Error(`Profile '${options.profile}' was specified but does not exist.`);
98
+ }
99
+ } else {
100
+ try {
101
+ profile = profiles.getActiveProfile(absRoot);
102
+ } catch {
103
+ profile = null;
104
+ }
94
105
  }
95
106
 
96
107
  // 3. Workspace Graph
@@ -172,9 +183,10 @@ function listAdapters() {
172
183
  */
173
184
  function renderAdapters(projectRoot, adapterNames = 'all', options = {}) {
174
185
  const context = loadCompilerContext(projectRoot, options);
175
- const targets = adapterNames === 'all'
186
+ const rawTargets = adapterNames === 'all'
176
187
  ? listAdapters()
177
188
  : (Array.isArray(adapterNames) ? adapterNames : [adapterNames]);
189
+ const targets = rawTargets.flatMap(a => (typeof a === 'string' && a.includes(',') ? a.split(',').map(s => s.trim()).filter(Boolean) : a));
178
190
 
179
191
  const allArtifacts = [];
180
192
  const pathToArtifact = new Map();
package/.agents/ctx.js CHANGED
@@ -253,8 +253,8 @@ if (command === 'export') {
253
253
  : (args.find(a => a.startsWith('--scope=')) || '').split('=')[1] || null;
254
254
 
255
255
  if (subcommand === 'list') {
256
- const list = profiles.listProfiles();
257
- const active = profiles.getActiveProfile();
256
+ const list = profiles.listProfiles(process.cwd());
257
+ const active = profiles.getActiveProfile(process.cwd());
258
258
  if (asJson) {
259
259
  console.log(JSON.stringify({ active, profiles: list }, null, 2));
260
260
  } else {
@@ -285,7 +285,7 @@ if (command === 'export') {
285
285
  console.error('[ERROR] Usage: node ctx.js profile show <name> [--json]');
286
286
  process.exit(1);
287
287
  }
288
- const profile = profiles.getProfile(name);
288
+ const profile = profiles.getProfile(name, process.cwd());
289
289
  if (!profile) {
290
290
  console.error(`[ERROR] Profile '${name}' not found.`);
291
291
  process.exit(1);
@@ -306,7 +306,7 @@ if (command === 'export') {
306
306
  process.exit(1);
307
307
  }
308
308
  try {
309
- const explanation = profiles.explainProfile(name);
309
+ const explanation = profiles.explainProfile(name, process.cwd());
310
310
  if (asJson) {
311
311
  console.log(JSON.stringify(explanation, null, 2));
312
312
  } else {
@@ -26,7 +26,7 @@ const https = require('https');
26
26
  const crypto = require('crypto');
27
27
  const { execFileSync } = require('child_process');
28
28
  const { ProjectMutationLock } = require('./filesystem/project-lock.js');
29
- const JournaledTransaction = require('./filesystem/journaled-transaction.js');
29
+ const { JournaledTransaction } = require('./filesystem/journaled-transaction.js');
30
30
  const {
31
31
  calculateTreeDigest,
32
32
  validateArchiveEntry,
@@ -284,11 +284,49 @@ function isValidNpmPackage(name) {
284
284
  return /^(?:@[a-z0-9_.-]+\/)?[a-z0-9_.-]+$/.test(name);
285
285
  }
286
286
 
287
- function parseRef(ref) {
287
+ function parseRef(ref, projectRoot = null) {
288
288
  if (typeof ref !== 'string' || !ref.trim()) {
289
289
  throw new Error('A plugin reference is required');
290
290
  }
291
291
 
292
+ // 1. Built-in package catalog skills take strict precedence for catalog names.
293
+ // Consumer project local files can never spoof or hijack package catalog skills.
294
+ const pkgCatalogPath = path.join(path.resolve(__dirname, '..'), 'catalog', 'skills', ref);
295
+ if (fs.existsSync(pkgCatalogPath) && fs.existsSync(path.join(pkgCatalogPath, 'SKILL.md'))) {
296
+ return {
297
+ type: 'catalog',
298
+ name: ref,
299
+ path: pkgCatalogPath,
300
+ isPinned: true,
301
+ raw: ref,
302
+ };
303
+ }
304
+
305
+ // 2. Explicit local references (must start with 'local:', './', '../', or 'file:')
306
+ if (ref.startsWith('local:') || ref.startsWith('./') || ref.startsWith('../') || ref.startsWith('file:')) {
307
+ const rawTarget = ref.startsWith('local:')
308
+ ? ref.slice(6)
309
+ : (ref.startsWith('file:') ? ref.slice(5) : ref);
310
+
311
+ const baseRoot = projectRoot || process.cwd();
312
+ let localDir = path.resolve(baseRoot, rawTarget);
313
+ if (!fs.existsSync(localDir)) {
314
+ localDir = path.join(baseRoot, 'catalog', 'skills', rawTarget);
315
+ }
316
+
317
+ if (fs.existsSync(localDir) && fs.existsSync(path.join(localDir, 'SKILL.md'))) {
318
+ const skillName = path.basename(localDir);
319
+ return {
320
+ type: 'catalog',
321
+ name: skillName,
322
+ path: localDir,
323
+ isPinned: true,
324
+ raw: ref,
325
+ };
326
+ }
327
+ throw new Error(`Local skill not found at: '${ref}'`);
328
+ }
329
+
292
330
  // Scoped packages contain a slash but are npm packages, not GitHub refs.
293
331
  if (ref.startsWith('@')) {
294
332
  if (!isValidNpmPackage(ref)) {
@@ -529,6 +567,62 @@ function installFromNpm(descriptor, skillName, dryRun, checksum, forceUnsafe = f
529
567
  return sha256;
530
568
  }
531
569
 
570
+ // ── Catalog installer ─────────────────────────────────────────────────────────
571
+ function installFromCatalog(descriptor, skillName, dryRun, checksum, forceUnsafe = false, tx = null) {
572
+ const sourceDir = descriptor.path;
573
+ const targetDir = path.join(PLUGINS_DIR, skillName);
574
+ const relTargetDir = toPosix(path.relative(ROOT, targetDir));
575
+
576
+ if (dryRun) {
577
+ console.log(c.dim(` [DRY-RUN] Would copy catalog skill from: ${sourceDir}`));
578
+ return 'sha256:0000000000000000000000000000000000000000000000000000000000000000';
579
+ }
580
+
581
+ function copyRecursive(src, dest, relDest) {
582
+ if (!fs.existsSync(dest)) {
583
+ fs.mkdirSync(dest, { recursive: true });
584
+ }
585
+ const entries = fs.readdirSync(src, { withFileTypes: true });
586
+ for (const entry of entries) {
587
+ const srcPath = path.join(src, entry.name);
588
+ const destPath = path.join(dest, entry.name);
589
+ const relPath = toPosix(path.join(relDest, entry.name));
590
+
591
+ if (entry.isDirectory()) {
592
+ copyRecursive(srcPath, destPath, relPath);
593
+ } else {
594
+ const content = fs.readFileSync(srcPath, 'utf8');
595
+ if (tx) {
596
+ tx.stageWrite(relPath, content);
597
+ } else {
598
+ fs.writeFileSync(destPath, content, 'utf8');
599
+ }
600
+ }
601
+ }
602
+ }
603
+
604
+ copyRecursive(sourceDir, targetDir, relTargetDir);
605
+
606
+ const primarySkillMd = path.join(sourceDir, 'SKILL.md');
607
+ const sha256 = fs.existsSync(primarySkillMd) ? crypto.createHash('sha256').update(fs.readFileSync(primarySkillMd)).digest('hex') : null;
608
+
609
+ const sourceMeta = {
610
+ source: 'catalog',
611
+ name: skillName,
612
+ path: toPosix(path.relative(ROOT, sourceDir)),
613
+ sha256,
614
+ installedAt: new Date().toISOString(),
615
+ };
616
+ if (tx) {
617
+ tx.stageWrite(`${relTargetDir}/.source`, JSON.stringify(sourceMeta, null, 2) + '\n');
618
+ } else {
619
+ fs.writeFileSync(path.join(targetDir, '.source'), JSON.stringify(sourceMeta, null, 2) + '\n');
620
+ }
621
+
622
+ console.log(c.green(` ✓ Installed from built-in catalog: ${skillName}`));
623
+ return sha256;
624
+ }
625
+
532
626
  // ── Derive skill name from ref ────────────────────────────────────────────────
533
627
  function deriveSkillName(ref) {
534
628
  // username/repo/path/to/my-skill → my-skill
@@ -570,7 +664,7 @@ async function add(ref, options = {}) {
570
664
  }
571
665
 
572
666
  // Supply-chain source pinning check (Wave 6)
573
- if (!dryRun) {
667
+ if (!dryRun && descriptor.type !== 'catalog') {
574
668
  const pinning = validatePluginPinning(
575
669
  {
576
670
  type: descriptor.type,
@@ -637,7 +731,9 @@ async function add(ref, options = {}) {
637
731
  lockToken = projectLock.acquire({ command: 'skill:add' });
638
732
 
639
733
  let installedSha256 = null;
640
- if (descriptor.type === 'github') {
734
+ if (descriptor.type === 'catalog') {
735
+ installedSha256 = installFromCatalog(descriptor, skillName, dryRun, checksum, forceUnsafe, tx);
736
+ } else if (descriptor.type === 'github') {
641
737
  installedSha256 = await installFromGitHub(descriptor, skillName, dryRun, checksum, forceUnsafe, tx);
642
738
  } else {
643
739
  installedSha256 = installFromNpm(descriptor, skillName, dryRun, checksum, forceUnsafe, tx);
@@ -298,14 +298,26 @@ function validateProfile(profile, registry = null) {
298
298
  }
299
299
 
300
300
  /**
301
- * Lists all available project profiles defined in core/profiles.
301
+ * Lists all available project profiles defined in core/profiles and optionally consumer project.
302
+ * Searches built-in profiles in packageRoot, and if projectRoot is supplied, searches
303
+ * consumer project .agents/profiles, .agents/core/profiles, and profiles directories.
302
304
  *
305
+ * @param {string|null} [projectRoot=null] - Optional consumer project root
303
306
  * @returns {ProfileConfig[]} Array of profile configurations
304
307
  */
305
- function listProfiles() {
308
+ function listProfiles(projectRoot = null) {
306
309
  const profiles = [];
307
310
  const searchDirs = [PROFILES_DIR, path.join(AGENTS_DIR, 'catalog', 'presets')];
308
311
 
312
+ if (projectRoot) {
313
+ const absConsumer = path.resolve(projectRoot);
314
+ searchDirs.push(
315
+ path.join(absConsumer, '.agents', 'profiles'),
316
+ path.join(absConsumer, '.agents', 'core', 'profiles'),
317
+ path.join(absConsumer, 'profiles')
318
+ );
319
+ }
320
+
309
321
  for (const dir of searchDirs) {
310
322
  if (!fs.existsSync(dir)) continue;
311
323
  const files = fs.readdirSync(dir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
@@ -314,22 +326,31 @@ function listProfiles() {
314
326
  const parsed = parseYamlProfile(content);
315
327
  if (!parsed.id) parsed.id = path.basename(file, path.extname(file));
316
328
  if (!parsed.name) parsed.name = parsed.id;
317
- profiles.push(parsed);
329
+
330
+ // Consumer profiles can override or extend built-in profiles; deduplicate by id
331
+ const normId = parsed.id.toLowerCase();
332
+ const existingIdx = profiles.findIndex(p => p.id.toLowerCase() === normId);
333
+ if (existingIdx !== -1) {
334
+ profiles[existingIdx] = parsed;
335
+ } else {
336
+ profiles.push(parsed);
337
+ }
318
338
  }
319
339
  }
320
340
  return profiles;
321
341
  }
322
342
 
323
343
  /**
324
- * Finds a profile by its slug ID or title.
344
+ * Finds a profile by its slug ID or title across package and consumer roots.
325
345
  *
326
346
  * @param {string} name - Profile identifier or name
347
+ * @param {string|null} [projectRoot=null] - Optional consumer project root
327
348
  * @returns {ProfileConfig|null} Found profile or null
328
349
  */
329
- function getProfile(name) {
350
+ function getProfile(name, projectRoot = null) {
330
351
  if (!name) return null;
331
352
  const clean = name.toLowerCase().trim();
332
- const all = listProfiles();
353
+ const all = listProfiles(projectRoot);
333
354
  return all.find(p => p.id.toLowerCase() === clean || p.name.toLowerCase() === clean) || null;
334
355
  }
335
356
 
@@ -398,7 +419,7 @@ function getActiveProfile(projectDir = process.cwd(), targetFileOrScope = null)
398
419
  }
399
420
 
400
421
  if (matchedProfileName) {
401
- const overrideProf = getProfile(matchedProfileName);
422
+ const overrideProf = getProfile(matchedProfileName, projectDir);
402
423
  if (overrideProf) {
403
424
  return {
404
425
  ...overrideProf,
@@ -424,9 +445,9 @@ function getActiveProfile(projectDir = process.cwd(), targetFileOrScope = null)
424
445
  * @returns {Object} Updated profile lock data
425
446
  */
426
447
  function applyProfile(profileName, projectDir = process.cwd(), options = {}) {
427
- const profile = getProfile(profileName);
448
+ const profile = getProfile(profileName, projectDir);
428
449
  if (!profile) {
429
- throw new Error(`Profile '${profileName}' not found. Available profiles: ${listProfiles().map(p => p.id).join(', ')}`);
450
+ throw new Error(`Profile '${profileName}' not found. Available profiles: ${listProfiles(projectDir).map(p => p.id).join(', ')}`);
430
451
  }
431
452
 
432
453
  validateProfile(profile);
@@ -563,9 +584,9 @@ function removeActiveProfile(projectDir = process.cwd(), options = {}) {
563
584
  * @returns {Object} Explanation structure
564
585
  */
565
586
  function explainProfile(profileName, projectDir = process.cwd()) {
566
- const profile = getProfile(profileName);
587
+ const profile = getProfile(profileName, projectDir);
567
588
  if (!profile) {
568
- throw new Error(`Profile '${profileName}' not found. Available profiles: ${listProfiles().map(p => p.id).join(', ')}`);
589
+ throw new Error(`Profile '${profileName}' not found. Available profiles: ${listProfiles(projectDir).map(p => p.id).join(', ')}`);
569
590
  }
570
591
 
571
592
  return {
package/README.md CHANGED
@@ -172,9 +172,9 @@ jobs:
172
172
  runs-on: ubuntu-latest
173
173
  steps:
174
174
  - uses: actions/checkout@v4
175
- - uses: kok-o/contextos-agents/.github/actions/contextos-gate@v2.0.0
175
+ - uses: kok-o/contextos-agents/.github/actions/contextos-gate@v2.1.1
176
176
  with:
177
- version: '2.0.0' # Pinned version of contextos-agents runner
177
+ version: '2.1.1' # Pinned version of contextos-agents runner
178
178
  adapters: 'all' # Adapters to verify (or specific: 'cursor', 'claude')
179
179
  working-directory: '.' # Project root directory
180
180
  ```
@@ -17,7 +17,28 @@ const HOOK_MARKER_START = '# BEGIN CONTEXTOS HOOK';
17
17
  const HOOK_MARKER_END = '# END CONTEXTOS HOOK';
18
18
 
19
19
  const HOOK_PAYLOAD = `${HOOK_MARKER_START}
20
- npx contextos-agents scan --staged --enforce || exit 1
20
+ CTX_SCOPE_ARG=""
21
+ if [ -f ".agents/task-scope.json" ]; then
22
+ CTX_SCOPE_ARG="--scope .agents/task-scope.json"
23
+ fi
24
+
25
+ if [ -f "./node_modules/.bin/contextos" ]; then
26
+ ./node_modules/.bin/contextos scan --staged --enforce --placeholders $CTX_SCOPE_ARG
27
+ CTX_STATUS=$?
28
+ elif [ -f "./node_modules/.bin/contextos-agents" ]; then
29
+ ./node_modules/.bin/contextos-agents scan --staged --enforce --placeholders $CTX_SCOPE_ARG
30
+ CTX_STATUS=$?
31
+ elif [ -f "./bin/index.js" ] && [ -f "./package.json" ]; then
32
+ node ./bin/index.js scan --staged --enforce --placeholders $CTX_SCOPE_ARG
33
+ CTX_STATUS=$?
34
+ else
35
+ echo "[ERROR] ContextOS local runner not found. Install locally (npm i -D contextos-agents) before committing." >&2
36
+ exit 2
37
+ fi
38
+
39
+ if [ $CTX_STATUS -ne 0 ]; then
40
+ exit $CTX_STATUS
41
+ fi
21
42
  ${HOOK_MARKER_END}`;
22
43
 
23
44
  function installHook(cwd = process.cwd()) {
@@ -37,19 +58,36 @@ function installHook(cwd = process.cwd()) {
37
58
  if (fs.existsSync(hookFile)) {
38
59
  const existing = fs.readFileSync(hookFile, 'utf8');
39
60
 
40
- if (existing.includes(HOOK_MARKER_START)) {
41
- // Replace existing block cleanly
42
- const regex = new RegExp(`${HOOK_MARKER_START}[\\s\\S]*?${HOOK_MARKER_END}`);
43
- const updated = existing.replace(regex, HOOK_PAYLOAD);
44
- fs.writeFileSync(hookFile, updated, 'utf8');
45
- console.log(`✓ ContextOS pre-commit hook updated in ${hookFile}`);
61
+ // Validate shebang if present
62
+ const shebangMatch = existing.match(/^#!([^\r\n]+)/);
63
+ if (shebangMatch) {
64
+ const interpreter = shebangMatch[1].toLowerCase();
65
+ const isShell = /\b(sh|bash|zsh|dash)\b/.test(interpreter);
66
+ if (!isShell) {
67
+ console.error(`[ERROR] Pre-commit hook uses non-shell interpreter (${shebangMatch[0]}). ContextOS pre-commit hook requires POSIX shell.`);
68
+ process.exit(1);
69
+ }
70
+ }
71
+
72
+ // Strip any existing ContextOS block (e.g. legacy trailing block from v2.1.0)
73
+ const stripped = existing.includes(HOOK_MARKER_START)
74
+ ? existing.replace(new RegExp(`\\n?${HOOK_MARKER_START}[\\s\\S]*?${HOOK_MARKER_END}\\n?`, 'g'), '').trim()
75
+ : existing.trim();
76
+
77
+ // Place HOOK_PAYLOAD right after shebang so it runs first and cannot be bypassed by user 'exit 0'
78
+ let updated;
79
+ const strippedShebangMatch = stripped.match(/^#!.*(?:\r?\n|$)/);
80
+ if (strippedShebangMatch) {
81
+ const shebang = strippedShebangMatch[0];
82
+ const rest = stripped.slice(shebang.length).trim();
83
+ const sep = rest ? '\n\n' : '\n';
84
+ updated = `${shebang.trimEnd()}\n\n${HOOK_PAYLOAD}${sep}${rest}`;
46
85
  } else {
47
- // Append while preserving user's existing hook script
48
- const separator = existing.endsWith('\n') ? '\n' : '\n\n';
49
- const updated = existing + separator + HOOK_PAYLOAD + '\n';
50
- fs.writeFileSync(hookFile, updated, 'utf8');
51
- console.log(`✓ ContextOS pre-commit hook appended to existing hook in ${hookFile}`);
86
+ updated = `#!/bin/sh\n\n${HOOK_PAYLOAD}${stripped ? `\n\n${stripped}` : ''}`;
52
87
  }
88
+
89
+ fs.writeFileSync(hookFile, updated.trimEnd() + '\n', 'utf8');
90
+ console.log(`✓ ContextOS pre-commit hook installed in ${hookFile}`);
53
91
  } else {
54
92
  // Fresh hook installation with shebang
55
93
  const fresh = `#!/bin/sh\n\n${HOOK_PAYLOAD}\n`;
@@ -7,9 +7,10 @@
7
7
 
8
8
  'use strict';
9
9
 
10
+ const path = require('path');
10
11
  const { runScan } = require('../lib/scan.js');
11
12
 
12
- function scanCommand(args, flags) {
13
+ function scanCommand(args, flags = {}) {
13
14
  const isJson = flags.json || args.includes('--json');
14
15
  const enforce = flags.enforce || args.includes('--enforce');
15
16
  const checkPlaceholders = flags.placeholders || args.includes('--placeholders');
@@ -18,8 +19,13 @@ function scanCommand(args, flags) {
18
19
  const scopeIdx = args.indexOf('--scope');
19
20
  const scopeFile = scopeIdx !== -1 && args[scopeIdx + 1] ? args[scopeIdx + 1] : null;
20
21
 
22
+ const projectIdx = args.indexOf('--project');
23
+ const projectArg = projectIdx !== -1 && args[projectIdx + 1] ? args[projectIdx + 1] : null;
24
+ const targetProject = flags.project || projectArg;
25
+ const projectDir = targetProject ? path.resolve(targetProject) : process.cwd();
26
+
21
27
  const result = runScan({
22
- cwd: process.cwd(),
28
+ cwd: projectDir,
23
29
  staged: true,
24
30
  secrets: checkSecrets,
25
31
  placeholders: checkPlaceholders,
@@ -43,6 +49,7 @@ function scanCommand(args, flags) {
43
49
 
44
50
  console.log(` Staged Files Scanned : ${result.stats.stagedFilesCount}`);
45
51
  console.log(` Enforcement Mode : ${enforce ? 'STRICT (fails on findings)' : 'ADVISORY (warnings only)'}`);
52
+ console.log(` Scope Containment : ${result.scope.status.toUpperCase()}`);
46
53
  console.log(` Violations Found : ${result.stats.violationsCount}\n`);
47
54
 
48
55
  if (result.findings.length > 0) {
@@ -58,7 +65,7 @@ function scanCommand(args, flags) {
58
65
 
59
66
  console.log('──────────────────────────────────────────────────────');
60
67
  if (result.ok) {
61
- console.log(' RESULT: PASSED (No blocking violations in staged index)');
68
+ console.log(' RESULT: PASSED (Verification completed cleanly, no blocking violations)');
62
69
  } else {
63
70
  console.log(' RESULT: FAILED (Commit blocked due to staged violations)');
64
71
  }
package/bin/index.js CHANGED
@@ -270,7 +270,10 @@ if (mainCommand === 'recover') {
270
270
  // Gate command (deterministic quality gate verification)
271
271
  if (mainCommand === 'gate') {
272
272
  const { runGate, emitGitHubAnnotations, writeGitHubSummary } = require('./lib/gate.js');
273
- const target = flags.target || (args[1] && !args[1].startsWith('-') ? args[1] : 'all');
273
+ let target = flags.target || (args[1] && !args[1].startsWith('-') ? args[1] : 'all');
274
+ if (typeof target === 'string' && target.includes(',')) {
275
+ target = target.split(',').map(s => s.trim()).filter(Boolean);
276
+ }
274
277
  const result = runGate(flags.project, {
275
278
  target,
276
279
  profile: flags.profile,
@@ -377,7 +380,7 @@ if (mainCommand === 'hook') {
377
380
 
378
381
  // Proxy commands to trusted package .agents/ctx.js targeting flags.project
379
382
  const PROXY_COMMANDS = [
380
- 'profile', 'export', 'validate', 'resolve', 'skill', 'index',
383
+ 'profile', 'export', 'validate', 'audit', 'resolve', 'skill', 'index',
381
384
  'clean-worktrees', 'stats', 'watch', 'compile', 'explain',
382
385
  'thread'
383
386
  ];
@@ -545,6 +548,9 @@ if (mainCommand === 'install-skill') {
545
548
  console.error(' To use the MCP Bridge or execution runtime, install @contextos/mcp:');
546
549
  console.error(' npm install @contextos/mcp --save-dev');
547
550
  process.exit(1);
551
+ } else if (mainCommand && mainCommand !== 'init') {
552
+ console.error(`[ERROR] Unknown command: '${mainCommand}'. Run 'contextos --help' for available commands.`);
553
+ process.exit(1);
548
554
  } else {
549
555
 
550
556
  // ── Paths ─────────────────────────────────────────────────────────────────────