atris 3.16.1 → 3.22.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/README.md +32 -7
- package/atris/skills/atris/SKILL.md +15 -2
- package/atris/skills/atris-feedback/SKILL.md +7 -0
- package/atris/skills/design/SKILL.md +29 -2
- package/atris/skills/engines/SKILL.md +44 -0
- package/atris/skills/flow/SKILL.md +1 -1
- package/atris/skills/wake/SKILL.md +37 -0
- package/atris/skills/youtube/SKILL.md +13 -39
- package/atris/team/validator/MEMBER.md +1 -0
- package/atris/wiki/concepts/agent-activation-contract.md +3 -3
- package/atris/wiki/concepts/workspace-initialization-contract.md +3 -3
- package/atris/wiki/index.md +1 -0
- package/atris.md +43 -19
- package/bin/atris.js +413 -31
- package/commands/agent-spawn.js +480 -0
- package/commands/analytics.js +6 -3
- package/commands/apps.js +11 -0
- package/commands/autopilot.js +42 -18
- package/commands/brain.js +74 -7
- package/commands/brainstorm.js +9 -58
- package/commands/clean.js +1 -4
- package/commands/compile.js +9 -4
- package/commands/console.js +8 -3
- package/commands/deck.js +184 -0
- package/commands/init.js +22 -11
- package/commands/lesson.js +76 -0
- package/commands/member.js +252 -48
- package/commands/mission.js +405 -13
- package/commands/now.js +4 -2
- package/commands/probe.js +105 -27
- package/commands/pulse.js +504 -0
- package/commands/radar.js +1 -0
- package/commands/recap.js +71 -25
- package/commands/run.js +615 -22
- package/commands/site.js +48 -0
- package/commands/slop.js +307 -0
- package/commands/spaceship.js +39 -0
- package/commands/sync.js +0 -2
- package/commands/task.js +429 -37
- package/commands/theme.js +217 -0
- package/commands/verify.js +7 -3
- package/lib/activity-stream.js +166 -0
- package/lib/auto-accept-certified.js +23 -1
- package/lib/context-gatherer.js +170 -0
- package/lib/deck-from-md.js +110 -0
- package/lib/escape-regexp.js +13 -0
- package/lib/file-ops.js +6 -3
- package/lib/html-render.js +257 -0
- package/lib/journal.js +1 -1
- package/lib/lesson-contradiction.js +113 -0
- package/lib/memory-view.js +95 -0
- package/lib/policy-lessons.js +3 -2
- package/lib/pulse.js +401 -0
- package/lib/runner-command.js +156 -0
- package/lib/site.js +114 -0
- package/lib/slides-deck.js +237 -0
- package/lib/state-detection.js +1 -4
- package/lib/task-db.js +101 -4
- package/lib/task-proof.js +1 -1
- package/lib/theme.js +264 -0
- package/lib/todo-fallback.js +2 -1
- package/lib/todo-sections.js +33 -0
- package/package.json +1 -2
- package/utils/api.js +14 -2
- package/atris/atrisDev.md +0 -717
package/commands/site.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// atris site — turn a folder of markdown (docs, your wiki, memory) into a
|
|
2
|
+
// beautiful, navigable static site in the design system. Built on lib/site.js.
|
|
3
|
+
//
|
|
4
|
+
// atris site <dir|doc.md> [--out dist] [--theme atris|terminal|paper] [--title T] [--serve]
|
|
5
|
+
|
|
6
|
+
const { buildSite, serveSite } = require('../lib/site');
|
|
7
|
+
|
|
8
|
+
function flag(argv, name) { const i = argv.indexOf(name); return i !== -1 ? argv[i + 1] : null; }
|
|
9
|
+
function hasFlag(argv, name) { return argv.includes(name); }
|
|
10
|
+
|
|
11
|
+
async function run(argv) {
|
|
12
|
+
const input = argv.find((a) => !a.startsWith('-'));
|
|
13
|
+
if (!input || input === 'help' || hasFlag(argv, '--help')) {
|
|
14
|
+
console.log(`
|
|
15
|
+
atris site — a beautiful static site from a folder of markdown
|
|
16
|
+
|
|
17
|
+
atris site <dir|doc.md> [--out dist] [--theme atris|terminal|paper] [--title T]
|
|
18
|
+
atris site atris/wiki --title "Atris Wiki" --serve
|
|
19
|
+
|
|
20
|
+
Each .md becomes a page; an index links them all. Same anti-slop design system,
|
|
21
|
+
semantic data-atris-block sections, ready for the web app. --serve previews it.
|
|
22
|
+
`);
|
|
23
|
+
return input === 'help' || hasFlag(argv, '--help') ? 0 : 2;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let res;
|
|
27
|
+
try {
|
|
28
|
+
res = buildSite(input, {
|
|
29
|
+
out: flag(argv, '--out') || 'dist',
|
|
30
|
+
theme: flag(argv, '--theme'),
|
|
31
|
+
title: flag(argv, '--title'),
|
|
32
|
+
brand: flag(argv, '--brand'),
|
|
33
|
+
});
|
|
34
|
+
} catch (e) { console.error(` ${e.message}`); return 2; }
|
|
35
|
+
|
|
36
|
+
console.log(`\n ✓ site built: ${res.pages.length} page${res.pages.length === 1 ? '' : 's'} + index -> ${res.outDir}/`);
|
|
37
|
+
console.log(` open ${res.indexPath}`);
|
|
38
|
+
|
|
39
|
+
if (hasFlag(argv, '--serve')) {
|
|
40
|
+
const port = Number(flag(argv, '--port')) || 4321;
|
|
41
|
+
const { url } = await serveSite(res.outDir, port);
|
|
42
|
+
console.log(`\n serving at ${url} (ctrl-c to stop)\n`);
|
|
43
|
+
await new Promise(() => {}); // keep alive until killed
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { run };
|
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 };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* atris spaceship — bounded, self-reporting overnight runner.
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper over scripts/spaceship.sh. The script is the implementation
|
|
5
|
+
* (a supervised loop that survives bad ticks and emails Keshav on every
|
|
6
|
+
* meaningful state change); this module just makes it reachable as
|
|
7
|
+
* `atris spaceship ...` and integrates with the CLI's command dispatch.
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* atris spaceship --hours 4
|
|
11
|
+
* atris spaceship --hours 4 --repo /path/to/repo --interval 780
|
|
12
|
+
* atris spaceship --hours 0.01 --tick-cmd /tmp/stub.sh --no-email # test
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const { spawn } = require('child_process');
|
|
18
|
+
|
|
19
|
+
const SCRIPT = path.join(__dirname, '..', 'scripts', 'spaceship.sh');
|
|
20
|
+
|
|
21
|
+
function spaceship(args = []) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
if (!fs.existsSync(SCRIPT)) {
|
|
24
|
+
reject(new Error(`spaceship.sh not found at ${SCRIPT}`));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const child = spawn('bash', [SCRIPT, ...args], {
|
|
28
|
+
stdio: 'inherit',
|
|
29
|
+
env: process.env,
|
|
30
|
+
});
|
|
31
|
+
child.on('error', reject);
|
|
32
|
+
child.on('close', (code) => {
|
|
33
|
+
if (code === 0) resolve({ success: true });
|
|
34
|
+
else reject(new Error(`spaceship exited with code ${code}`));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { spaceship, SCRIPT };
|
package/commands/sync.js
CHANGED
|
@@ -463,7 +463,6 @@ function syncAtris() {
|
|
|
463
463
|
|
|
464
464
|
const filesToSync = [
|
|
465
465
|
{ source: 'atris.md', target: 'atris.md' },
|
|
466
|
-
{ source: 'atris/atrisDev.md', target: 'atrisDev.md' },
|
|
467
466
|
{ source: 'PERSONA.md', target: 'PERSONA.md' },
|
|
468
467
|
{ source: 'GETTING_STARTED.md', target: 'GETTING_STARTED.md' },
|
|
469
468
|
{ source: 'atris/CLAUDE.md', target: 'CLAUDE.md' },
|
|
@@ -808,7 +807,6 @@ function _findAtrisProjects(rootDir, maxDepth = 8) {
|
|
|
808
807
|
// Canonical files shipped from the package root. Must match syncAtris's filesToSync.
|
|
809
808
|
const SYNC_ALL_FILES = [
|
|
810
809
|
{ source: 'atris.md', target: 'atris.md' },
|
|
811
|
-
{ source: 'atris/atrisDev.md', target: 'atrisDev.md' },
|
|
812
810
|
{ source: 'PERSONA.md', target: 'PERSONA.md' },
|
|
813
811
|
{ source: 'GETTING_STARTED.md', target: 'GETTING_STARTED.md' },
|
|
814
812
|
{ source: 'atris/CLAUDE.md', target: 'CLAUDE.md' },
|