contextos-agents 1.5.0 → 1.6.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.
@@ -43,14 +43,20 @@ const PROMPT_INJECTION_PATTERNS = [
43
43
  /you\s+are\s+now/i,
44
44
  /system\s*:\s*/i,
45
45
  /\[SYSTEM\]/i,
46
+ /base64\s*,\s*[A-Za-z0-9+/=]{40,}/i,
47
+ /https?:\/\/[^\s)]+\/(?:exfil|steal|leak|webhook|collect)/i,
48
+ /\b(?:eval|exec|Function)\s*\(/i,
46
49
  ];
47
50
 
48
- function scanForPromptInjection(content) {
51
+ function scanForPromptInjection(content, options = {}) {
49
52
  if (typeof content !== 'string') return false;
50
53
  const found = PROMPT_INJECTION_PATTERNS.filter(p => p.test(content));
51
54
  if (found.length > 0) {
52
- console.warn(c.yellow(' ⚠️ Potential prompt injection detected in downloaded skill'));
53
- console.warn(c.yellow(` Patterns: ${found.map(p => p.source).join(', ')}`));
55
+ console.warn(c.red(' 🚨 Potential prompt injection or unsafe pattern detected in skill:'));
56
+ console.warn(c.yellow(` Pattern matches: ${found.map(p => p.source).join(', ')}`));
57
+ if (options.throwOnMatch) {
58
+ throw new Error(`Security validation failed: prompt injection detected (${found.length} pattern matches). Installation aborted. Pass --force-unsafe-prompts to override.`);
59
+ }
54
60
  return true;
55
61
  }
56
62
  return false;
@@ -166,7 +172,7 @@ function parseRef(ref) {
166
172
  }
167
173
 
168
174
  // ── GitHub installer ──────────────────────────────────────────────────────────
169
- async function installFromGitHub(descriptor, skillName, dryRun, checksum) {
175
+ async function installFromGitHub(descriptor, skillName, dryRun, checksum, forceUnsafe = false) {
170
176
  const { owner, repo, gitRef, isPinned, subPath } = descriptor;
171
177
  const skillPath = subPath || '';
172
178
  const base = `https://raw.githubusercontent.com/${owner}/${repo}/${gitRef}`;
@@ -198,7 +204,7 @@ async function installFromGitHub(descriptor, skillName, dryRun, checksum) {
198
204
  if (checksum && sha256.toLowerCase() !== checksum.toLowerCase()) {
199
205
  throw new Error(`Checksum mismatch for ${skillName}: expected ${checksum}, got ${sha256}`);
200
206
  }
201
- scanForPromptInjection(skillMdContent);
207
+ scanForPromptInjection(skillMdContent, { throwOnMatch: !forceUnsafe });
202
208
 
203
209
  // Optionally fetch skill.yaml if it exists
204
210
  const yamlUrl = skillPath
@@ -244,7 +250,7 @@ async function installFromGitHub(descriptor, skillName, dryRun, checksum) {
244
250
  }
245
251
 
246
252
  // ── npm installer ─────────────────────────────────────────────────────────────
247
- function installFromNpm(descriptor, skillName, dryRun, checksum) {
253
+ function installFromNpm(descriptor, skillName, dryRun, checksum, forceUnsafe = false) {
248
254
  const pkgName = descriptor.package;
249
255
 
250
256
  if (dryRun) {
@@ -280,7 +286,7 @@ function installFromNpm(descriptor, skillName, dryRun, checksum) {
280
286
  if (checksum && sha256.toLowerCase() !== checksum.toLowerCase()) {
281
287
  throw new Error(`Checksum mismatch for ${skillName}: expected ${checksum}, got ${sha256}`);
282
288
  }
283
- scanForPromptInjection(skillMdContent);
289
+ scanForPromptInjection(skillMdContent, { throwOnMatch: !forceUnsafe });
284
290
 
285
291
  const targetDir = path.join(PLUGINS_DIR, skillName);
286
292
  fs.rmSync(targetDir, { recursive: true, force: true });
@@ -321,9 +327,13 @@ function isSafeSkillName(skillName) {
321
327
  // ═════════════════════════════════════════════════════════════════════════════
322
328
 
323
329
  /**
324
- * skill add <ref> [--dry-run] [--checksum <sha256>]
330
+ * skill add <ref> [--dry-run] [--checksum <sha256>] [--force-unsafe-prompts]
325
331
  */
326
- async function add(ref, { dryRun = false, checksum } = {}) {
332
+ async function add(ref, options = {}) {
333
+ const dryRun = Boolean(options.dryRun || options['dry-run']);
334
+ const checksum = options.checksum || null;
335
+ const forceUnsafe = Boolean(options.forceUnsafe || options['force-unsafe-prompts']);
336
+
327
337
  if (!ref) {
328
338
  console.error(c.red('Usage: ctx.js skill add <ref> [--checksum <sha256>]'));
329
339
  console.error(' ref can be: username/repo, username/repo/path/to/skill, or npm-package-name');
@@ -361,9 +371,9 @@ async function add(ref, { dryRun = false, checksum } = {}) {
361
371
  let installedSha256 = null;
362
372
  try {
363
373
  if (descriptor.type === 'github') {
364
- installedSha256 = await installFromGitHub(descriptor, skillName, dryRun, checksum);
374
+ installedSha256 = await installFromGitHub(descriptor, skillName, dryRun, checksum, forceUnsafe);
365
375
  } else {
366
- installedSha256 = installFromNpm(descriptor, skillName, dryRun, checksum);
376
+ installedSha256 = installFromNpm(descriptor, skillName, dryRun, checksum, forceUnsafe);
367
377
  }
368
378
  } catch (err) {
369
379
  console.error(c.red(`\n[ERROR] Failed to install '${skillName}': ${err.message}`));
@@ -225,13 +225,115 @@ function buildSkillIndex(projectDir = process.cwd()) {
225
225
  return index;
226
226
  }
227
227
 
228
+ /**
229
+ * Phase 2: AST & Project Dependency Graph Analysis.
230
+ * Scans project manifests and configs to detect active technologies
231
+ * that keyword or regex matching in prompts might omit.
232
+ *
233
+ * @param {string} [projectDir=process.cwd()] - Project root directory
234
+ * @returns {Map<string, number>} Map of skill name to score weight
235
+ */
236
+ function analyzeImportGraph(projectDir = process.cwd()) {
237
+ const techSignals = new Map();
238
+ if (!projectDir || !fs.existsSync(projectDir)) return techSignals;
239
+
240
+ const addSignal = (skill, weight) => {
241
+ techSignals.set(skill, (techSignals.get(skill) || 0) + weight);
242
+ };
243
+
244
+ // 1. Scan package.json dependencies and devDependencies
245
+ const pkgPath = path.join(projectDir, 'package.json');
246
+ if (fs.existsSync(pkgPath)) {
247
+ try {
248
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
249
+ const allDeps = {
250
+ ...(pkg.dependencies || {}),
251
+ ...(pkg.devDependencies || {}),
252
+ ...(pkg.peerDependencies || {}),
253
+ };
254
+
255
+ const DEP_SKILL_MAP = {
256
+ 'react': 'react',
257
+ 'react-dom': 'react',
258
+ 'next': 'nextjs',
259
+ 'prisma': 'database',
260
+ '@prisma/client': 'database',
261
+ 'drizzle-orm': 'database',
262
+ 'drizzle-kit': 'database',
263
+ 'typeorm': 'database',
264
+ 'mongoose': 'database',
265
+ 'pg': 'database',
266
+ 'mysql2': 'database',
267
+ 'vitest': 'testing',
268
+ 'jest': 'testing',
269
+ 'playwright': 'testing',
270
+ '@playwright/test': 'testing',
271
+ 'cypress': 'testing',
272
+ 'express': 'node',
273
+ 'fastify': 'node',
274
+ 'hono': 'node',
275
+ '@nestjs/core': 'nestjs',
276
+ 'typescript': 'typescript',
277
+ 'zod': 'typescript',
278
+ 'tailwindcss': 'ui-ux-pro',
279
+ 'lucide-react': 'ui-ux-pro',
280
+ '@radix-ui/react-dialog': 'web-accessibility',
281
+ 'framer-motion': 'impeccable-design',
282
+ 'dockerode': 'docker',
283
+ 'ioredis': 'system-design',
284
+ 'kafkajs': 'microservices',
285
+ 'amqplib': 'microservices',
286
+ };
287
+
288
+ for (const [dep, skill] of Object.entries(DEP_SKILL_MAP)) {
289
+ if (allDeps[dep]) {
290
+ addSignal(skill, 15);
291
+ }
292
+ }
293
+ } catch {
294
+ // ignore malformed package.json
295
+ }
296
+ }
297
+
298
+ // 2. Scan framework & tooling configuration files
299
+ const CONFIG_FILE_MAP = [
300
+ { file: 'tsconfig.json', skill: 'typescript', weight: 10 },
301
+ { file: 'next.config.js', skill: 'nextjs', weight: 15 },
302
+ { file: 'next.config.mjs', skill: 'nextjs', weight: 15 },
303
+ { file: 'next.config.ts', skill: 'nextjs', weight: 15 },
304
+ { file: 'tailwind.config.js', skill: 'ui-ux-pro', weight: 10 },
305
+ { file: 'tailwind.config.ts', skill: 'ui-ux-pro', weight: 10 },
306
+ { file: 'nest-cli.json', skill: 'nestjs', weight: 15 },
307
+ { file: 'prisma/schema.prisma', skill: 'database', weight: 15 },
308
+ { file: 'drizzle.config.ts', skill: 'database', weight: 15 },
309
+ { file: 'drizzle.config.js', skill: 'database', weight: 15 },
310
+ { file: 'Dockerfile', skill: 'docker', weight: 15 },
311
+ { file: 'docker-compose.yml', skill: 'docker', weight: 15 },
312
+ { file: 'docker-compose.yaml', skill: 'docker', weight: 15 },
313
+ { file: 'requirements.txt', skill: 'fastapi', weight: 15 },
314
+ { file: 'pyproject.toml', skill: 'fastapi', weight: 15 },
315
+ { file: 'vitest.config.ts', skill: 'testing', weight: 15 },
316
+ { file: 'vitest.config.js', skill: 'testing', weight: 15 },
317
+ { file: 'playwright.config.ts', skill: 'testing', weight: 15 },
318
+ { file: 'playwright.config.js', skill: 'testing', weight: 15 },
319
+ ];
320
+
321
+ for (const { file, skill, weight } of CONFIG_FILE_MAP) {
322
+ if (fs.existsSync(path.join(projectDir, file))) {
323
+ addSignal(skill, weight);
324
+ }
325
+ }
326
+
327
+ return techSignals;
328
+ }
329
+
228
330
  /**
229
331
  * Resolves the minimal set of skills for a given prompt, file list, and phase.
230
332
  *
231
333
  * @param {ResolveSkillsOptions} [options={}] - Task context options
232
334
  * @returns {ResolvedSkillsResult} Resolved domain, role, and activated skill names
233
335
  */
234
- function resolveSkills({ prompt = '', files = [], phase = 'Build', domain = '' } = {}) {
336
+ function resolveSkills({ prompt = '', files = [], phase = 'Build', domain = '', projectDir = process.cwd() } = {}) {
235
337
  const promptText = (prompt || '').toLowerCase();
236
338
  const scores = new Map();
237
339
 
@@ -267,6 +369,14 @@ function resolveSkills({ prompt = '', files = [], phase = 'Build', domain = '' }
267
369
  }
268
370
  }
269
371
 
372
+ // Phase 2: Merge AST & Project Dependency Graph Signals
373
+ if (projectDir) {
374
+ const astSignals = analyzeImportGraph(projectDir);
375
+ for (const [skill, astScore] of astSignals) {
376
+ scores.set(skill, (scores.get(skill) || 0) + astScore);
377
+ }
378
+ }
379
+
270
380
  // Sort matched domain skills by score descending
271
381
  const sortedDomainSkills = Array.from(scores.entries())
272
382
  .sort((a, b) => b[1] - a[1])
@@ -332,5 +442,6 @@ function formatDeclaration(resolution) {
332
442
  module.exports = {
333
443
  buildSkillIndex,
334
444
  resolveSkills,
445
+ analyzeImportGraph,
335
446
  formatDeclaration,
336
447
  };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * .agents/stats.js
3
+ * ContextOS — Context Savings & Token Optimization Report
4
+ *
5
+ * Measures:
6
+ * 1. Full context payload (all skills + AGENTS.md)
7
+ * 2. Profiled context payload (after applying active profile exclusions)
8
+ * 3. Dynamically resolved context (lean on-demand skill set for typical tasks)
9
+ * 4. Concrete token savings percentage and cost reductions
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const profiles = require('./profiles.js');
17
+
18
+ function estimateTokens(text) {
19
+ // Common heuristic across GPT-4, Claude, Gemini: ~4 characters per token
20
+ return Math.max(1, Math.round(text.length / 4));
21
+ }
22
+
23
+ function calculateContextStats(projectDir = process.cwd()) {
24
+ const agentsDir = path.join(projectDir, '.agents');
25
+ const skillsDir = path.join(agentsDir, 'core', 'skills');
26
+ const agentsMdPath = path.join(agentsDir, 'AGENTS.md');
27
+
28
+ let agentsMdChars = 0;
29
+ if (fs.existsSync(agentsMdPath)) {
30
+ agentsMdChars = fs.readFileSync(agentsMdPath, 'utf8').length;
31
+ }
32
+
33
+ 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');
39
+ if (fs.existsSync(skillMd)) {
40
+ skillCharMap.set(entry.name, fs.readFileSync(skillMd, 'utf8').length);
41
+ }
42
+ }
43
+ }
44
+
45
+ const totalSkillCount = skillCharMap.size;
46
+ let fullChars = agentsMdChars;
47
+ for (const chars of skillCharMap.values()) {
48
+ fullChars += chars;
49
+ }
50
+
51
+ // Active profile
52
+ const activeProfile = profiles.getActiveProfile(projectDir);
53
+ const profileName = activeProfile ? (activeProfile.name || activeProfile.profile) : 'startup (recommended)';
54
+ const excludedSkills = new Set(
55
+ activeProfile ? activeProfile.exclude_skills || [] : (profiles.getProfile('startup')?.exclude_skills || [])
56
+ );
57
+
58
+ let profileChars = agentsMdChars;
59
+ let profiledSkillCount = 0;
60
+ for (const [skill, chars] of skillCharMap) {
61
+ if (!excludedSkills.has(skill)) {
62
+ profileChars += chars;
63
+ profiledSkillCount++;
64
+ }
65
+ }
66
+
67
+ // Resolved context: 2 foundational skills + average 2 domain skills (e.g. react + ui-ux-pro)
68
+ const typicalResolvedSkills = ['engineering-workflow', 'ponytail-mindset', 'react', 'ui-ux-pro'];
69
+ let resolvedChars = agentsMdChars;
70
+ let resolvedSkillCount = 0;
71
+ for (const s of typicalResolvedSkills) {
72
+ if (skillCharMap.has(s)) {
73
+ resolvedChars += skillCharMap.get(s);
74
+ resolvedSkillCount++;
75
+ }
76
+ }
77
+
78
+ const fullTokens = estimateTokens({ length: fullChars });
79
+ const profileTokens = estimateTokens({ length: profileChars });
80
+ const resolvedTokens = estimateTokens({ length: resolvedChars });
81
+
82
+ const profileSavings = fullTokens > 0 ? (((fullTokens - profileTokens) / fullTokens) * 100).toFixed(1) : '0.0';
83
+ const resolvedSavings = fullTokens > 0 ? (((fullTokens - resolvedTokens) / fullTokens) * 100).toFixed(1) : '0.0';
84
+
85
+ return {
86
+ totalSkillCount,
87
+ profiledSkillCount,
88
+ resolvedSkillCount,
89
+ profileName,
90
+ full: { chars: fullChars, tokens: fullTokens },
91
+ profile: { chars: profileChars, tokens: profileTokens, savings: profileSavings },
92
+ resolved: { chars: resolvedChars, tokens: resolvedTokens, savings: resolvedSavings },
93
+ };
94
+ }
95
+
96
+ function runStats(projectDir = process.cwd()) {
97
+ const stats = calculateContextStats(projectDir);
98
+
99
+ console.log('\nContextOS — Context Savings Report\n');
100
+ console.log('┌──────────────────────────────────────┬─────────────┬──────────┐');
101
+ console.log('│ Mode │ Tokens │ Savings │');
102
+ console.log('├──────────────────────────────────────┼─────────────┼──────────┤');
103
+
104
+ const fullLabel = `Full (${stats.totalSkillCount} skills)`;
105
+ const fullTok = stats.full.tokens.toLocaleString().padStart(9);
106
+ console.log(`│ ${fullLabel.padEnd(36)} │ ${fullTok} │ — │`);
107
+
108
+ const profLabel = `Profile: ${stats.profileName} (${stats.profiledSkillCount} skills)`;
109
+ const profTok = stats.profile.tokens.toLocaleString().padStart(9);
110
+ const profSav = `${stats.profile.savings}%`.padStart(7);
111
+ console.log(`│ ${profLabel.slice(0, 36).padEnd(36)} │ ${profTok} │ ${profSav} │`);
112
+
113
+ const resLabel = `Resolved: typical task (${stats.resolvedSkillCount} skills)`;
114
+ const resTok = stats.resolved.tokens.toLocaleString().padStart(9);
115
+ const resSav = `${stats.resolved.savings}%`.padStart(7);
116
+ console.log(`│ ${resLabel.slice(0, 36).padEnd(36)} │ ${resTok} │ ${resSav} │`);
117
+
118
+ console.log('└──────────────────────────────────────┴─────────────┴──────────┘');
119
+ console.log('\nToken estimation: ~4 chars/token (GPT-4 / Claude / Gemini approximation)');
120
+ console.log('Dynamic skill resolution prevents prompt bloat and cuts LLM API costs.\n');
121
+
122
+ return stats;
123
+ }
124
+
125
+ if (require.main === module) {
126
+ runStats();
127
+ }
128
+
129
+ module.exports = {
130
+ calculateContextStats,
131
+ runStats,
132
+ estimateTokens,
133
+ };
@@ -0,0 +1,161 @@
1
+ /**
2
+ * .agents/watch.js
3
+ * ContextOS — Continuous Context Sync & File Watcher Daemon
4
+ *
5
+ * Watches:
6
+ * 1. .agents/core/skills/ — regenerates agent configs on skill markdown edits
7
+ * 2. package.json & manifests — detects tech stack drift and recommends profile adjustments
8
+ *
9
+ * Provides instant feedback during local development without manual re-exports.
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { execFileSync } = require('child_process');
17
+ const profiles = require('./profiles.js');
18
+
19
+ /**
20
+ * Runs the ContextOS watch daemon.
21
+ *
22
+ * @param {string} [projectDir=process.cwd()] - Target project directory
23
+ * @param {Object} [options={}] - Watcher configuration options
24
+ * @param {number} [options.debounceMs=300] - Debounce delay in milliseconds
25
+ * @param {Function} [options.onSync] - Callback after a sync event completes
26
+ * @param {boolean} [options.exitOnSigint=true] - Whether to register process exit handlers
27
+ * @returns {Object} Controller object with a `close()` method
28
+ */
29
+ function runWatch(projectDir = process.cwd(), options = {}) {
30
+ const debounceMs = options.debounceMs || 300;
31
+ const agentsDir = path.join(projectDir, '.agents');
32
+ const skillsDir = path.join(agentsDir, 'core', 'skills');
33
+ const ctxPath = path.join(agentsDir, 'ctx.js');
34
+
35
+ console.log('\n[ContextOS Watch] Initializing background continuous sync daemon...');
36
+ console.log(`[ContextOS Watch] Project directory: ${projectDir}`);
37
+
38
+ let lastStack = profiles.detectStack(projectDir);
39
+ console.log(`[ContextOS Watch] Initial stack: ${lastStack.detected.join(', ') || 'Generic JS'} (${lastStack.recommendedProfile})`);
40
+
41
+ let debounceTimer = null;
42
+ let isSyncing = false;
43
+ const watchers = [];
44
+
45
+ function triggerRecompile(sourceReason) {
46
+ if (debounceTimer) clearTimeout(debounceTimer);
47
+
48
+ debounceTimer = setTimeout(() => {
49
+ if (isSyncing) return;
50
+ isSyncing = true;
51
+
52
+ try {
53
+ const time = new Date().toLocaleTimeString();
54
+ console.log(`[${time}] [SYNC] Changes detected in ${sourceReason}. Recompiling agent skills...`);
55
+
56
+ if (fs.existsSync(ctxPath)) {
57
+ execFileSync(process.execPath, [ctxPath, 'export', 'all'], {
58
+ cwd: projectDir,
59
+ stdio: ['ignore', 'pipe', 'pipe'],
60
+ });
61
+ console.log(`[${time}] [OK] Skills exported to all agents (Gemini, Claude, Cursor, Copilot, Aider, Zed).`);
62
+ }
63
+
64
+ if (typeof options.onSync === 'function') {
65
+ options.onSync({ type: 'recompile', reason: sourceReason });
66
+ }
67
+ } catch (err) {
68
+ console.error(`[WARN] Auto-recompile failed: ${err.message}`);
69
+ } finally {
70
+ isSyncing = false;
71
+ }
72
+ }, debounceMs);
73
+ }
74
+
75
+ function checkStackChange() {
76
+ try {
77
+ const currentStack = profiles.detectStack(projectDir);
78
+ const prevStr = lastStack.detected.sort().join(',');
79
+ const currStr = currentStack.detected.sort().join(',');
80
+
81
+ if (prevStr !== currStr) {
82
+ const time = new Date().toLocaleTimeString();
83
+ console.log(`\n[${time}] [DETECT] Project dependencies changed!`);
84
+ console.log(` Detected stack : ${currentStack.detected.join(', ')}`);
85
+ console.log(` Recommendation : Profile '${currentStack.recommendedProfile}'`);
86
+ console.log(` Run: contextos profile apply ${currentStack.recommendedProfile}\n`);
87
+ lastStack = currentStack;
88
+
89
+ if (typeof options.onSync === 'function') {
90
+ options.onSync({ type: 'stack_change', stack: currentStack });
91
+ }
92
+ }
93
+ } catch {
94
+ // ignore transient filesystem read errors
95
+ }
96
+ }
97
+
98
+ // 1. Watch .agents/core/skills/ directory
99
+ if (fs.existsSync(skillsDir)) {
100
+ try {
101
+ const skillsWatcher = fs.watch(skillsDir, { recursive: true }, (eventType, filename) => {
102
+ if (!filename) return;
103
+ const norm = filename.replace(/\\/g, '/');
104
+ if (norm.endsWith('.md') || norm.endsWith('.json') || norm.endsWith('.yaml') || norm.endsWith('.yml')) {
105
+ triggerRecompile(`skills/${filename}`);
106
+ }
107
+ });
108
+ watchers.push(skillsWatcher);
109
+ } catch (e) {
110
+ console.warn(`[WARN] Could not establish recursive watcher on skills directory: ${e.message}`);
111
+ }
112
+ }
113
+
114
+ // 2. Watch package.json
115
+ const pkgPath = path.join(projectDir, 'package.json');
116
+ if (fs.existsSync(pkgPath)) {
117
+ try {
118
+ const pkgWatcher = fs.watch(pkgPath, () => {
119
+ checkStackChange();
120
+ });
121
+ watchers.push(pkgWatcher);
122
+ } catch (e) {
123
+ console.warn(`[WARN] Could not watch package.json: ${e.message}`);
124
+ }
125
+ }
126
+
127
+ console.log('[ContextOS Watch] Watching for skill updates and dependency shifts. Press Ctrl+C to stop.\n');
128
+
129
+ function close() {
130
+ if (debounceTimer) clearTimeout(debounceTimer);
131
+ for (const w of watchers) {
132
+ try {
133
+ w.close();
134
+ } catch {}
135
+ }
136
+ }
137
+
138
+ if (options.exitOnSigint !== false) {
139
+ const onExit = () => {
140
+ console.log('\n[ContextOS Watch] Stopping watcher daemon.');
141
+ close();
142
+ process.exit(0);
143
+ };
144
+ process.once('SIGINT', onExit);
145
+ process.once('SIGTERM', onExit);
146
+ }
147
+
148
+ return {
149
+ close,
150
+ triggerRecompile,
151
+ checkStackChange,
152
+ };
153
+ }
154
+
155
+ if (require.main === module) {
156
+ runWatch();
157
+ }
158
+
159
+ module.exports = {
160
+ runWatch,
161
+ };
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/contextos-agents.svg)](https://www.npmjs.com/package/contextos-agents)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Node.js](https://img.shields.io/badge/node-%3E%3D16.7.0-brightgreen.svg)](https://nodejs.org/)
5
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/)
6
6
  [![Tests](https://img.shields.io/badge/tests-passing-brightgreen.svg)](#testing)
7
7
 
8
8
  This is an open-source set of skills and behavioral rules for AI assistants. The package automatically installs an `.agents` folder into your project, teaching your AI assistant software development best practices (UI Design, Architecture, Security, and more).
@@ -49,10 +49,10 @@ Most AI coding assistants suffer from two extremes: they either operate in a vac
49
49
 
50
50
  ### Key Developer Advantages
51
51
 
52
- - 🚀 **Zero-Config Onboarding:** Run `npx contextos-agents` in your repository. It auto-detects your stack (React, Node, Python, etc.) and sets up the ideal profile in seconds.
53
- - 🎯 **Tailored Project Profiles:** Use `mvp` for lean, rapid prototyping without bloated microservices boilerplate, or `enterprise` for strict TDD, DDD, and security auditing.
54
- - 🛡️ **Autonomous Multi-Agent Worktrees:** Run parallel tasks safely with the bundled MCP server—subagents work in isolated Git worktrees without corrupting your active workspace.
55
- - 📊 **Verifiable Benchmarks:** Backed by reproducible side-by-side benchmarks demonstrating measurable code quality improvements and reduced token usage.
52
+ - **Zero-Config Onboarding:** Run `npx contextos-agents` in your repository. It auto-detects your stack (React, Node, Python, etc.) and sets up the ideal profile in seconds.
53
+ - **Tailored Project Profiles:** Use `mvp` for lean, rapid prototyping without bloated microservices boilerplate, or `enterprise` for strict TDD, DDD, and security auditing.
54
+ - **Autonomous Multi-Agent Worktrees:** Run parallel tasks safely with the bundled MCP server—subagents work in isolated Git worktrees without corrupting your active workspace.
55
+ - **Verifiable Benchmarks:** Backed by reproducible side-by-side benchmarks demonstrating measurable code quality improvements and reduced token usage.
56
56
 
57
57
  ## Project Profiles & Stack Auto-Detection
58
58
 
@@ -209,6 +209,32 @@ ContextOS commits generated adapter configurations (`.cursorrules`, `.cursor/rul
209
209
  - **Automated Sync & Drift Prevention:** CI strictly validates that generated exports match source skills (`node .agents/ctx.js validate`). Any uncommitted adapter drift fails CI checks via `git diff --exit-code`.
210
210
  - **Contributor Workflow:** Source rules are authored exclusively in `.agents/core/skills/<name>/SKILL.md`. Running `node .agents/ctx.js export all` regenerates all assistant configurations deterministically.
211
211
 
212
+ ### CI Quality Gate Action (`contextos-gate`)
213
+
214
+ You can guard your repository against skill drift, secret leaks, and rule regressions using the official reusable GitHub Composite Action:
215
+
216
+ ```yaml
217
+ # .github/workflows/pr-gate.yml
218
+ name: ContextOS Quality Gate
219
+
220
+ on:
221
+ pull_request:
222
+ branches: [main]
223
+ push:
224
+ branches: [main]
225
+
226
+ jobs:
227
+ gate:
228
+ runs-on: ubuntu-latest
229
+ steps:
230
+ - uses: actions/checkout@v4
231
+ - uses: kok-o/contextos-agents/.github/actions/contextos-gate@main
232
+ with:
233
+ node-version: '20'
234
+ ```
235
+
236
+ The action validates skill frontmatter integrity, checks for adapter configuration drift, scans for accidental secrets or API keys, and runs your test suite.
237
+
212
238
  ### Plugin Skills & Validation
213
239
 
214
240
  You can expand your `.agents` folder with community plugins or validate your own custom skills using the top-level commands:
@@ -381,7 +407,7 @@ The runtime sandbox evaluates model outputs against real-world engineering invar
381
407
  | **DDD Order Aggregate Root** | Architecture & DDD | `ddd`, `system-design`, `decisions` | Immutable `Money` Value Object, state-machine invariants (PENDING → PAID → SHIPPED), explicit Domain Event classes with queue draining. |
382
408
  | **Resilient API Client** | Reliability & Async | `typescript`, `system-design`, `performance` | 3-state Circuit Breaker (CLOSED → OPEN → HALF-OPEN), `AbortController` timeouts, typed error taxonomy without credential leakage. |
383
409
 
384
- ### Running Benchmarks Locally
410
+ ### Running Benchmarks Locally & in CI
385
411
 
386
412
  You can run the benchmark suite locally with your own API keys:
387
413
 
@@ -393,10 +419,24 @@ npm run benchmark:runtime -- --provider gemini --model gemini-2.5-flash
393
419
  # Run with OpenAI:
394
420
  $env:OPENAI_API_KEY = "sk-..."
395
421
  npm run benchmark:runtime -- --provider openai --model gpt-4o
422
+
423
+ # Run complete multi-model runtime matrix (Gemini, OpenRouter, AgentRouter):
424
+ GEMINI_API_KEY=... OPENROUTER_API_KEY=... AGENTROUTER_API_KEY=... node benchmarks/run-multi-runtime.cjs
396
425
  ```
397
426
 
398
427
  When executed, reports are generated in `benchmarks/results/` (`.html`, `.md`, `.json`) and tracked so results are visible and shareable.
399
428
 
429
+ > [!NOTE]
430
+ > **Why Runtime Benchmarks Are Dispatch-Only in CI:** Standard CI checks (`validate-skills.yml`) run hermetically without external API calls to avoid flaky network dependencies and API token expenditures on every pull request. Live runtime evaluation is triggered on demand via GitHub Actions **Workflow Dispatch** ([`benchmark-runtime.yml`](.github/workflows/benchmark-runtime.yml)) using secure repository secrets.
431
+
432
+
433
+ ## Security — Third-Party Skills
434
+
435
+ ContextOS skills are **executable context** — they become part of the system prompt that controls your AI agent's behavior. A malicious skill could instruct the AI agent to exfiltrate environment variables, modify files, or ignore your project's security policies.
436
+
437
+ > [!CAUTION]
438
+ > **Install skills only from repositories you trust as you would trust executable code.** Skills installed via `ctx.js skill add` from npm or GitHub are not sandboxed. ContextOS includes a built-in prompt injection scanner, but it cannot guarantee safety of arbitrary third-party content.
439
+
400
440
  ## Contributing
401
441
 
402
442
  We are open to pull requests! See [CONTRIBUTING.md](./CONTRIBUTING.md) for a step-by-step guide on how to add a new skill.