atris 3.35.0 → 3.37.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.
- package/AGENTS.md +37 -0
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +22 -4
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +2 -0
- package/ax +147 -19
- package/bin/atris.js +576 -267
- package/commands/activate.js +194 -88
- package/commands/agents.js +166 -0
- package/commands/autoland.js +459 -107
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/engine.js +993 -32
- package/commands/experiments.js +28 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/gm.js +23 -0
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +72 -44
- package/commands/interview.js +67 -1
- package/commands/land.js +152 -52
- package/commands/lifecycle.js +39 -3
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +511 -34
- package/commands/mission.js +3144 -338
- package/commands/next.js +137 -0
- package/commands/now.js +220 -25
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/stream.js +861 -0
- package/commands/study.js +693 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +1346 -117
- package/commands/team.js +73 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +234 -13
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +331 -34
- package/lib/autoland.js +319 -54
- package/lib/ax-auto-lane.js +96 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2219 -67
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/next-moves.js +327 -10
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +10 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +108 -29
- package/lib/task-proof.js +23 -1
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const LAYERS = [
|
|
7
|
+
{ key: 'features', dir: path.join('atris', 'features') },
|
|
8
|
+
{ key: 'team', dir: path.join('atris', 'team') },
|
|
9
|
+
{ key: 'wiki', dir: path.join('atris', 'wiki') },
|
|
10
|
+
{ key: 'logs', dir: path.join('atris', 'logs') },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const STOPWORDS = new Set([
|
|
14
|
+
'a', 'an', 'and', 'are', 'as', 'at', 'for', 'from', 'in', 'is', 'it',
|
|
15
|
+
'of', 'on', 'or', 'the', 'to', 'where', 'who',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const SYNONYM_MAP = {
|
|
19
|
+
billing: ['credits', 'stripe', 'subscription', 'connect', 'treasury'],
|
|
20
|
+
payments: ['credits', 'stripe', 'subscription', 'connect', 'treasury'],
|
|
21
|
+
payment: ['credits', 'stripe', 'subscription', 'connect', 'treasury'],
|
|
22
|
+
money: ['credits', 'stripe', 'subscription', 'connect', 'treasury'],
|
|
23
|
+
auth: ['oauth', 'identity', 'jwt', 'security', 'access'],
|
|
24
|
+
login: ['auth', 'oauth', 'identity', 'jwt', 'security', 'access'],
|
|
25
|
+
email: ['mail', 'inbox', 'ses', 'agent-mail', 'atrismail', 'postman'],
|
|
26
|
+
agents: ['agent', 'pulse', 'autopilot', 'fleet', 'member', 'persona', 'architect'],
|
|
27
|
+
database: ['db', 'schema', 'migration', 'supabase', 'sql'],
|
|
28
|
+
db: ['database', 'schema', 'migration', 'supabase', 'sql'],
|
|
29
|
+
slack: ['messaging', 'integration', 'integrations'],
|
|
30
|
+
messaging: ['slack', 'integration', 'integrations'],
|
|
31
|
+
pulse: ['autopilot', 'proactive-ops', 'autonomous'],
|
|
32
|
+
autonomous: ['pulse', 'autopilot', 'proactive-ops'],
|
|
33
|
+
browser: ['scraper', 'automation', 'browser-lead'],
|
|
34
|
+
tools: ['tool', 'toolkit', 'skill', 'skills'],
|
|
35
|
+
toolkit: ['tool', 'tools', 'skill', 'skills'],
|
|
36
|
+
apps: ['app', 'builder', 'workflow', 'create-app', 'app-pm', 'block-builder'],
|
|
37
|
+
workflow: ['app', 'apps', 'builder', 'create-app', 'app-pm', 'block-builder'],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function showSearchHelp() {
|
|
41
|
+
console.log('Usage: atris search <keyword> [--raw]');
|
|
42
|
+
console.log('Example: atris search auth');
|
|
43
|
+
console.log(' --raw Print the full file:line dump instead of the compact answer');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeText(value) {
|
|
47
|
+
return String(value || '').toLowerCase().trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toRelative(root, filePath) {
|
|
51
|
+
return path.relative(root, filePath).split(path.sep).join('/');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function queryTerms(query) {
|
|
55
|
+
const terms = new Set();
|
|
56
|
+
const normalized = normalizeText(query);
|
|
57
|
+
if (normalized) terms.add(normalized);
|
|
58
|
+
|
|
59
|
+
const tokens = normalized
|
|
60
|
+
.split(/[^a-z0-9._/-]+/)
|
|
61
|
+
.map(token => token.trim())
|
|
62
|
+
.filter(token => token && !STOPWORDS.has(token));
|
|
63
|
+
|
|
64
|
+
for (const token of tokens) {
|
|
65
|
+
terms.add(token);
|
|
66
|
+
if (token.endsWith('s') && token.length > 3) {
|
|
67
|
+
terms.add(token.slice(0, -1));
|
|
68
|
+
} else if (token.length > 3) {
|
|
69
|
+
terms.add(`${token}s`);
|
|
70
|
+
}
|
|
71
|
+
for (const synonym of SYNONYM_MAP[token] || []) {
|
|
72
|
+
terms.add(synonym);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return Array.from(terms)
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.sort((a, b) => b.length - a.length || a.localeCompare(b));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hasTerm(value, terms) {
|
|
82
|
+
const text = normalizeText(value);
|
|
83
|
+
return terms.some(term => text.includes(term));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function listMarkdownFiles(rootDir) {
|
|
87
|
+
if (!fs.existsSync(rootDir)) return [];
|
|
88
|
+
const files = [];
|
|
89
|
+
|
|
90
|
+
function walk(dir) {
|
|
91
|
+
let entries;
|
|
92
|
+
try {
|
|
93
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
94
|
+
} catch {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
entries
|
|
99
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
100
|
+
.forEach((entry) => {
|
|
101
|
+
const fullPath = path.join(dir, entry.name);
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
walk(fullPath);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!entry.isFile()) return;
|
|
107
|
+
if (/\.md$/i.test(entry.name)) files.push(fullPath);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
walk(rootDir);
|
|
112
|
+
return files;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function recencyMs(relativePath, stat) {
|
|
116
|
+
const match = relativePath.match(/(\d{4})-(\d{2})-(\d{2})/);
|
|
117
|
+
if (match) {
|
|
118
|
+
const ms = Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
119
|
+
if (Number.isFinite(ms)) return ms;
|
|
120
|
+
}
|
|
121
|
+
return Number(stat && stat.mtimeMs) || 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function scanLayer(root, layer, terms) {
|
|
125
|
+
const dir = path.join(root, layer.dir);
|
|
126
|
+
const result = {
|
|
127
|
+
key: layer.key,
|
|
128
|
+
dir: layer.dir.split(path.sep).join('/'),
|
|
129
|
+
exists: fs.existsSync(dir),
|
|
130
|
+
fileHits: [],
|
|
131
|
+
lineHits: [],
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
for (const filePath of listMarkdownFiles(dir)) {
|
|
135
|
+
let stat;
|
|
136
|
+
let content;
|
|
137
|
+
try {
|
|
138
|
+
stat = fs.statSync(filePath);
|
|
139
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
140
|
+
} catch {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const relativePath = toRelative(root, filePath);
|
|
145
|
+
const fileRecency = recencyMs(relativePath, stat);
|
|
146
|
+
const pathMatched = hasTerm(relativePath, terms);
|
|
147
|
+
const fileLineHits = [];
|
|
148
|
+
|
|
149
|
+
content.split(/\r?\n/).forEach((line, index) => {
|
|
150
|
+
if (!hasTerm(line, terms)) return;
|
|
151
|
+
const hit = {
|
|
152
|
+
layer: layer.key,
|
|
153
|
+
file: relativePath,
|
|
154
|
+
line: index + 1,
|
|
155
|
+
content: line.trim(),
|
|
156
|
+
recencyMs: fileRecency,
|
|
157
|
+
pathOnly: false,
|
|
158
|
+
};
|
|
159
|
+
fileLineHits.push(hit);
|
|
160
|
+
result.lineHits.push(hit);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (pathMatched || fileLineHits.length > 0) {
|
|
164
|
+
result.fileHits.push({
|
|
165
|
+
layer: layer.key,
|
|
166
|
+
file: relativePath,
|
|
167
|
+
pathMatched,
|
|
168
|
+
lineHits: fileLineHits,
|
|
169
|
+
recencyMs: fileRecency,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function compareByRecency(a, b) {
|
|
178
|
+
return (b.recencyMs || 0) - (a.recencyMs || 0)
|
|
179
|
+
|| a.file.localeCompare(b.file)
|
|
180
|
+
|| (a.line || 0) - (b.line || 0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function sortedFileHits(layerResult) {
|
|
184
|
+
return [...(layerResult.fileHits || [])].sort(compareByRecency);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function sortedLineHits(layerResult) {
|
|
188
|
+
return [...(layerResult.lineHits || [])].sort(compareByRecency);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function collectSearchResults(root, query) {
|
|
192
|
+
const terms = queryTerms(query);
|
|
193
|
+
const layers = {};
|
|
194
|
+
for (const layer of LAYERS) {
|
|
195
|
+
layers[layer.key] = scanLayer(root, layer, terms);
|
|
196
|
+
}
|
|
197
|
+
return { query, terms, layers };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function featurePath(file) {
|
|
201
|
+
const parts = file.split('/');
|
|
202
|
+
const index = parts.indexOf('features');
|
|
203
|
+
const slug = index >= 0 ? parts[index + 1] : null;
|
|
204
|
+
if (slug && slug !== 'README.md') return `atris/features/${slug}/`;
|
|
205
|
+
return file;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function ownerPath(root, file) {
|
|
209
|
+
const parts = file.split('/');
|
|
210
|
+
const index = parts.indexOf('team');
|
|
211
|
+
const member = index >= 0 && parts[index + 1]
|
|
212
|
+
? `atris/team/${parts[index + 1]}/MEMBER.md`
|
|
213
|
+
: null;
|
|
214
|
+
if (member && fs.existsSync(path.join(root, member))) return member;
|
|
215
|
+
return path.basename(file) === 'MEMBER.md' ? file : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function selectedFeature(results) {
|
|
219
|
+
const hit = sortedFileHits(results.layers.features)[0];
|
|
220
|
+
return hit ? featurePath(hit.file) : 'none';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function selectedOwner(root, results) {
|
|
224
|
+
for (const hit of sortedFileHits(results.layers.team)) {
|
|
225
|
+
const member = ownerPath(root, hit.file);
|
|
226
|
+
if (member) return member;
|
|
227
|
+
}
|
|
228
|
+
return 'none';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function selectedWiki(results) {
|
|
232
|
+
const hit = sortedFileHits(results.layers.wiki)[0];
|
|
233
|
+
return hit ? hit.file : 'gap - not indexed yet';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function hitCount(layerResult) {
|
|
237
|
+
return (layerResult.fileHits || []).reduce((total, fileHit) => {
|
|
238
|
+
if (fileHit.lineHits.length > 0) return total + fileHit.lineHits.length;
|
|
239
|
+
return total + (fileHit.pathMatched ? 1 : 0);
|
|
240
|
+
}, 0);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function totalHitCount(results) {
|
|
244
|
+
return LAYERS.reduce((total, layer) => total + hitCount(results.layers[layer.key]), 0);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function truncateLine(value, max = 140) {
|
|
248
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
249
|
+
if (text.length <= max) return text;
|
|
250
|
+
return `${text.slice(0, Math.max(0, max - 3))}...`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function renderCompactSearch(root, results) {
|
|
254
|
+
const lines = [
|
|
255
|
+
`Feature: ${selectedFeature(results)}`,
|
|
256
|
+
`Owner: ${selectedOwner(root, results)}`,
|
|
257
|
+
`Wiki: ${selectedWiki(results)}`,
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
const journalHits = sortedLineHits(results.layers.logs);
|
|
261
|
+
if (totalHitCount(results) === 0) {
|
|
262
|
+
lines.push('No matches found.');
|
|
263
|
+
}
|
|
264
|
+
if (journalHits.length === 0) {
|
|
265
|
+
lines.push('Journal: none');
|
|
266
|
+
return lines.join('\n');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const total = totalHitCount(results);
|
|
270
|
+
const limit = total > 5 ? 3 : 5;
|
|
271
|
+
lines.push(`Found ${journalHits.length} match${journalHits.length === 1 ? '' : 'es'}.`);
|
|
272
|
+
lines.push('Journal:');
|
|
273
|
+
for (const hit of journalHits.slice(0, limit)) {
|
|
274
|
+
lines.push(`- ${hit.file}:${hit.line}: ${truncateLine(hit.content)}`);
|
|
275
|
+
}
|
|
276
|
+
if (journalHits.length > limit) {
|
|
277
|
+
lines.push(`(${journalHits.length - limit} more journal hits; use --raw for full dump)`);
|
|
278
|
+
}
|
|
279
|
+
return lines.join('\n');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function rawHits(results) {
|
|
283
|
+
const hits = [];
|
|
284
|
+
for (const layer of LAYERS) {
|
|
285
|
+
for (const fileHit of sortedFileHits(results.layers[layer.key])) {
|
|
286
|
+
if (fileHit.lineHits.length > 0) {
|
|
287
|
+
hits.push(...fileHit.lineHits.sort((a, b) => (a.line || 0) - (b.line || 0)));
|
|
288
|
+
} else if (fileHit.pathMatched) {
|
|
289
|
+
hits.push({
|
|
290
|
+
layer: layer.key,
|
|
291
|
+
file: fileHit.file,
|
|
292
|
+
line: 1,
|
|
293
|
+
content: `(path match) ${fileHit.file}`,
|
|
294
|
+
recencyMs: fileHit.recencyMs,
|
|
295
|
+
pathOnly: true,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return hits;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function renderRawSearch(results) {
|
|
304
|
+
const hits = rawHits(results);
|
|
305
|
+
const lines = [
|
|
306
|
+
`Searching for "${results.query}" in atris/features, atris/team, atris/wiki, atris/logs...`,
|
|
307
|
+
'',
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
if (hits.length === 0) {
|
|
311
|
+
lines.push('No matches found.');
|
|
312
|
+
return lines.join('\n');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
lines.push(`Found ${hits.length} match${hits.length === 1 ? '' : 'es'}:`);
|
|
316
|
+
lines.push('');
|
|
317
|
+
for (const hit of hits) {
|
|
318
|
+
lines.push(`${hit.file}:${hit.line}`);
|
|
319
|
+
lines.push(` ${truncateLine(hit.content, 100)}`);
|
|
320
|
+
lines.push('');
|
|
321
|
+
}
|
|
322
|
+
return lines.join('\n').trimEnd();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function parseSearchArgs(args) {
|
|
326
|
+
const argv = Array.isArray(args) ? args : [];
|
|
327
|
+
const helpIndex = argv.findIndex(arg => arg === '--help' || arg === '-h');
|
|
328
|
+
const raw = argv.includes('--raw');
|
|
329
|
+
const query = argv.filter(arg => arg !== '--raw' && arg !== '--help' && arg !== '-h').join(' ').trim();
|
|
330
|
+
return { helpIndex, raw, query, argCount: argv.length };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function searchCommand(args = [], options = {}) {
|
|
334
|
+
const { helpIndex, raw, query, argCount } = parseSearchArgs(args);
|
|
335
|
+
|
|
336
|
+
if (helpIndex >= 0) {
|
|
337
|
+
showSearchHelp();
|
|
338
|
+
return argCount === 1 ? 0 : 1;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!query) {
|
|
342
|
+
showSearchHelp();
|
|
343
|
+
return 1;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const root = options.root || process.cwd();
|
|
347
|
+
const results = collectSearchResults(root, query);
|
|
348
|
+
if (totalHitCount(results) === 0 && !fs.existsSync(path.join(root, 'atris'))) {
|
|
349
|
+
// Outside a workspace the empty scan rows are noise; lead with the way in.
|
|
350
|
+
console.log('No atris folder here. Run atris init to set one up.');
|
|
351
|
+
return 0;
|
|
352
|
+
}
|
|
353
|
+
console.log(raw ? renderRawSearch(results) : renderCompactSearch(root, results));
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
module.exports = {
|
|
358
|
+
collectSearchResults,
|
|
359
|
+
renderCompactSearch,
|
|
360
|
+
renderRawSearch,
|
|
361
|
+
searchCommand,
|
|
362
|
+
showSearchHelp,
|
|
363
|
+
};
|
package/commands/skill.js
CHANGED
|
@@ -123,6 +123,52 @@ function findReadableSkills() {
|
|
|
123
123
|
|
|
124
124
|
// --- Audit Checks ---
|
|
125
125
|
|
|
126
|
+
function stripMarkdownCode(content) {
|
|
127
|
+
let fenceLength = 0;
|
|
128
|
+
|
|
129
|
+
return content.split('\n').map((line) => {
|
|
130
|
+
if (fenceLength > 0) {
|
|
131
|
+
const closingFence = line.match(/^ {0,3}(`+)[ \t]*$/);
|
|
132
|
+
if (closingFence && closingFence[1].length >= fenceLength) fenceLength = 0;
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const openingFence = line.match(/^ {0,3}(`{3,})([^`]*)$/);
|
|
137
|
+
if (openingFence) {
|
|
138
|
+
fenceLength = openingFence[1].length;
|
|
139
|
+
return '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let prose = '';
|
|
143
|
+
for (let index = 0; index < line.length;) {
|
|
144
|
+
if (line[index] !== '`') {
|
|
145
|
+
prose += line[index++];
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const start = index;
|
|
150
|
+
while (line[index] === '`') index++;
|
|
151
|
+
const delimiterLength = index - start;
|
|
152
|
+
let closingStart = index;
|
|
153
|
+
|
|
154
|
+
while (closingStart < line.length) {
|
|
155
|
+
closingStart = line.indexOf('`', closingStart);
|
|
156
|
+
if (closingStart === -1) break;
|
|
157
|
+
let closingEnd = closingStart;
|
|
158
|
+
while (line[closingEnd] === '`') closingEnd++;
|
|
159
|
+
if (closingEnd - closingStart === delimiterLength) {
|
|
160
|
+
index = closingEnd;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
closingStart = closingEnd;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (closingStart === -1) prose += line.slice(start, index);
|
|
167
|
+
}
|
|
168
|
+
return prose;
|
|
169
|
+
}).join('\n');
|
|
170
|
+
}
|
|
171
|
+
|
|
126
172
|
function runAuditChecks(skill) {
|
|
127
173
|
const fm = skill.frontmatter || {};
|
|
128
174
|
const checks = [];
|
|
@@ -183,9 +229,7 @@ function runAuditChecks(skill) {
|
|
|
183
229
|
});
|
|
184
230
|
|
|
185
231
|
// 7. no XML tags in content (skip placeholders like <name>, <keyword>, code blocks)
|
|
186
|
-
const proseContent = skill.content
|
|
187
|
-
.replace(/```[\s\S]*?```/g, '') // fenced code blocks
|
|
188
|
-
.replace(/`[^`\n]*`/g, ''); // inline code spans
|
|
232
|
+
const proseContent = stripMarkdownCode(skill.content);
|
|
189
233
|
const xmlMatches = proseContent.match(/<[a-zA-Z][^>]*>/g) || [];
|
|
190
234
|
// Single-letter tags like <X>/<N> are prose placeholders, not real XML.
|
|
191
235
|
const placeholders = /^<(name|keyword|placeholder|value|type|path|file|dir|id|url|tag|description|your-|user-|project-|skill-|[a-zA-Z]>)/i;
|
package/commands/slop.js
CHANGED
|
@@ -32,6 +32,9 @@ const RULES = [
|
|
|
32
32
|
{ id: 'ai-purple-gradient', sev: 'error',
|
|
33
33
|
re: /((from|via|to)-(purple|violet|indigo|fuchsia)-\d{2,3}\b|linear-gradient\([^)]*(#6366f1|#8b5cf6|#a855f7|#7c3aed|#4f46e5|\bpurple\b|\bviolet\b|\bindigo\b))/i,
|
|
34
34
|
why: 'purple/indigo gradient: default "AI startup" palette' },
|
|
35
|
+
{ id: 'rainbow-gradient', sev: 'warn',
|
|
36
|
+
re: /\bfrom-([a-z]+)-(?:[3-6]00)\b[^"'`]{0,60}\bto-(?!\1-)([a-z]+)-(?:[3-6]00)\b/i,
|
|
37
|
+
why: 'saturated two-color gradient fill across different hues (pink to orange, blue to cyan): the rainbow CTA reflex; use one solid brand color' },
|
|
35
38
|
{ id: 'ai-indigo-brand', sev: 'warn',
|
|
36
39
|
re: /(#6366f1|#4f46e5|#4338ca|(?:bg|text|border|from|to|ring)-indigo-(?:500|600|700)\b)/i,
|
|
37
40
|
why: 'canonical AI indigo used as brand color' },
|
|
@@ -44,6 +47,9 @@ const RULES = [
|
|
|
44
47
|
{ id: 'mega-shadow', sev: 'warn',
|
|
45
48
|
re: /(shadow-2xl\b|box-shadow:\s*0\s+\d{2,}px)/i,
|
|
46
49
|
why: 'oversized generic drop shadow (depth-by-blur)' },
|
|
50
|
+
{ id: 'colored-glow-shadow', sev: 'warn',
|
|
51
|
+
re: /\bshadow-(?:indigo|violet|purple|fuchsia|blue|sky|cyan|pink|rose|emerald|green|teal)-(?:400|500|600)(?:\/\d{1,3})?\b/i,
|
|
52
|
+
why: 'colored glowing drop shadow under a button (shadow-<hue>-500): the neon-glow AI reflex; keep shadows soft and neutral' },
|
|
47
53
|
{ id: 'side-stripe-card', sev: 'warn',
|
|
48
54
|
re: /border-(?:left|l)-(?:4|8|\[\d+px\])\b|border-left:\s*[3-9]px\s+solid/i,
|
|
49
55
|
why: 'accent side-stripe on a card: generated layout reflex' },
|
|
@@ -53,6 +59,15 @@ const RULES = [
|
|
|
53
59
|
{ id: 'pulse-animation', sev: 'warn',
|
|
54
60
|
re: /animation:[^;]*\binfinite\b|@keyframes\s+(pulse|ping|blink|glow|throb)\b|\banimate-(pulse|ping|bounce)\b/i,
|
|
55
61
|
why: 'looping pulse/ping/glow animation: distracting live-status reflex' },
|
|
62
|
+
{ id: 'hover-lift', sev: 'warn',
|
|
63
|
+
re: /\bhover:(?:-translate-y-(?:0\.5|[1-9]\d?)|scale-1(?:1[0-9]|[2-9]\d))\b/,
|
|
64
|
+
why: 'card lifts/over-grows on hover (translate-y shift or scale past 1.05): the generated-card reflex; keep hover scale subtle and never shift content' },
|
|
65
|
+
{ id: 'decorative-blur-blob', sev: 'warn',
|
|
66
|
+
re: /\bblur-3xl\b|filter:\s*blur\(\s*(?:6[4-9]|[7-9]\d|\d{3,})px\)/i,
|
|
67
|
+
why: 'giant blurred gradient orb behind the hero (blur-3xl / 64px+ blur): the AI aurora-blob background reflex' },
|
|
68
|
+
{ id: 'dark-hero-gradient', sev: 'warn',
|
|
69
|
+
re: /\b(?:from|via)-(?:slate|gray|zinc|neutral|stone)-9(?:00|50)\b[^"'`]{0,80}\bto-(?:black|(?:slate|gray|zinc|neutral|stone)-950)\b/i,
|
|
70
|
+
why: 'near-black slate/gray hero gradient fading to black: the dark premium SaaS template look' },
|
|
56
71
|
{ id: 'eyebrow-caps', sev: 'warn',
|
|
57
72
|
re: /text-transform:\s*uppercase\b|\buppercase\b[^"'`]{0,30}tracking-|tracking-[^"'`]{0,30}\buppercase\b/i,
|
|
58
73
|
why: 'tracked all-caps eyebrow/label: dated reflex; use sentence case' },
|
|
@@ -66,6 +81,23 @@ const RULES = [
|
|
|
66
81
|
{ id: 'hype-copy', sev: 'error',
|
|
67
82
|
re: /\b(boost your productivity|supercharge|unleash|game[- ]?chang(?:er|ing)|seamlessly|effortlessly|revolutioniz(?:e|ing)|take your .{1,30} to the next level|elevate your|cutting[- ]edge|powered by ai|next[- ]generation)\b/i,
|
|
68
83
|
why: 'hype/marketing slop phrase: say the specific thing instead' },
|
|
84
|
+
{ id: 'kicker-dot-caps', sev: 'error',
|
|
85
|
+
re: /[●◉]\s*[A-Z][A-Z0-9]+(?:\s+[A-Z0-9]+)*\b/,
|
|
86
|
+
notFiles: /\.(md|mdx|txt)$/i, // prose/wireframe docs use ● as radio buttons and transport glyphs
|
|
87
|
+
why: 'dot char + all-caps label: the status-dot eyebrow kicker, the #1 banned hero pattern' },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// Pair rules: two regexes that must BOTH hit within a small line window. Catches
|
|
91
|
+
// composites that span JSX lines (a dot element on one line, its label on the next).
|
|
92
|
+
const PAIR_RULES = [
|
|
93
|
+
{ id: 'hero-kicker', sev: 'error', span: 4,
|
|
94
|
+
a: /rounded-full[^"'`]{0,80}\b[wh]-(?:1(?:\.5)?|2(?:\.5)?|3)\b|\b[wh]-(?:1(?:\.5)?|2(?:\.5)?|3)\b[^"'`]{0,80}rounded-full/i,
|
|
95
|
+
b: /font-mono|tracking-(?:wide(?:r|st)?|\[)|text-\[?1[01]px|letter-spacing:\s*0?\.\d/i,
|
|
96
|
+
why: 'tiny status dot next to a small mono/tracked kicker label: banned hero-kicker composite' },
|
|
97
|
+
{ id: 'pastel-icon-tile', sev: 'warn', span: 2,
|
|
98
|
+
a: /\bbg-(?:indigo|violet|purple|fuchsia|blue|sky|cyan|teal|emerald|green|lime|amber|orange|rose|pink|red)-(?:50|100)\b/i,
|
|
99
|
+
b: /\brounded(?:-(?:sm|md|lg|xl|2xl))?\b[^"'`]{0,80}\b(?:size-|[wh]-)(?:8|9|10|11|12|14|16)\b|\b(?:size-|[wh]-)(?:8|9|10|11|12|14|16)\b[^"'`]{0,80}\brounded(?:-(?:sm|md|lg|xl|2xl))?\b/i,
|
|
100
|
+
why: 'pastel-tinted rounded icon tile (small light-100 square holding an icon): the generated feature-card reflex' },
|
|
69
101
|
];
|
|
70
102
|
|
|
71
103
|
const ICON = { error: '✗', warn: '⚠' }; // ✗ ⚠
|
|
@@ -167,7 +199,7 @@ function walk(target, out) {
|
|
|
167
199
|
return out;
|
|
168
200
|
}
|
|
169
201
|
|
|
170
|
-
function scanFile(file, rules = RULES) {
|
|
202
|
+
function scanFile(file, rules = RULES, pairRules = PAIR_RULES) {
|
|
171
203
|
const findings = [];
|
|
172
204
|
let text;
|
|
173
205
|
try { text = fs.readFileSync(file, 'utf8'); } catch { return findings; }
|
|
@@ -175,6 +207,7 @@ function scanFile(file, rules = RULES) {
|
|
|
175
207
|
for (let i = 0; i < lines.length; i++) {
|
|
176
208
|
const line = lines[i];
|
|
177
209
|
for (const rule of rules) {
|
|
210
|
+
if (rule.notFiles && rule.notFiles.test(file)) continue;
|
|
178
211
|
const m = rule.re.exec(line);
|
|
179
212
|
if (m) {
|
|
180
213
|
findings.push({
|
|
@@ -184,6 +217,21 @@ function scanFile(file, rules = RULES) {
|
|
|
184
217
|
}
|
|
185
218
|
}
|
|
186
219
|
}
|
|
220
|
+
for (const pr of pairRules) {
|
|
221
|
+
for (let i = 0; i < lines.length; i++) {
|
|
222
|
+
if (!pr.a.test(lines[i])) continue;
|
|
223
|
+
const lo = Math.max(0, i - pr.span), hi = Math.min(lines.length - 1, i + pr.span);
|
|
224
|
+
for (let j = lo; j <= hi; j++) {
|
|
225
|
+
if (pr.b.test(lines[j])) {
|
|
226
|
+
findings.push({
|
|
227
|
+
file, line: i + 1, rule: pr.id, sev: pr.sev, why: pr.why,
|
|
228
|
+
snippet: lines[i].trim().slice(0, 48),
|
|
229
|
+
});
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
187
235
|
return findings;
|
|
188
236
|
}
|
|
189
237
|
|
|
@@ -304,4 +352,4 @@ function slopCommand(argv) {
|
|
|
304
352
|
return 0;
|
|
305
353
|
}
|
|
306
354
|
|
|
307
|
-
module.exports = { slopCommand, detect, scanFile, RULES, loadProjectRules, addProjectRule, gitChangedLines, applyFixes, installHook };
|
|
355
|
+
module.exports = { slopCommand, detect, scanFile, RULES, PAIR_RULES, loadProjectRules, addProjectRule, gitChangedLines, applyFixes, installHook };
|
package/commands/soul.js
CHANGED
|
@@ -121,7 +121,7 @@ function displaySoul(soul) {
|
|
|
121
121
|
const line = '─'.repeat(W);
|
|
122
122
|
|
|
123
123
|
console.log(`\n ┌${line}┐`);
|
|
124
|
-
console.log(` │ ${'
|
|
124
|
+
console.log(` │ ${'soul · ' + soul.project}${' '.repeat(Math.max(0, W - 8 - soul.project.length))}│`);
|
|
125
125
|
console.log(` ├${line}┤`);
|
|
126
126
|
|
|
127
127
|
// Identity
|