atris 2.2.0 → 2.2.2
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.
- package/GETTING_STARTED.md +2 -2
- package/README.md +2 -2
- package/atris/GETTING_STARTED.md +2 -2
- package/atris/atris.md +4 -3
- package/atris/features/README.md +41 -15
- package/atris/skills/README.md +28 -2
- package/atris/skills/atris/SKILL.md +7 -0
- package/atris/skills/autopilot/SKILL.md +8 -4
- package/atris/skills/backend/SKILL.md +6 -1
- package/atris/skills/calendar/SKILL.md +301 -0
- package/atris/skills/clawhub/atris/SKILL.md +121 -0
- package/atris/skills/copy-editor/SKILL.md +7 -8
- package/atris/skills/design/SKILL.md +5 -1
- package/atris/skills/drive/SKILL.md +363 -0
- package/atris/skills/email-agent/SKILL.md +88 -0
- package/atris/skills/memory/SKILL.md +3 -0
- package/atris/skills/meta/SKILL.md +4 -0
- package/atris/skills/skill-improver/SKILL.md +147 -0
- package/atris/skills/slack/SKILL.md +320 -0
- package/atris/skills/writing/SKILL.md +3 -0
- package/atris/team/brainstormer.md +1 -0
- package/atris/team/executor.md +1 -0
- package/atris/team/launcher.md +1 -0
- package/atris/team/navigator.md +2 -0
- package/atris/team/validator.md +15 -0
- package/atris.md +17 -0
- package/bin/atris.js +14 -2
- package/commands/init.js +13 -0
- package/commands/skill.js +496 -0
- package/commands/status.js +9 -1
- package/commands/workflow.js +7 -0
- package/package.json +1 -1
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// --- YAML Frontmatter Parser (regex-based, no deps) ---
|
|
5
|
+
|
|
6
|
+
function parseFrontmatter(content) {
|
|
7
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
8
|
+
if (!match) return null;
|
|
9
|
+
|
|
10
|
+
const yaml = match[1];
|
|
11
|
+
const result = {};
|
|
12
|
+
let currentKey = null;
|
|
13
|
+
|
|
14
|
+
for (const line of yaml.split('\n')) {
|
|
15
|
+
// List item: " - value"
|
|
16
|
+
const listMatch = line.match(/^\s+-\s+(.+)$/);
|
|
17
|
+
if (listMatch && currentKey) {
|
|
18
|
+
if (!Array.isArray(result[currentKey])) result[currentKey] = [];
|
|
19
|
+
result[currentKey].push(listMatch[1].trim());
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Nested key (one level): " key: value"
|
|
24
|
+
const nestedMatch = line.match(/^\s+([a-z_-]+):\s*(.*)$/);
|
|
25
|
+
if (nestedMatch && currentKey && typeof result[currentKey] === 'object' && !Array.isArray(result[currentKey])) {
|
|
26
|
+
result[currentKey][nestedMatch[1]] = nestedMatch[2].trim() || true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Top-level key: "key: value"
|
|
31
|
+
const kvMatch = line.match(/^([a-z_-]+):\s*(.*)$/);
|
|
32
|
+
if (kvMatch) {
|
|
33
|
+
currentKey = kvMatch[1];
|
|
34
|
+
const val = kvMatch[2].trim();
|
|
35
|
+
if (val === '') {
|
|
36
|
+
// Could be start of a list or nested object — leave empty, next lines fill it
|
|
37
|
+
result[currentKey] = {};
|
|
38
|
+
} else if (val.startsWith('[') && val.endsWith(']')) {
|
|
39
|
+
// Inline array: [a, b, "c d"]
|
|
40
|
+
result[currentKey] = val.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
|
|
41
|
+
} else {
|
|
42
|
+
result[currentKey] = val.replace(/^["']|["']$/g, '');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getFrontmatterRaw(content) {
|
|
51
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
52
|
+
return match ? match[1] : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Skill Discovery ---
|
|
56
|
+
|
|
57
|
+
function findAllSkills(skillsDir) {
|
|
58
|
+
if (!fs.existsSync(skillsDir)) return [];
|
|
59
|
+
|
|
60
|
+
const skills = [];
|
|
61
|
+
|
|
62
|
+
function walk(dir, prefix) {
|
|
63
|
+
const entries = fs.readdirSync(dir);
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
const fullPath = path.join(dir, entry);
|
|
66
|
+
const stat = fs.statSync(fullPath);
|
|
67
|
+
if (!stat.isDirectory()) continue;
|
|
68
|
+
|
|
69
|
+
const skillFile = path.join(fullPath, 'SKILL.md');
|
|
70
|
+
if (fs.existsSync(skillFile)) {
|
|
71
|
+
const content = fs.readFileSync(skillFile, 'utf8');
|
|
72
|
+
const folderName = prefix ? `${prefix}/${entry}` : entry;
|
|
73
|
+
skills.push({
|
|
74
|
+
folder: folderName,
|
|
75
|
+
leafFolder: entry,
|
|
76
|
+
path: skillFile,
|
|
77
|
+
content,
|
|
78
|
+
frontmatter: parseFrontmatter(content)
|
|
79
|
+
});
|
|
80
|
+
} else if (stat.isDirectory()) {
|
|
81
|
+
// Check nested (e.g., clawhub/atris)
|
|
82
|
+
walk(fullPath, prefix ? `${prefix}/${entry}` : entry);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
walk(skillsDir, '');
|
|
88
|
+
return skills;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --- Audit Checks ---
|
|
92
|
+
|
|
93
|
+
function runAuditChecks(skill) {
|
|
94
|
+
const fm = skill.frontmatter || {};
|
|
95
|
+
const checks = [];
|
|
96
|
+
|
|
97
|
+
// 1. name exists
|
|
98
|
+
checks.push({
|
|
99
|
+
id: 'name-exists',
|
|
100
|
+
severity: 'ERROR',
|
|
101
|
+
pass: !!fm.name,
|
|
102
|
+
message: fm.name ? `name: ${fm.name}` : 'missing name field'
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// 2. name is kebab-case
|
|
106
|
+
const isKebab = fm.name ? /^[a-z][a-z0-9-]*$/.test(fm.name) : false;
|
|
107
|
+
checks.push({
|
|
108
|
+
id: 'name-kebab',
|
|
109
|
+
severity: 'ERROR',
|
|
110
|
+
pass: isKebab,
|
|
111
|
+
message: isKebab ? 'valid kebab-case' : `"${fm.name || ''}" is not kebab-case`
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// 3. name matches folder
|
|
115
|
+
const nameMatchesFolder = fm.name === skill.leafFolder;
|
|
116
|
+
checks.push({
|
|
117
|
+
id: 'name-matches-folder',
|
|
118
|
+
severity: 'ERROR',
|
|
119
|
+
pass: nameMatchesFolder,
|
|
120
|
+
message: nameMatchesFolder
|
|
121
|
+
? `matches folder "${skill.leafFolder}"`
|
|
122
|
+
: `name "${fm.name}" != folder "${skill.leafFolder}"`
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// 4. description exists
|
|
126
|
+
checks.push({
|
|
127
|
+
id: 'desc-exists',
|
|
128
|
+
severity: 'ERROR',
|
|
129
|
+
pass: !!fm.description,
|
|
130
|
+
message: fm.description ? `${fm.description.length} chars` : 'missing description'
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// 5. description under 1024 chars
|
|
134
|
+
const descLen = (fm.description || '').length;
|
|
135
|
+
checks.push({
|
|
136
|
+
id: 'desc-length',
|
|
137
|
+
severity: 'WARN',
|
|
138
|
+
pass: descLen <= 1024,
|
|
139
|
+
message: `${descLen}/1024 chars`
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// 6. description has trigger/WHEN guidance
|
|
143
|
+
const desc = (fm.description || '').toLowerCase();
|
|
144
|
+
const hasTriggers = /use when|triggers? on|use for|use if/.test(desc);
|
|
145
|
+
checks.push({
|
|
146
|
+
id: 'desc-has-when',
|
|
147
|
+
severity: 'WARN',
|
|
148
|
+
pass: hasTriggers,
|
|
149
|
+
message: hasTriggers ? 'has WHEN guidance' : 'no trigger/WHEN phrases in description'
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// 7. no XML tags in content (skip placeholders like <name>, <keyword>, code blocks)
|
|
153
|
+
const xmlMatches = skill.content.match(/<[a-zA-Z][^>]*>/g) || [];
|
|
154
|
+
const placeholders = /^<(name|keyword|placeholder|value|type|path|file|dir|id|url|tag|description|your-|user-|project-|skill-)/i;
|
|
155
|
+
const realXml = xmlMatches.filter(t =>
|
|
156
|
+
!t.startsWith('<!--') && !t.startsWith('<!') && !placeholders.test(t)
|
|
157
|
+
);
|
|
158
|
+
checks.push({
|
|
159
|
+
id: 'no-xml-tags',
|
|
160
|
+
severity: 'ERROR',
|
|
161
|
+
pass: realXml.length === 0,
|
|
162
|
+
message: realXml.length === 0 ? 'clean' : `found XML: ${realXml.slice(0, 3).join(', ')}`
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// 8. version field present
|
|
166
|
+
checks.push({
|
|
167
|
+
id: 'version-exists',
|
|
168
|
+
severity: 'WARN',
|
|
169
|
+
pass: !!fm.version,
|
|
170
|
+
message: fm.version ? `v${fm.version}` : 'missing version field'
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// 9. tags field present
|
|
174
|
+
const hasTags = Array.isArray(fm.tags) && fm.tags.length > 0;
|
|
175
|
+
checks.push({
|
|
176
|
+
id: 'tags-exist',
|
|
177
|
+
severity: 'WARN',
|
|
178
|
+
pass: hasTags,
|
|
179
|
+
message: hasTags ? `${fm.tags.length} tags` : 'missing tags field'
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// 10. no README.md in skill folder
|
|
183
|
+
const skillDir = path.dirname(skill.path);
|
|
184
|
+
const hasReadme = fs.existsSync(path.join(skillDir, 'README.md'));
|
|
185
|
+
checks.push({
|
|
186
|
+
id: 'no-readme',
|
|
187
|
+
severity: 'WARN',
|
|
188
|
+
pass: !hasReadme,
|
|
189
|
+
message: hasReadme ? 'README.md found (should not exist in skill folder)' : 'clean'
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// 11. under 5000 words
|
|
193
|
+
const wordCount = skill.content.split(/\s+/).length;
|
|
194
|
+
checks.push({
|
|
195
|
+
id: 'word-count',
|
|
196
|
+
severity: 'INFO',
|
|
197
|
+
pass: wordCount <= 5000,
|
|
198
|
+
message: `${wordCount} words (limit: 5000)`
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// 12. has numbered steps
|
|
202
|
+
const hasSteps = /^\d+\.\s/m.test(skill.content);
|
|
203
|
+
checks.push({
|
|
204
|
+
id: 'has-steps',
|
|
205
|
+
severity: 'INFO',
|
|
206
|
+
pass: hasSteps,
|
|
207
|
+
message: hasSteps ? 'has numbered instructions' : 'no numbered instructions found'
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return checks;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function getStatus(checks) {
|
|
214
|
+
const hasError = checks.some(c => !c.pass && c.severity === 'ERROR');
|
|
215
|
+
const hasWarn = checks.some(c => !c.pass && c.severity === 'WARN');
|
|
216
|
+
if (hasError) return 'FAIL';
|
|
217
|
+
if (hasWarn) return 'WARN';
|
|
218
|
+
return 'PASS';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// --- Auto-Fix ---
|
|
222
|
+
|
|
223
|
+
function autoFix(skill, dryRun) {
|
|
224
|
+
const fixes = [];
|
|
225
|
+
let content = skill.content;
|
|
226
|
+
const fm = skill.frontmatter || {};
|
|
227
|
+
const rawFm = getFrontmatterRaw(content);
|
|
228
|
+
if (!rawFm) return fixes;
|
|
229
|
+
|
|
230
|
+
let newFmLines = rawFm.split('\n');
|
|
231
|
+
|
|
232
|
+
// Fix 1: name mismatch — replace name value with folder name
|
|
233
|
+
if (fm.name && fm.name !== skill.leafFolder) {
|
|
234
|
+
const idx = newFmLines.findIndex(l => l.match(/^name:\s/));
|
|
235
|
+
if (idx !== -1) {
|
|
236
|
+
newFmLines[idx] = `name: ${skill.leafFolder}`;
|
|
237
|
+
fixes.push(`Fixed name: "${fm.name}" -> "${skill.leafFolder}"`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Fix 2: merge triggers into description, remove triggers field
|
|
242
|
+
if (fm.triggers) {
|
|
243
|
+
const triggers = Array.isArray(fm.triggers) ? fm.triggers : [fm.triggers];
|
|
244
|
+
const triggerText = triggers.map(t => `"${t}"`).join(', ');
|
|
245
|
+
|
|
246
|
+
// Update description to include triggers
|
|
247
|
+
const descIdx = newFmLines.findIndex(l => l.match(/^description:\s/));
|
|
248
|
+
if (descIdx !== -1) {
|
|
249
|
+
let desc = fm.description || '';
|
|
250
|
+
if (!/triggers? on/i.test(desc)) {
|
|
251
|
+
desc = desc.replace(/\s*$/, '') + ` Triggers on ${triggerText}.`;
|
|
252
|
+
newFmLines[descIdx] = `description: ${desc}`;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Remove triggers field (could be inline or block list)
|
|
257
|
+
const trigIdx = newFmLines.findIndex(l => l.match(/^triggers:\s*/));
|
|
258
|
+
if (trigIdx !== -1) {
|
|
259
|
+
// Remove the triggers line and any following list items
|
|
260
|
+
let endIdx = trigIdx + 1;
|
|
261
|
+
while (endIdx < newFmLines.length && newFmLines[endIdx].match(/^\s+-\s/)) {
|
|
262
|
+
endIdx++;
|
|
263
|
+
}
|
|
264
|
+
newFmLines.splice(trigIdx, endIdx - trigIdx);
|
|
265
|
+
fixes.push(`Merged ${triggers.length} triggers into description, removed triggers field`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Fix 3: add version if missing
|
|
270
|
+
if (!fm.version) {
|
|
271
|
+
// Insert after description line
|
|
272
|
+
const descIdx = newFmLines.findIndex(l => l.match(/^description:\s/));
|
|
273
|
+
const insertAt = descIdx !== -1 ? descIdx + 1 : newFmLines.length;
|
|
274
|
+
newFmLines.splice(insertAt, 0, 'version: 1.0.0');
|
|
275
|
+
fixes.push('Added version: 1.0.0');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Fix 4: add tags if missing
|
|
279
|
+
const hasTags = Array.isArray(fm.tags) && fm.tags.length > 0;
|
|
280
|
+
if (!hasTags) {
|
|
281
|
+
const tags = generateTags(skill.leafFolder, fm.description || '');
|
|
282
|
+
const tagLines = ['tags:'].concat(tags.map(t => ` - ${t}`));
|
|
283
|
+
newFmLines = newFmLines.concat(tagLines);
|
|
284
|
+
fixes.push(`Added tags: [${tags.join(', ')}]`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Fix 5: remove XML tags from body
|
|
288
|
+
const fmEnd = content.indexOf('\n---', 4);
|
|
289
|
+
if (fmEnd !== -1) {
|
|
290
|
+
let body = content.substring(fmEnd);
|
|
291
|
+
const xmlPattern = /<([a-zA-Z][a-zA-Z0-9]*)[^>]*>([\s\S]*?)<\/\1>/g;
|
|
292
|
+
let xmlCount = 0;
|
|
293
|
+
const newBody = body.replace(xmlPattern, (match, tag, inner) => {
|
|
294
|
+
xmlCount++;
|
|
295
|
+
return `[${inner.trim()}]`;
|
|
296
|
+
});
|
|
297
|
+
if (xmlCount > 0) {
|
|
298
|
+
content = content.substring(0, fmEnd) + newBody;
|
|
299
|
+
fixes.push(`Replaced ${xmlCount} XML tag(s) with bracket notation`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (fixes.length > 0 && !dryRun) {
|
|
304
|
+
// Reconstruct file with new frontmatter
|
|
305
|
+
const fmEndIdx = content.indexOf('\n---', 4);
|
|
306
|
+
const bodyPart = content.substring(fmEndIdx + 4); // after closing ---
|
|
307
|
+
const newContent = '---\n' + newFmLines.join('\n') + '\n---' + bodyPart;
|
|
308
|
+
|
|
309
|
+
// Re-apply XML fixes to the full content
|
|
310
|
+
const xmlPattern = /<([a-zA-Z][a-zA-Z0-9]*)[^>]*>([\s\S]*?)<\/\1>/g;
|
|
311
|
+
const finalContent = newContent.replace(xmlPattern, (match, tag, inner) => {
|
|
312
|
+
return `[${inner.trim()}]`;
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
fs.writeFileSync(skill.path, finalContent, 'utf8');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return fixes;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function generateTags(folderName, description) {
|
|
322
|
+
const tags = [];
|
|
323
|
+
|
|
324
|
+
// Tag from folder name
|
|
325
|
+
tags.push(folderName);
|
|
326
|
+
|
|
327
|
+
// Extract keywords from description
|
|
328
|
+
const keywords = {
|
|
329
|
+
'workflow': ['workflow', 'plan', 'process', 'loop'],
|
|
330
|
+
'automation': ['automat', 'autonomous', 'loop', 'execute'],
|
|
331
|
+
'writing': ['writing', 'essay', 'draft', 'edit', 'copy'],
|
|
332
|
+
'frontend': ['frontend', 'ui', 'design', 'css', 'layout', 'component'],
|
|
333
|
+
'backend': ['backend', 'api', 'server', 'database', 'endpoint'],
|
|
334
|
+
'email': ['email', 'gmail', 'inbox', 'message'],
|
|
335
|
+
'memory': ['memory', 'history', 'journal', 'search', 'past'],
|
|
336
|
+
'metacognition': ['metacognition', 'think', 'stuck', 'self-check'],
|
|
337
|
+
'navigation': ['navigation', 'map', 'codebase', 'file:line'],
|
|
338
|
+
'anti-slop': ['slop', 'ai pattern', 'cleanup', 'humanize'],
|
|
339
|
+
'quality': ['audit', 'review', 'validate', 'improve']
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const descLower = description.toLowerCase();
|
|
343
|
+
for (const [tag, patterns] of Object.entries(keywords)) {
|
|
344
|
+
if (tag === folderName) continue; // Already added
|
|
345
|
+
if (patterns.some(p => descLower.includes(p))) {
|
|
346
|
+
tags.push(tag);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return tags.slice(0, 5); // Max 5 tags
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// --- Subcommand Handlers ---
|
|
354
|
+
|
|
355
|
+
function skillList() {
|
|
356
|
+
const skillsDir = path.join(process.cwd(), 'atris', 'skills');
|
|
357
|
+
const skills = findAllSkills(skillsDir);
|
|
358
|
+
|
|
359
|
+
if (skills.length === 0) {
|
|
360
|
+
console.log('No skills found in atris/skills/. Run "atris init" first.');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
console.log('');
|
|
365
|
+
console.log(' Skill Version Checks Status');
|
|
366
|
+
console.log(' ───── ─────── ────── ──────');
|
|
367
|
+
|
|
368
|
+
let needsAttention = 0;
|
|
369
|
+
for (const skill of skills) {
|
|
370
|
+
const checks = runAuditChecks(skill);
|
|
371
|
+
const passing = checks.filter(c => c.pass).length;
|
|
372
|
+
const total = checks.length;
|
|
373
|
+
const status = getStatus(checks);
|
|
374
|
+
const version = (skill.frontmatter || {}).version || '-';
|
|
375
|
+
const statusIcon = status === 'PASS' ? '\x1b[32mPASS\x1b[0m'
|
|
376
|
+
: status === 'WARN' ? '\x1b[33mWARN\x1b[0m'
|
|
377
|
+
: '\x1b[31mFAIL\x1b[0m';
|
|
378
|
+
|
|
379
|
+
if (status !== 'PASS') needsAttention++;
|
|
380
|
+
|
|
381
|
+
const name = skill.folder.padEnd(18);
|
|
382
|
+
const ver = version.padEnd(10);
|
|
383
|
+
const score = `${passing}/${total}`.padEnd(8);
|
|
384
|
+
console.log(` ${name} ${ver} ${score} ${statusIcon}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
console.log('');
|
|
388
|
+
if (needsAttention > 0) {
|
|
389
|
+
console.log(` ${needsAttention} skill(s) need attention. Run: atris skill audit --all`);
|
|
390
|
+
} else {
|
|
391
|
+
console.log(' All skills passing.');
|
|
392
|
+
}
|
|
393
|
+
console.log('');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function skillAudit(name) {
|
|
397
|
+
const skillsDir = path.join(process.cwd(), 'atris', 'skills');
|
|
398
|
+
const allSkills = findAllSkills(skillsDir);
|
|
399
|
+
|
|
400
|
+
const targets = name === '--all'
|
|
401
|
+
? allSkills
|
|
402
|
+
: allSkills.filter(s => s.folder === name || s.leafFolder === name);
|
|
403
|
+
|
|
404
|
+
if (targets.length === 0) {
|
|
405
|
+
console.error(`Skill "${name}" not found. Run "atris skill list" to see available skills.`);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
for (const skill of targets) {
|
|
410
|
+
const checks = runAuditChecks(skill);
|
|
411
|
+
const errors = checks.filter(c => !c.pass && c.severity === 'ERROR').length;
|
|
412
|
+
const warns = checks.filter(c => !c.pass && c.severity === 'WARN').length;
|
|
413
|
+
const passing = checks.filter(c => c.pass).length;
|
|
414
|
+
|
|
415
|
+
console.log('');
|
|
416
|
+
console.log(` Audit: ${skill.folder}`);
|
|
417
|
+
console.log(' ' + '─'.repeat(40));
|
|
418
|
+
|
|
419
|
+
for (const check of checks) {
|
|
420
|
+
const icon = check.pass ? '\x1b[32m\u2713\x1b[0m' : '\x1b[31m\u2717\x1b[0m';
|
|
421
|
+
const id = check.id.padEnd(22);
|
|
422
|
+
console.log(` ${icon} ${id} ${check.message}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
console.log('');
|
|
426
|
+
console.log(` Score: ${passing}/${checks.length} (${errors} errors, ${warns} warnings)`);
|
|
427
|
+
|
|
428
|
+
if (errors > 0 || warns > 0) {
|
|
429
|
+
console.log(` Run: atris skill fix ${skill.folder}`);
|
|
430
|
+
}
|
|
431
|
+
console.log('');
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function skillFix(name) {
|
|
436
|
+
const skillsDir = path.join(process.cwd(), 'atris', 'skills');
|
|
437
|
+
const allSkills = findAllSkills(skillsDir);
|
|
438
|
+
|
|
439
|
+
const targets = name === '--all'
|
|
440
|
+
? allSkills
|
|
441
|
+
: allSkills.filter(s => s.folder === name || s.leafFolder === name);
|
|
442
|
+
|
|
443
|
+
if (targets.length === 0) {
|
|
444
|
+
console.error(`Skill "${name}" not found. Run "atris skill list" to see available skills.`);
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
let totalFixes = 0;
|
|
449
|
+
|
|
450
|
+
for (const skill of targets) {
|
|
451
|
+
const fixes = autoFix(skill, false);
|
|
452
|
+
|
|
453
|
+
if (fixes.length > 0) {
|
|
454
|
+
console.log('');
|
|
455
|
+
console.log(` Fixing: ${skill.folder}`);
|
|
456
|
+
for (const fix of fixes) {
|
|
457
|
+
console.log(` \x1b[32m\u2713\x1b[0m ${fix}`);
|
|
458
|
+
}
|
|
459
|
+
totalFixes += fixes.length;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (totalFixes === 0) {
|
|
464
|
+
console.log('');
|
|
465
|
+
console.log(' Nothing to fix. All auto-fixable issues already resolved.');
|
|
466
|
+
} else {
|
|
467
|
+
console.log('');
|
|
468
|
+
console.log(` ${totalFixes} fix(es) applied. Run: atris skill audit ${name}`);
|
|
469
|
+
}
|
|
470
|
+
console.log('');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// --- Main Dispatcher ---
|
|
474
|
+
|
|
475
|
+
function skillCommand(subcommand, ...args) {
|
|
476
|
+
switch (subcommand) {
|
|
477
|
+
case 'list':
|
|
478
|
+
case 'ls':
|
|
479
|
+
return skillList();
|
|
480
|
+
case 'audit':
|
|
481
|
+
return skillAudit(args[0] || '--all');
|
|
482
|
+
case 'fix':
|
|
483
|
+
return skillFix(args[0] || '--all');
|
|
484
|
+
default:
|
|
485
|
+
console.log('');
|
|
486
|
+
console.log('Usage: atris skill <subcommand> [name]');
|
|
487
|
+
console.log('');
|
|
488
|
+
console.log('Subcommands:');
|
|
489
|
+
console.log(' list Show all skills with compliance status');
|
|
490
|
+
console.log(' audit [name|--all] Validate skill against Anthropic guide');
|
|
491
|
+
console.log(' fix [name|--all] Auto-fix common compliance issues');
|
|
492
|
+
console.log('');
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
module.exports = { skillCommand, parseFrontmatter, runAuditChecks, findAllSkills };
|
package/commands/status.js
CHANGED
|
@@ -146,9 +146,17 @@ function statusAtris(isQuick = false) {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
// Read lessons count
|
|
150
|
+
let lessonsCount = 0;
|
|
151
|
+
const lessonsFile = path.join(targetDir, 'lessons.md');
|
|
152
|
+
if (fs.existsSync(lessonsFile)) {
|
|
153
|
+
const lessonsContent = fs.readFileSync(lessonsFile, 'utf8');
|
|
154
|
+
lessonsCount = (lessonsContent.match(/^- \*\*/gm) || []).length;
|
|
155
|
+
}
|
|
156
|
+
|
|
149
157
|
// Quick mode: one-line summary
|
|
150
158
|
if (isQuick) {
|
|
151
|
-
console.log(`📥 ${inboxItems.length} | 📋 ${backlogTasks.length} | 🔨 ${inProgressTasks.length} | ✅ ${completions.length}`);
|
|
159
|
+
console.log(`📥 ${inboxItems.length} | 📋 ${backlogTasks.length} | 🔨 ${inProgressTasks.length} | ✅ ${completions.length} | 📚 ${lessonsCount}`);
|
|
152
160
|
return;
|
|
153
161
|
}
|
|
154
162
|
|
package/commands/workflow.js
CHANGED
|
@@ -116,6 +116,9 @@ async function planAtris(userInput = null) {
|
|
|
116
116
|
console.log(`- MAP: ${mapDisplay}`);
|
|
117
117
|
console.log(`- TODO: ${taskSourcePath || 'atris/TODO.md (missing)'}`);
|
|
118
118
|
console.log(`- Features index: ${featuresReadmeRef || 'atris/features/README.md (missing)'}`);
|
|
119
|
+
const lessonsPath = path.join(targetDir, 'lessons.md');
|
|
120
|
+
const lessonsRef = fs.existsSync(lessonsPath) ? path.relative(process.cwd(), lessonsPath) : null;
|
|
121
|
+
console.log(`- Lessons: ${lessonsRef || 'atris/lessons.md (none yet)'}`);
|
|
119
122
|
console.log(`- Journal (today): ${journalPath}`);
|
|
120
123
|
console.log('');
|
|
121
124
|
console.log(`📥 Inbox items: ${inboxCount}`);
|
|
@@ -148,6 +151,7 @@ async function planAtris(userInput = null) {
|
|
|
148
151
|
if (mapFileRef) console.log(`- ${mapFileRef}`);
|
|
149
152
|
if (taskSourcePath) console.log(`- ${taskSourcePath}`);
|
|
150
153
|
if (featuresReadmeRef) console.log(`- ${featuresReadmeRef}`);
|
|
154
|
+
if (lessonsRef) console.log(`- ${lessonsRef}`);
|
|
151
155
|
console.log(`- ${journalPath}`);
|
|
152
156
|
console.log('');
|
|
153
157
|
if (!mapFileRef || mapIsPlaceholder) {
|
|
@@ -847,6 +851,8 @@ async function reviewAtris() {
|
|
|
847
851
|
console.log('1) Run the project test suite (follow TESTING_GUIDE if present).');
|
|
848
852
|
console.log('2) Execute any `atris/features/*/validate.md` scripts; if a step fails, fix + rerun.');
|
|
849
853
|
console.log('3) Update TODO.md + today\'s journal with results; propose tool upgrades if drift repeats.');
|
|
854
|
+
console.log('4) If anything surprised you (broke, worked unexpectedly), append to atris/lessons.md:');
|
|
855
|
+
console.log(' - **[YYYY-MM-DD] [feature-name]** — (pass|fail) — One-line lesson');
|
|
850
856
|
console.log('');
|
|
851
857
|
console.log('Done when: ✅ All good. Ready for human testing.');
|
|
852
858
|
console.log('');
|
|
@@ -938,6 +944,7 @@ async function reviewAtris() {
|
|
|
938
944
|
userPrompt += ` • Update docs if needed (MAP.md, TODO.md)\n`;
|
|
939
945
|
userPrompt += ` • Clean TODO.md (move completed tasks to Completed section, then delete)\n`;
|
|
940
946
|
userPrompt += ` • Extract learnings for journal\n`;
|
|
947
|
+
userPrompt += ` • If anything surprised you, append to atris/lessons.md\n`;
|
|
941
948
|
userPrompt += ` • EVOLUTION: If you see drift in the logs, propose a tool upgrade.\n\n`;
|
|
942
949
|
userPrompt += `The cycle: do → review → [issues] → do → review → ✅ Ready\n`;
|
|
943
950
|
userPrompt += `Start validating now. Read files, run tests, verify implementation.`;
|
package/package.json
CHANGED