atris 3.25.1 → 3.26.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/bin/atris.js +32 -2
- package/commands/align.js +22 -9
- package/commands/brain.js +4 -2
- package/commands/card.js +121 -0
- package/commands/deck.js +184 -0
- package/commands/reel.js +128 -0
- package/commands/run.js +34 -1
- package/commands/site.js +48 -0
- package/commands/skill.js +13 -9
- package/commands/slop.js +307 -0
- package/commands/task.js +23 -5
- package/commands/theme.js +217 -0
- package/lib/card.js +120 -0
- package/lib/deck-from-md.js +110 -0
- package/lib/html-render.js +257 -0
- package/lib/memory-view.js +95 -0
- package/lib/reel.js +52 -0
- package/lib/site.js +114 -0
- package/lib/slides-deck.js +237 -0
- package/lib/task-proof.js +35 -0
- package/lib/theme.js +264 -0
- package/package.json +1 -1
package/commands/skill.js
CHANGED
|
@@ -622,15 +622,19 @@ curl -s "https://api.atris.ai/api/integrations/YOUR_INTEGRATION/items" \\
|
|
|
622
622
|
// --- CREATE subcommand ---
|
|
623
623
|
|
|
624
624
|
function skillCreate(nameArg, ...flags) {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
console.
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
625
|
+
// A flag-shaped first arg is not a skill name (e.g. `skill create --help`);
|
|
626
|
+
// show usage instead of creating a junk folder named "--help".
|
|
627
|
+
const wantsHelp = nameArg === '--help' || nameArg === '-h' || nameArg === 'help';
|
|
628
|
+
if (!nameArg || nameArg.startsWith('-')) {
|
|
629
|
+
const out = wantsHelp ? console.log : console.error;
|
|
630
|
+
out('Usage: atris skill create <name> [--integration] [--description="..."] [--local]');
|
|
631
|
+
out('');
|
|
632
|
+
out('Examples:');
|
|
633
|
+
out(' atris skill create daily-standup');
|
|
634
|
+
out(' atris skill create email-outreach --integration');
|
|
635
|
+
out(' atris skill create example-co/bol-processor --integration');
|
|
636
|
+
out(' atris skill create my-skill --local # project only, skip system dirs');
|
|
637
|
+
process.exit(wantsHelp ? 0 : 1);
|
|
634
638
|
}
|
|
635
639
|
|
|
636
640
|
const isIntegration = flags.includes('--integration');
|
package/commands/slop.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// atris slop — deterministic frontend-slop detector (no LLM).
|
|
2
|
+
//
|
|
3
|
+
// Steal-from-Impeccable, the Atris way: makes "looks AI-generated" concrete and
|
|
4
|
+
// CHECKABLE. A failure is a fact (file:line + rule), not a taste opinion — so it
|
|
5
|
+
// drops straight into the autopilot/review verification gate and CI. Each finding
|
|
6
|
+
// is the seed of a typed lesson; the ruleset is meant to GROW from lessons.md
|
|
7
|
+
// rather than be hand-curated forever.
|
|
8
|
+
//
|
|
9
|
+
// Zero external deps (Node built-ins only) — repo contract.
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// atris slop detect [path] # scan a file or dir (default: .)
|
|
13
|
+
// atris slop detect src/ --json # machine output for CI / the loop
|
|
14
|
+
// atris slop detect src/ --quiet # only print the summary line
|
|
15
|
+
//
|
|
16
|
+
// Exit code: 0 = clean, 1 = slop found, 2 = bad usage. CI/PR gates read this.
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { execFileSync } = require('child_process');
|
|
21
|
+
|
|
22
|
+
const SCAN_EXTS = new Set(['.css', '.scss', '.sass', '.less', '.tsx', '.jsx', '.ts', '.js', '.mjs', '.html', '.vue', '.svelte', '.astro',
|
|
23
|
+
'.md', '.mdx', '.txt']); // prose too: the voice doctrine (em-dash, hype-copy) is enforceable, not just advice
|
|
24
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.astro', 'coverage', '.cache', 'out', 'vendor']);
|
|
25
|
+
|
|
26
|
+
// Each rule is deterministic: a regex + a one-line why. severity drives the icon.
|
|
27
|
+
// Kept high-precision on purpose — a noisy gate gets muted, and a muted gate is dead.
|
|
28
|
+
const RULES = [
|
|
29
|
+
{ id: 'ai-gradient-text', sev: 'error',
|
|
30
|
+
re: /(text-transparent[^"'`]{0,40}bg-clip-text|bg-clip-text[^"'`]{0,40}text-transparent|-webkit-text-fill-color:\s*transparent|background-clip:\s*text)/i,
|
|
31
|
+
why: 'gradient-filled text headline: the #1 generated-look tell' },
|
|
32
|
+
{ id: 'ai-purple-gradient', sev: 'error',
|
|
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
|
+
why: 'purple/indigo gradient: default "AI startup" palette' },
|
|
35
|
+
{ id: 'ai-indigo-brand', sev: 'warn',
|
|
36
|
+
re: /(#6366f1|#4f46e5|#4338ca|(?:bg|text|border|from|to|ring)-indigo-(?:500|600|700)\b)/i,
|
|
37
|
+
why: 'canonical AI indigo used as brand color' },
|
|
38
|
+
{ id: 'glassmorphism', sev: 'warn',
|
|
39
|
+
re: /(backdrop-blur(?:-\w+)?\b|backdrop-filter:\s*blur)/i,
|
|
40
|
+
why: 'glassmorphism (frosted blur): overused default' },
|
|
41
|
+
{ id: 'over-rounding', sev: 'warn',
|
|
42
|
+
re: /(rounded-(?:3xl|\[(?:[2-9]\d?|1\d\d)(?:px|rem)\])|border-radius:\s*(?:2[4-9]|[3-9]\d|\d{3})px|border-radius:\s*(?:[2-9](?:\.\d+)?)rem)/i,
|
|
43
|
+
why: 'over-rounded corners (>=24px / rounded-3xl)' },
|
|
44
|
+
{ id: 'mega-shadow', sev: 'warn',
|
|
45
|
+
re: /(shadow-2xl\b|box-shadow:\s*0\s+\d{2,}px)/i,
|
|
46
|
+
why: 'oversized generic drop shadow (depth-by-blur)' },
|
|
47
|
+
{ id: 'side-stripe-card', sev: 'warn',
|
|
48
|
+
re: /border-(?:left|l)-(?:4|8|\[\d+px\])\b|border-left:\s*[3-9]px\s+solid/i,
|
|
49
|
+
why: 'accent side-stripe on a card: generated layout reflex' },
|
|
50
|
+
{ id: 'transition-all', sev: 'warn',
|
|
51
|
+
re: /\btransition-all\b|transition:\s*all\b/i,
|
|
52
|
+
why: 'transition-all: animate-everything laziness, not intent' },
|
|
53
|
+
{ id: 'pulse-animation', sev: 'warn',
|
|
54
|
+
re: /animation:[^;]*\binfinite\b|@keyframes\s+(pulse|ping|blink|glow|throb)\b|\banimate-(pulse|ping|bounce)\b/i,
|
|
55
|
+
why: 'looping pulse/ping/glow animation: distracting live-status reflex' },
|
|
56
|
+
{ id: 'eyebrow-caps', sev: 'warn',
|
|
57
|
+
re: /text-transform:\s*uppercase\b|\buppercase\b[^"'`]{0,30}tracking-|tracking-[^"'`]{0,30}\buppercase\b/i,
|
|
58
|
+
why: 'tracked all-caps eyebrow/label: dated reflex; use sentence case' },
|
|
59
|
+
{ id: 'decorative-emoji', sev: 'warn',
|
|
60
|
+
re: /[✨\u{1F680}\u{1F4A1}\u{1F525}\u{1F389}⚡\u{1F31F}\u{1FA84}\u{1F4AB}\u{1F44B}]/u,
|
|
61
|
+
why: 'decorative emoji in UI copy' },
|
|
62
|
+
{ id: 'em-dash', sev: 'warn',
|
|
63
|
+
re: /—/,
|
|
64
|
+
fix: (s) => s.replace(/\s*—\s*/g, ', '), // safe deterministic repair (prose)
|
|
65
|
+
why: 'em dash: a top AI-writing tell; use a comma, colon, or period' },
|
|
66
|
+
{ id: 'hype-copy', sev: 'error',
|
|
67
|
+
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
|
+
why: 'hype/marketing slop phrase: say the specific thing instead' },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const ICON = { error: '✗', warn: '⚠' }; // ✗ ⚠
|
|
72
|
+
|
|
73
|
+
const PROJECT_RULES_FILE = path.join('.atris', 'slop.rules.json');
|
|
74
|
+
|
|
75
|
+
// Compounding: projects grow their own anti-slop ruleset in .atris/slop.rules.json.
|
|
76
|
+
// Each entry: { id, pattern, flags?, why?, sev? }. Loaded on top of the built-ins.
|
|
77
|
+
function loadProjectRules(root = process.cwd()) {
|
|
78
|
+
try {
|
|
79
|
+
const raw = JSON.parse(fs.readFileSync(path.join(root, PROJECT_RULES_FILE), 'utf8'));
|
|
80
|
+
const arr = Array.isArray(raw) ? raw : (raw.rules || []);
|
|
81
|
+
return arr.map((r) => {
|
|
82
|
+
if (!r || !r.id || !r.pattern) return null;
|
|
83
|
+
let re; try { re = new RegExp(r.pattern, r.flags || 'i'); } catch { return null; }
|
|
84
|
+
return { id: r.id, sev: r.sev === 'error' ? 'error' : 'warn', re, why: r.why || r.id, project: true };
|
|
85
|
+
}).filter(Boolean);
|
|
86
|
+
} catch { return []; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function addProjectRule(rule, root = process.cwd()) {
|
|
90
|
+
const file = path.join(root, PROJECT_RULES_FILE);
|
|
91
|
+
let arr = [];
|
|
92
|
+
try { const raw = JSON.parse(fs.readFileSync(file, 'utf8')); arr = Array.isArray(raw) ? raw : (raw.rules || []); } catch {}
|
|
93
|
+
arr = arr.filter((r) => r.id !== rule.id);
|
|
94
|
+
arr.push(rule);
|
|
95
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
96
|
+
fs.writeFileSync(file, JSON.stringify(arr, null, 2) + '\n');
|
|
97
|
+
return file;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Map<absFile, Set<changedLineNumber>> from the working-tree (or --cached) git diff.
|
|
101
|
+
function gitChangedLines(staged, cwd = process.cwd()) {
|
|
102
|
+
const map = new Map();
|
|
103
|
+
let out;
|
|
104
|
+
try { out = execFileSync('git', ['diff', '--unified=0', ...(staged ? ['--cached'] : [])], { encoding: 'utf8', cwd }); }
|
|
105
|
+
catch { return map; }
|
|
106
|
+
let cur = null;
|
|
107
|
+
for (const line of out.split('\n')) {
|
|
108
|
+
const f = line.match(/^\+\+\+ b\/(.+)$/);
|
|
109
|
+
if (f) { cur = path.resolve(cwd, f[1]); map.set(cur, map.get(cur) || new Set()); continue; }
|
|
110
|
+
const h = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
|
111
|
+
if (h && cur) { const start = +h[1], count = h[2] != null ? +h[2] : 1; for (let i = 0; i < count; i++) map.get(cur).add(start + i); }
|
|
112
|
+
}
|
|
113
|
+
return map;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Install a pre-commit hook that gates staged changes through `atris slop detect --staged`.
|
|
117
|
+
// Idempotent and non-destructive: appends a marked block, skips gracefully if atris is absent.
|
|
118
|
+
function installHook(root = process.cwd()) {
|
|
119
|
+
if (!fs.existsSync(path.join(root, '.git'))) throw new Error('not a git repo (no .git here)');
|
|
120
|
+
const hookDir = path.join(root, '.git', 'hooks');
|
|
121
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
122
|
+
const hookPath = path.join(hookDir, 'pre-commit');
|
|
123
|
+
const marker = '# atris slop gate';
|
|
124
|
+
let content = '';
|
|
125
|
+
try { content = fs.readFileSync(hookPath, 'utf8'); } catch {}
|
|
126
|
+
if (content.includes(marker)) return { hookPath, already: true };
|
|
127
|
+
if (!content) content = '#!/bin/sh\n';
|
|
128
|
+
if (!content.endsWith('\n')) content += '\n';
|
|
129
|
+
content += `\n${marker}\nif command -v atris >/dev/null 2>&1; then atris slop detect --staged --quiet || exit 1; fi\n`;
|
|
130
|
+
fs.writeFileSync(hookPath, content);
|
|
131
|
+
fs.chmodSync(hookPath, 0o755);
|
|
132
|
+
return { hookPath, already: false };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Apply every fixable rule's safe transform in place. Returns { fixedCount, fixedFiles }.
|
|
136
|
+
function applyFixes(files, rules) {
|
|
137
|
+
const fixable = rules.filter((r) => typeof r.fix === 'function');
|
|
138
|
+
let fixedCount = 0; const fixedFiles = [];
|
|
139
|
+
for (const file of files) {
|
|
140
|
+
let text; try { text = fs.readFileSync(file, 'utf8'); } catch { continue; }
|
|
141
|
+
const lines = text.split('\n');
|
|
142
|
+
let touched = false;
|
|
143
|
+
for (let i = 0; i < lines.length; i++) {
|
|
144
|
+
let line = lines[i];
|
|
145
|
+
for (const r of fixable) { if (r.re.test(line)) { const nl = r.fix(line); if (nl !== line) { line = nl; fixedCount++; } } }
|
|
146
|
+
if (line !== lines[i]) { lines[i] = line; touched = true; }
|
|
147
|
+
}
|
|
148
|
+
if (touched) { fs.writeFileSync(file, lines.join('\n')); fixedFiles.push(file); }
|
|
149
|
+
}
|
|
150
|
+
return { fixedCount, fixedFiles };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function walk(target, out) {
|
|
154
|
+
let stat;
|
|
155
|
+
try { stat = fs.statSync(target); } catch { return out; }
|
|
156
|
+
if (stat.isFile()) {
|
|
157
|
+
if (SCAN_EXTS.has(path.extname(target))) out.push(target);
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
if (stat.isDirectory()) {
|
|
161
|
+
if (SKIP_DIRS.has(path.basename(target))) return out;
|
|
162
|
+
for (const name of fs.readdirSync(target)) {
|
|
163
|
+
if (name.startsWith('.') && name !== '.') continue;
|
|
164
|
+
walk(path.join(target, name), out);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function scanFile(file, rules = RULES) {
|
|
171
|
+
const findings = [];
|
|
172
|
+
let text;
|
|
173
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch { return findings; }
|
|
174
|
+
const lines = text.split('\n');
|
|
175
|
+
for (let i = 0; i < lines.length; i++) {
|
|
176
|
+
const line = lines[i];
|
|
177
|
+
for (const rule of rules) {
|
|
178
|
+
const m = rule.re.exec(line);
|
|
179
|
+
if (m) {
|
|
180
|
+
findings.push({
|
|
181
|
+
file, line: i + 1, rule: rule.id, sev: rule.sev, why: rule.why,
|
|
182
|
+
snippet: m[0].trim().slice(0, 48),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return findings;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function detect(argv) {
|
|
191
|
+
const json = argv.includes('--json');
|
|
192
|
+
const quiet = argv.includes('--quiet');
|
|
193
|
+
const doFix = argv.includes('--fix');
|
|
194
|
+
const staged = argv.includes('--staged');
|
|
195
|
+
const diffMode = staged || argv.includes('--diff');
|
|
196
|
+
const rules = RULES.concat(loadProjectRules());
|
|
197
|
+
|
|
198
|
+
// pick the file set: a git diff (changed files) or a path walk
|
|
199
|
+
let files, changed = null;
|
|
200
|
+
if (diffMode) {
|
|
201
|
+
changed = gitChangedLines(staged);
|
|
202
|
+
files = [...changed.keys()].filter((f) => SCAN_EXTS.has(path.extname(f)) && fs.existsSync(f));
|
|
203
|
+
} else {
|
|
204
|
+
const target = argv.find((a) => !a.startsWith('-')) || '.';
|
|
205
|
+
files = walk(path.resolve(target), []);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let fixed = null;
|
|
209
|
+
if (doFix) {
|
|
210
|
+
fixed = applyFixes(files, rules);
|
|
211
|
+
if (!json && fixed.fixedCount) {
|
|
212
|
+
console.log(`\n ✎ fixed ${fixed.fixedCount} tell${fixed.fixedCount === 1 ? '' : 's'} in ${fixed.fixedFiles.length} file${fixed.fixedFiles.length === 1 ? '' : 's'}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let findings = files.flatMap((f) => scanFile(f, rules));
|
|
217
|
+
if (diffMode && changed) findings = findings.filter((f) => changed.get(f.file) && changed.get(f.file).has(f.line));
|
|
218
|
+
const errors = findings.filter((f) => f.sev === 'error').length;
|
|
219
|
+
|
|
220
|
+
if (json) {
|
|
221
|
+
console.log(JSON.stringify({
|
|
222
|
+
ok: findings.length === 0, scanned: files.length,
|
|
223
|
+
mode: diffMode ? (staged ? 'staged' : 'diff') : 'path',
|
|
224
|
+
fixed: fixed ? fixed.fixedCount : 0,
|
|
225
|
+
slop: findings.length, errors,
|
|
226
|
+
findings: findings.map((f) => ({ ...f, file: path.relative(process.cwd(), f.file) })),
|
|
227
|
+
}, null, 2));
|
|
228
|
+
return findings.length ? 1 : 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const rel = (f) => path.relative(process.cwd(), f);
|
|
232
|
+
if (!quiet) {
|
|
233
|
+
if (!findings.length) {
|
|
234
|
+
console.log(`\n ✓ clean — no slop tells in ${files.length} file${files.length === 1 ? '' : 's'}`);
|
|
235
|
+
} else {
|
|
236
|
+
console.log('');
|
|
237
|
+
const w = Math.max(...findings.map((f) => `${rel(f.file)}:${f.line}`.length));
|
|
238
|
+
for (const f of findings) {
|
|
239
|
+
const loc = `${rel(f.file)}:${f.line}`.padEnd(w);
|
|
240
|
+
console.log(` ${ICON[f.sev]} ${loc} ${f.rule.padEnd(20)} ${f.why}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (findings.length) {
|
|
245
|
+
console.log(`\n ${findings.length} slop tell${findings.length === 1 ? '' : 's'} (${errors} error) · exit 1\n`);
|
|
246
|
+
} else if (quiet) {
|
|
247
|
+
console.log(` ✓ clean · exit 0`);
|
|
248
|
+
}
|
|
249
|
+
return findings.length ? 1 : 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function slopCommand(argv) {
|
|
253
|
+
const sub = argv[0];
|
|
254
|
+
if (!sub || sub === 'detect' || sub.startsWith('-') || !['detect', 'rules', 'help', 'hook', 'install-hook'].includes(sub)) {
|
|
255
|
+
// default + `detect`: scan. Bare `atris slop` scans cwd too.
|
|
256
|
+
const rest = sub === 'detect' ? argv.slice(1) : argv;
|
|
257
|
+
return detect(rest);
|
|
258
|
+
}
|
|
259
|
+
if (sub === 'hook' || sub === 'install-hook') {
|
|
260
|
+
try {
|
|
261
|
+
const { hookPath, already } = installHook();
|
|
262
|
+
console.log(already
|
|
263
|
+
? `\n already installed: ${path.relative(process.cwd(), hookPath)}\n`
|
|
264
|
+
: `\n ✓ slop pre-commit gate installed: ${path.relative(process.cwd(), hookPath)}\n every commit now runs: atris slop detect --staged\n`);
|
|
265
|
+
return 0;
|
|
266
|
+
} catch (e) { console.error(` ${e.message}`); return 2; }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (sub === 'rules') {
|
|
270
|
+
if (argv.includes('--add')) {
|
|
271
|
+
const rest = argv.slice(argv.indexOf('--add') + 1).filter((a) => !a.startsWith('-'));
|
|
272
|
+
const [id, pattern, ...whyParts] = rest;
|
|
273
|
+
if (!id || !pattern) { console.error(' usage: atris slop rules --add <id> <regex-pattern> <why...> [--sev error|warn]'); return 2; }
|
|
274
|
+
let valid = true; try { new RegExp(pattern, 'i'); } catch { valid = false; }
|
|
275
|
+
if (!valid) { console.error(` invalid regex: ${pattern}`); return 2; }
|
|
276
|
+
const sev = (argv[argv.indexOf('--sev') + 1] === 'error') ? 'error' : 'warn';
|
|
277
|
+
const file = addProjectRule({ id, pattern, why: whyParts.join(' ') || id, sev });
|
|
278
|
+
console.log(` ✓ added project rule "${id}" to ${path.relative(process.cwd(), file)}`);
|
|
279
|
+
return 0;
|
|
280
|
+
}
|
|
281
|
+
const project = loadProjectRules();
|
|
282
|
+
console.log('\n atris slop — deterministic rules:\n');
|
|
283
|
+
for (const r of RULES) console.log(` ${ICON[r.sev]} ${r.id.padEnd(20)} ${r.why}`);
|
|
284
|
+
for (const r of project) console.log(` ${ICON[r.sev]} ${r.id.padEnd(20)} ${r.why} (project)`);
|
|
285
|
+
console.log(`\n ${RULES.length} built-in${project.length ? ` + ${project.length} project` : ''} rule${RULES.length + project.length === 1 ? '' : 's'}\n`);
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
// help
|
|
289
|
+
console.log(`
|
|
290
|
+
atris slop — deterministic slop detector + repairer (no LLM)
|
|
291
|
+
|
|
292
|
+
atris slop detect [path] scan a file or dir (default: .)
|
|
293
|
+
atris slop detect --diff scan only changed lines (commit/PR gate)
|
|
294
|
+
atris slop detect --staged scan only staged changes (pre-commit hook)
|
|
295
|
+
atris slop detect --fix auto-repair the safe tells (em dashes), report the rest
|
|
296
|
+
atris slop detect [path] --json machine output for CI / the loop
|
|
297
|
+
atris slop rules list active rules (built-in + project)
|
|
298
|
+
atris slop rules --add <id> <pattern> <why> grow the project ruleset
|
|
299
|
+
atris slop hook install a pre-commit gate (runs --staged)
|
|
300
|
+
|
|
301
|
+
Project rules live in .atris/slop.rules.json and compound over time.
|
|
302
|
+
exit 0 = clean, 1 = slop found. Wire into PR checks and the autopilot gate.
|
|
303
|
+
`);
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = { slopCommand, detect, scanFile, RULES, loadProjectRules, addProjectRule, gitChangedLines, applyFixes, installHook };
|
package/commands/task.js
CHANGED
|
@@ -7,7 +7,7 @@ const fs = require('fs');
|
|
|
7
7
|
const http = require('http');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
|
-
const { taskProofState } = require('../lib/task-proof');
|
|
10
|
+
const { taskProofState, buildVerifiedProof } = require('../lib/task-proof');
|
|
11
11
|
const { evaluateAutoAccept, parseVerifyCommand } = require('../lib/auto-accept-certified');
|
|
12
12
|
const { extractReceiptEvidence } = require('../lib/receipt-evidence');
|
|
13
13
|
const escapeRegExp = require('../lib/escape-regexp');
|
|
@@ -96,6 +96,7 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
96
96
|
atris task say <id> "<message>" Add context to a task
|
|
97
97
|
atris task chat <id> "<message>" [--goal "..."] Refine a task chat + working goal
|
|
98
98
|
atris task ready <id> --proof "..." Agent proof ready; native goal can complete
|
|
99
|
+
atris task ready <id> --verify "<cmd>" Run <cmd>; only ready if it exits 0 (executed proof)
|
|
99
100
|
atris task review-chat <id> [--as <owner>] Start a task-owned /codex verification chat
|
|
100
101
|
atris task accept <id> [--proof "..."] Human accepts proof, marks done
|
|
101
102
|
atris task auto-accept-certified --dry-run [--strict-verify] [--limit <n>]
|
|
@@ -6317,12 +6318,29 @@ function cmdReady(args) {
|
|
|
6317
6318
|
console.error('atris task ready: id required');
|
|
6318
6319
|
process.exit(2);
|
|
6319
6320
|
}
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6321
|
+
// Two ways to prove: --proof "<note>" (claimed, pattern-checked, unchanged) or
|
|
6322
|
+
// --verify "<command>" which actually RUNS the command and gates ready on exit 0,
|
|
6323
|
+
// turning a claim into executed evidence. --verify can carry an optional --proof note.
|
|
6324
|
+
const proofFlag = flag(args, '--proof');
|
|
6325
|
+
const verifyFlag = flag(args, '--verify');
|
|
6326
|
+
let proof = typeof proofFlag === 'string' ? proofFlag : '';
|
|
6327
|
+
if (typeof verifyFlag === 'string' && verifyFlag.trim()) {
|
|
6328
|
+
const verified = buildVerifiedProof(verifyFlag, proof, undefined, { cwd: process.cwd() });
|
|
6329
|
+
if (!verified.ok) {
|
|
6330
|
+
const detail = verified.exit != null ? ` (exit ${verified.exit})` : (verified.signal ? ` (signal ${verified.signal})` : '');
|
|
6331
|
+
console.error(`atris task ready: verifier failed${detail}: ${verifyFlag}`);
|
|
6332
|
+
if (verified.output) console.error(verified.output);
|
|
6333
|
+
else if (verified.error) console.error(verified.error);
|
|
6334
|
+
process.exit(1);
|
|
6335
|
+
}
|
|
6336
|
+
proof = verified.proof;
|
|
6337
|
+
if (!wantsJson(args)) console.log(`✓ verified: \`${verifyFlag}\` exited 0`);
|
|
6338
|
+
}
|
|
6339
|
+
if (!proof) {
|
|
6340
|
+
console.error('atris task ready: --proof or --verify required');
|
|
6323
6341
|
process.exit(2);
|
|
6324
6342
|
}
|
|
6325
|
-
requireMeaningfulTaskProof('atris task ready',
|
|
6343
|
+
requireMeaningfulTaskProof('atris task ready', proof);
|
|
6326
6344
|
const lesson = flag(args, '--lesson') || '';
|
|
6327
6345
|
const nextTaskInput = normalizeReviewNextTaskInput(typeof flag(args, '--next') === 'string' ? flag(args, '--next') : '');
|
|
6328
6346
|
const actor = String(flag(args, '--as') || DEFAULT_OWNER);
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// atris theme — brand themes for the whole design system. Define your colors and
|
|
2
|
+
// fonts once in .atris/theme.json; every deck, HTML page, and site uses them.
|
|
3
|
+
//
|
|
4
|
+
// atris theme create guided interview -> your own theme (alias: new)
|
|
5
|
+
// atris theme edit <name> re-run the interview to tweak an existing theme
|
|
6
|
+
// atris theme init scaffold a starter .atris/theme.json
|
|
7
|
+
// atris theme list list built-in + project themes
|
|
8
|
+
// atris theme show <name> print a resolved theme
|
|
9
|
+
|
|
10
|
+
const readline = require('readline');
|
|
11
|
+
const {
|
|
12
|
+
mergedThemes, writeStarterTheme, loadProjectThemes, loadThemeRecipe, PROJECT_THEME_FILE,
|
|
13
|
+
MOODS, MOOD_NAMES, FONT_PERSONALITIES, buildTheme, themeWarnings, upsertProjectTheme,
|
|
14
|
+
resolveMode, isHex, normHex, hexToRgb,
|
|
15
|
+
} = require('../lib/theme');
|
|
16
|
+
const { THEMES: HTML_THEMES } = require('../lib/html-render');
|
|
17
|
+
|
|
18
|
+
// ---------- small terminal helpers ----------
|
|
19
|
+
function parseFlags(argv) {
|
|
20
|
+
const flags = {}; const pos = [];
|
|
21
|
+
for (let i = 0; i < argv.length; i++) {
|
|
22
|
+
const a = argv[i];
|
|
23
|
+
if (a === '-y' || a === '--yes') { flags.yes = true; continue; }
|
|
24
|
+
if (a.startsWith('--')) {
|
|
25
|
+
const key = a.slice(2);
|
|
26
|
+
const next = argv[i + 1];
|
|
27
|
+
if (next != null && !next.startsWith('--')) { flags[key] = next; i++; }
|
|
28
|
+
else flags[key] = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
pos.push(a);
|
|
32
|
+
}
|
|
33
|
+
return { flags, pos };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// a colored block when we're on a real terminal, two blank cells otherwise (clean pipes)
|
|
37
|
+
function sw(hex) {
|
|
38
|
+
const c = hexToRgb(hex);
|
|
39
|
+
if (!c || !process.stdout.isTTY) return ' ';
|
|
40
|
+
return `\x1b[48;2;${c.r};${c.g};${c.b}m \x1b[0m`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function printPreview(name, answers, theme) {
|
|
44
|
+
const c = theme.color;
|
|
45
|
+
const mode = resolveMode(answers.mood || 'editorial', answers.mode);
|
|
46
|
+
console.log(`\n ${name} · ${answers.mood || 'editorial'} ${mode}`);
|
|
47
|
+
const row = (label, hex) => console.log(` ${sw(hex)} ${label.padEnd(8)} ${hex}`);
|
|
48
|
+
row('bg', c.bg);
|
|
49
|
+
row('ink', c.ink);
|
|
50
|
+
row('accent', c.accent);
|
|
51
|
+
row('accent2', c.accent2);
|
|
52
|
+
console.log(` ${theme.fonts.display} display, ${theme.fonts.body} body`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------- the guided interview (tasteful vocabulary) ----------
|
|
56
|
+
async function interview(defaults = {}) {
|
|
57
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
58
|
+
const ask = (q) => new Promise((res) => rl.question(q, res));
|
|
59
|
+
const ans = {};
|
|
60
|
+
try {
|
|
61
|
+
console.log('\n let\'s make your theme. five quick choices, all changeable later.\n');
|
|
62
|
+
|
|
63
|
+
const dn = defaults.name || 'brand';
|
|
64
|
+
ans.name = (await ask(` 1/5 name it [${dn}]: `)).trim() || dn;
|
|
65
|
+
|
|
66
|
+
console.log('\n 2/5 what should it feel like?');
|
|
67
|
+
MOOD_NAMES.forEach((m, i) => console.log(` ${i + 1}. ${m.padEnd(10)} ${MOODS[m].blurb}`));
|
|
68
|
+
const dmIdx = Math.max(0, MOOD_NAMES.indexOf(defaults.mood || 'editorial'));
|
|
69
|
+
const moodPick = (await ask(` pick 1-${MOOD_NAMES.length} [${dmIdx + 1}]: `)).trim();
|
|
70
|
+
ans.mood = MOOD_NAMES[(parseInt(moodPick, 10) - 1)] || MOOD_NAMES[dmIdx];
|
|
71
|
+
const mood = MOODS[ans.mood];
|
|
72
|
+
|
|
73
|
+
if (mood.forceMode) {
|
|
74
|
+
ans.mode = mood.forceMode;
|
|
75
|
+
console.log(` (${ans.mood} is ${mood.forceMode}-only)`);
|
|
76
|
+
} else {
|
|
77
|
+
const dmode = defaults.mode || mood.defaultMode;
|
|
78
|
+
const mp = (await ask(`\n 3/5 light or dark? [${dmode[0]}]: `)).trim().toLowerCase();
|
|
79
|
+
ans.mode = mp.startsWith('l') ? 'light' : mp.startsWith('d') ? 'dark' : dmode;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log('\n 4/5 your accent — the one color that is unmistakably yours:');
|
|
83
|
+
mood.swatches.forEach((s, i) => console.log(` ${i + 1}. ${sw(s)} ${s}`));
|
|
84
|
+
const dac = normHex(defaults.accent) || mood.swatches[0];
|
|
85
|
+
const ap = (await ask(` pick 1-${mood.swatches.length}, or paste a hex [${dac}]: `)).trim();
|
|
86
|
+
if (!ap) ans.accent = dac;
|
|
87
|
+
else if (/^[1-9]$/.test(ap) && mood.swatches[parseInt(ap, 10) - 1]) ans.accent = mood.swatches[parseInt(ap, 10) - 1];
|
|
88
|
+
else if (isHex(ap)) ans.accent = normHex(ap);
|
|
89
|
+
else { console.log(` '${ap}' is not 1-${mood.swatches.length} or a hex, keeping ${dac}`); ans.accent = dac; }
|
|
90
|
+
|
|
91
|
+
console.log('\n 5/5 type personality:');
|
|
92
|
+
const fontKeys = Object.keys(FONT_PERSONALITIES);
|
|
93
|
+
fontKeys.forEach((k, i) => console.log(` ${i + 1}. ${k.padEnd(15)} ${FONT_PERSONALITIES[k].display} / ${FONT_PERSONALITIES[k].body}`));
|
|
94
|
+
const dfIdx = fontKeys.indexOf(typeof defaults.fonts === 'string' ? defaults.fonts : mood.fonts);
|
|
95
|
+
const fp = (await ask(` pick 1-${fontKeys.length} [${(dfIdx < 0 ? 0 : dfIdx) + 1}]: `)).trim();
|
|
96
|
+
ans.fonts = fontKeys[(parseInt(fp, 10) - 1)] || fontKeys[dfIdx < 0 ? 0 : dfIdx];
|
|
97
|
+
} finally {
|
|
98
|
+
rl.close();
|
|
99
|
+
}
|
|
100
|
+
return ans;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function confirmSync(q) {
|
|
104
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
105
|
+
return new Promise((res) => rl.question(q, (a) => { rl.close(); res(!String(a).trim().toLowerCase().startsWith('n')); }));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// shared create/edit flow. defaults seed an edit; flags override anything; interactive
|
|
109
|
+
// only when there's a TTY and no defining flags.
|
|
110
|
+
async function createFlow({ name, defaults = {}, flags }) {
|
|
111
|
+
const answers = { ...defaults };
|
|
112
|
+
if (flags.mood) answers.mood = flags.mood;
|
|
113
|
+
if (flags.mode) answers.mode = flags.mode;
|
|
114
|
+
if (flags.accent) answers.accent = flags.accent;
|
|
115
|
+
if (flags.accent2) answers.accent2 = flags.accent2;
|
|
116
|
+
if (flags.fonts) answers.fonts = flags.fonts;
|
|
117
|
+
let themeName = name || flags.name || defaults.name || 'brand';
|
|
118
|
+
|
|
119
|
+
const hasDefiningFlag = Boolean(flags.mood || flags.mode || flags.accent || flags.accent2 || flags.fonts || flags.name);
|
|
120
|
+
const interactive = Boolean(process.stdin.isTTY) && !flags.yes && !hasDefiningFlag;
|
|
121
|
+
|
|
122
|
+
if (interactive) {
|
|
123
|
+
const r = await interview({ name: themeName, ...answers });
|
|
124
|
+
Object.assign(answers, r);
|
|
125
|
+
themeName = r.name || themeName;
|
|
126
|
+
}
|
|
127
|
+
themeName = String(themeName).trim() || 'brand';
|
|
128
|
+
|
|
129
|
+
if (answers.mood && !MOODS[answers.mood]) { console.error(` unknown mood "${answers.mood}". try: ${MOOD_NAMES.join(', ')}`); return 2; }
|
|
130
|
+
if (answers.accent && !isHex(answers.accent)) { console.error(` not a hex color: ${answers.accent}`); return 2; }
|
|
131
|
+
if (answers.accent2 && !isHex(answers.accent2)) { console.error(` not a hex color: ${answers.accent2}`); return 2; }
|
|
132
|
+
if (answers.fonts && typeof answers.fonts === 'string' && !FONT_PERSONALITIES[answers.fonts]) {
|
|
133
|
+
console.error(` unknown font personality "${answers.fonts}". try: ${Object.keys(FONT_PERSONALITIES).join(', ')}`); return 2;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const theme = buildTheme(answers);
|
|
137
|
+
printPreview(themeName, answers, theme);
|
|
138
|
+
const warns = themeWarnings(theme);
|
|
139
|
+
if (warns.length) { console.log('\n heads up:'); warns.forEach((w) => console.log(` ! ${w}`)); }
|
|
140
|
+
|
|
141
|
+
if (interactive) {
|
|
142
|
+
const ok = await confirmSync('\n save this theme? [enter = yes, n = cancel] ');
|
|
143
|
+
if (!ok) { console.log(' cancelled, nothing written.\n'); return 0; }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const recipe = {
|
|
147
|
+
mood: answers.mood || 'editorial',
|
|
148
|
+
mode: resolveMode(answers.mood || 'editorial', answers.mode),
|
|
149
|
+
accent: theme.color.accent,
|
|
150
|
+
accent2: theme.color.accent2,
|
|
151
|
+
fonts: typeof answers.fonts === 'string' ? answers.fonts : (MOODS[answers.mood || 'editorial'].fonts),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
let res;
|
|
155
|
+
try { res = upsertProjectTheme(themeName, theme, process.cwd(), recipe); }
|
|
156
|
+
catch (e) { console.error(` ${e.message}`); return 1; }
|
|
157
|
+
|
|
158
|
+
console.log(`\n ✓ ${res.action} theme "${res.name}" in ${PROJECT_THEME_FILE} (${res.count} theme${res.count === 1 ? '' : 's'})`);
|
|
159
|
+
console.log(' use it anywhere:');
|
|
160
|
+
console.log(` atris deck from doc.md --html --theme ${res.name}`);
|
|
161
|
+
console.log(` atris site ./docs --theme ${res.name}`);
|
|
162
|
+
console.log(` tweak it later: atris theme edit ${res.name}\n`);
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function run(argv) {
|
|
167
|
+
const sub = argv[0];
|
|
168
|
+
const rest = argv.slice(1);
|
|
169
|
+
|
|
170
|
+
if (sub === 'create' || sub === 'new') {
|
|
171
|
+
const { flags, pos } = parseFlags(rest);
|
|
172
|
+
return createFlow({ name: pos[0], flags });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (sub === 'edit') {
|
|
176
|
+
const { flags, pos } = parseFlags(rest);
|
|
177
|
+
const name = pos[0] || flags.name;
|
|
178
|
+
if (!name) { console.error(' usage: atris theme edit <name>'); return 2; }
|
|
179
|
+
const existing = loadProjectThemes()[name];
|
|
180
|
+
if (!existing) { console.error(` no project theme "${name}". make one with: atris theme create ${name}`); return 2; }
|
|
181
|
+
// prefer the saved recipe; otherwise seed from the resolved colors/fonts
|
|
182
|
+
const recipe = loadThemeRecipe(name);
|
|
183
|
+
const defaults = recipe || { accent: existing.color.accent, accent2: existing.color.accent2, fonts: existing.fonts };
|
|
184
|
+
defaults.name = name;
|
|
185
|
+
return createFlow({ name, defaults, flags });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (sub === 'init') {
|
|
189
|
+
const { file, already } = writeStarterTheme();
|
|
190
|
+
console.log(already
|
|
191
|
+
? `\n already exists: ${file}\n customize it with: atris theme edit brand\n`
|
|
192
|
+
: `\n ✓ brand theme scaffolded: ${file}\n or build one by feel: atris theme create\n`);
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (sub === 'show') {
|
|
197
|
+
const name = rest[0];
|
|
198
|
+
const t = mergedThemes(HTML_THEMES)[name];
|
|
199
|
+
if (!t) { console.error(` unknown theme "${name}"`); return 2; }
|
|
200
|
+
console.log(JSON.stringify(t, null, 2));
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// list (default)
|
|
205
|
+
const project = loadProjectThemes();
|
|
206
|
+
const all = mergedThemes(HTML_THEMES);
|
|
207
|
+
console.log('\n atris themes:\n');
|
|
208
|
+
for (const name of Object.keys(all)) {
|
|
209
|
+
const tag = project[name] ? 'project' : 'built-in';
|
|
210
|
+
console.log(` ${sw(all[name].color.accent)} ${name.padEnd(12)} accent ${all[name].color.accent} bg ${all[name].color.bg} (${tag})`);
|
|
211
|
+
}
|
|
212
|
+
console.log(`\n make your own by feel: atris theme create`);
|
|
213
|
+
console.log(` themes live in ${PROJECT_THEME_FILE}. Used by deck, html, and site.\n`);
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = { run };
|