Skills engine v0.1 + multi-channel infrastructure (#307)
* refactor: multi-channel infrastructure with explicit channel/is_group tracking - Add channels[] array and findChannel() routing in index.ts, replacing hardcoded whatsapp.* calls with channel-agnostic callbacks - Add channel TEXT and is_group INTEGER columns to chats table with COALESCE upsert to protect existing values from null overwrites - is_group defaults to 0 (safe: unknown chats excluded from groups) - WhatsApp passes explicit channel='whatsapp' and isGroup to onChatMetadata - getAvailableGroups filters on is_group instead of JID pattern matching - findChannel logs warnings instead of silently dropping unroutable JIDs - Migration backfills channel/is_group from JID patterns for existing DBs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: skills engine v0.1 — deterministic skill packages with rerere resolution Three-way merge engine for applying skill packages on top of a core codebase. Skills declare which files they add/modify, and the engine uses git merge-file for conflict detection with git rerere for automatic resolution of previously-seen conflicts. Key components: - apply: three-way merge with backup/rollback safety net - replay: clean-slate replay for uninstall and rebase - update: core version updates with deletion detection - rebase: bake applied skills into base (one-way) - manifest: validation with path traversal protection - resolution-cache: pre-computed rerere resolutions - structured: npm deps, env vars, docker-compose merging - CI: per-skill test matrix with conflict detection 151 unit tests covering merge, rerere, backup, replay, uninstall, update, rebase, structured ops, and edge cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add Discord and Telegram skill packages Skill packages for adding Discord and Telegram channels to NanoClaw. Each package includes: - Channel implementation (add/src/channels/) - Three-way merge targets for index.ts, config.ts, routing.test.ts - Intent docs explaining merge invariants - Standalone integration tests - manifest.yaml with dependency/conflict declarations Applied via: npx tsx scripts/apply-skill.ts .claude/skills/add-discord These are inert until applied — no runtime impact. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * remove unused docs (skills-system-status, implementation-guide) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
118
scripts/generate-ci-matrix.ts
Normal file
118
scripts/generate-ci-matrix.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { parse } from 'yaml';
|
||||
import { SkillManifest } from '../skills-engine/types.js';
|
||||
|
||||
export interface MatrixEntry {
|
||||
skills: string[];
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface SkillOverlapInfo {
|
||||
name: string;
|
||||
modifies: string[];
|
||||
npmDependencies: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract overlap-relevant info from a parsed manifest.
|
||||
* @param dirName - The skill's directory name (e.g. 'add-discord'), used in matrix
|
||||
* entries so CI/scripts can locate the skill package on disk.
|
||||
*/
|
||||
export function extractOverlapInfo(manifest: SkillManifest, dirName: string): SkillOverlapInfo {
|
||||
const npmDeps = manifest.structured?.npm_dependencies
|
||||
? Object.keys(manifest.structured.npm_dependencies)
|
||||
: [];
|
||||
|
||||
return {
|
||||
name: dirName,
|
||||
modifies: manifest.modifies ?? [],
|
||||
npmDependencies: npmDeps,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute overlap matrix from a list of skill overlap infos.
|
||||
* Two skills overlap if they share any `modifies` entry or both declare
|
||||
* `structured.npm_dependencies` for the same package.
|
||||
*/
|
||||
export function computeOverlapMatrix(skills: SkillOverlapInfo[]): MatrixEntry[] {
|
||||
const entries: MatrixEntry[] = [];
|
||||
|
||||
for (let i = 0; i < skills.length; i++) {
|
||||
for (let j = i + 1; j < skills.length; j++) {
|
||||
const a = skills[i];
|
||||
const b = skills[j];
|
||||
const reasons: string[] = [];
|
||||
|
||||
// Check shared modifies entries
|
||||
const sharedModifies = a.modifies.filter((m) => b.modifies.includes(m));
|
||||
if (sharedModifies.length > 0) {
|
||||
reasons.push(`shared modifies: ${sharedModifies.join(', ')}`);
|
||||
}
|
||||
|
||||
// Check shared npm_dependencies packages
|
||||
const sharedNpm = a.npmDependencies.filter((pkg) =>
|
||||
b.npmDependencies.includes(pkg),
|
||||
);
|
||||
if (sharedNpm.length > 0) {
|
||||
reasons.push(`shared npm packages: ${sharedNpm.join(', ')}`);
|
||||
}
|
||||
|
||||
if (reasons.length > 0) {
|
||||
entries.push({
|
||||
skills: [a.name, b.name],
|
||||
reason: reasons.join('; '),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all skill manifests from a skills directory (e.g. .claude/skills/).
|
||||
* Each subdirectory should contain a manifest.yaml.
|
||||
* Returns both the parsed manifest and the directory name.
|
||||
*/
|
||||
export function readAllManifests(skillsDir: string): { manifest: SkillManifest; dirName: string }[] {
|
||||
if (!fs.existsSync(skillsDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results: { manifest: SkillManifest; dirName: string }[] = [];
|
||||
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
|
||||
const manifestPath = path.join(skillsDir, entry.name, 'manifest.yaml');
|
||||
if (!fs.existsSync(manifestPath)) continue;
|
||||
|
||||
const content = fs.readFileSync(manifestPath, 'utf-8');
|
||||
const manifest = parse(content) as SkillManifest;
|
||||
results.push({ manifest, dirName: entry.name });
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the full CI matrix from a skills directory.
|
||||
*/
|
||||
export function generateMatrix(skillsDir: string): MatrixEntry[] {
|
||||
const entries = readAllManifests(skillsDir);
|
||||
const overlapInfos = entries.map((e) => extractOverlapInfo(e.manifest, e.dirName));
|
||||
return computeOverlapMatrix(overlapInfos);
|
||||
}
|
||||
|
||||
// --- Main ---
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.url.replace('file://', ''))) {
|
||||
const projectRoot = process.cwd();
|
||||
const skillsDir = path.join(projectRoot, '.claude', 'skills');
|
||||
const matrix = generateMatrix(skillsDir);
|
||||
console.log(JSON.stringify(matrix, null, 2));
|
||||
}
|
||||
Reference in New Issue
Block a user