atris 3.30.12 → 3.32.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.
Files changed (64) hide show
  1. package/AGENTS.md +16 -3
  2. package/FOR_AGENTS.md +81 -0
  3. package/README.md +6 -4
  4. package/atris/CLAUDE.md +8 -0
  5. package/atris/atris.md +51 -19
  6. package/atris/skills/README.md +1 -0
  7. package/atris/skills/blocks/SKILL.md +134 -0
  8. package/atris/skills/clawhub/philosophy-of-work/SKILL.md +56 -0
  9. package/atris/skills/youtube/SKILL.md +31 -11
  10. package/atris.md +15 -0
  11. package/ax +189 -7
  12. package/bin/atris.js +273 -225
  13. package/commands/autoland.js +379 -0
  14. package/commands/autopilot-front.js +273 -0
  15. package/commands/autopilot.js +94 -4
  16. package/commands/business.js +1 -1
  17. package/commands/clean.js +72 -9
  18. package/commands/codex-goal.js +72 -22
  19. package/commands/computer.js +48 -3
  20. package/commands/gm.js +1 -1
  21. package/commands/harvest.js +179 -0
  22. package/commands/init.js +22 -1
  23. package/commands/land.js +442 -0
  24. package/commands/loop-front.js +122 -4
  25. package/commands/member.js +551 -19
  26. package/commands/mission.js +3674 -278
  27. package/commands/play.js +1 -1
  28. package/commands/pulse.js +65 -7
  29. package/commands/run-front.js +144 -0
  30. package/commands/run.js +10 -7
  31. package/commands/sign.js +90 -0
  32. package/commands/strings.js +301 -0
  33. package/commands/task.js +782 -108
  34. package/commands/truth.js +171 -0
  35. package/commands/xp.js +32 -8
  36. package/commands/youtube.js +72 -5
  37. package/decks/README.md +6 -12
  38. package/lib/auto-accept-certified.js +10 -0
  39. package/lib/autoland.js +391 -0
  40. package/lib/context-gatherer.js +0 -8
  41. package/lib/mission-artifact.js +504 -0
  42. package/lib/mission-room.js +846 -0
  43. package/lib/next-moves.js +212 -6
  44. package/lib/operator-next.js +7 -0
  45. package/lib/pulse.js +78 -4
  46. package/lib/runner-command.js +51 -20
  47. package/lib/runs-prune.js +242 -0
  48. package/lib/task-db.js +19 -2
  49. package/lib/task-proof.js +1 -1
  50. package/package.json +4 -4
  51. package/decks/atris-seed-pitch-v3.json +0 -118
  52. package/decks/atris-seed-pitch-v4-skeleton.json +0 -106
  53. package/decks/atris-seed-pitch-v5.json +0 -109
  54. package/decks/atris-seed-pitch-v6.json +0 -137
  55. package/decks/atris-seed-pitch-v7.json +0 -133
  56. package/decks/mark-pincus-narrative.json +0 -102
  57. package/decks/mark-pincus-sourcery.json +0 -94
  58. package/decks/yash-applied-compute-detailed.json +0 -150
  59. package/decks/yash-applied-compute-generalist.json +0 -82
  60. package/decks/yash-applied-compute-narrative.json +0 -54
  61. package/lib/ax-chat-input.js +0 -164
  62. package/lib/ax-goal.js +0 -307
  63. package/lib/ax-prefs.js +0 -63
  64. package/lib/ax-shimmer.js +0 -63
@@ -0,0 +1,301 @@
1
+ // atris strings — a content design system built from live codebase content (no LLM).
2
+ //
3
+ // The missing pillar next to `atris slop`: slop catches HOW copy is written (tells,
4
+ // hype, em-dashes); strings governs WHAT words you ship. It scans the repo for
5
+ // user-facing strings, builds a terminology registry in .atris/strings.json, flags
6
+ // the same string written three different ways (the "unnecessary variant" tell), and
7
+ // enforces preferred terms at the commit/PR gate so you rename "live" -> "active"
8
+ // once and it holds everywhere.
9
+ //
10
+ // Zero external deps (Node built-ins only) — repo contract. Deterministic: a finding
11
+ // is a fact (file:line + term), not a taste opinion, so it drops into CI + the gate.
12
+ //
13
+ // Usage:
14
+ // atris strings scan [path] # extract UI strings -> .atris/strings.json
15
+ // atris strings variants # the same string written N different ways
16
+ // atris strings term --ban live --prefer active --why "..." # codify a rule
17
+ // atris strings check --staged # gate: banned terms in changed lines (exit 1)
18
+ // atris strings list # dump the registry
19
+ //
20
+ // Exit code: 0 = clean, 1 = violation/variants found, 2 = bad usage.
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const { gitChangedLines } = require('./slop'); // reuse the diff parser — DRY
25
+
26
+ const CODE_EXTS = new Set(['.tsx', '.jsx', '.ts', '.js', '.mjs', '.vue', '.svelte', '.astro', '.html']);
27
+ const TEXT_EXTS = new Set([...CODE_EXTS, '.md', '.mdx', '.txt']);
28
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.astro', 'coverage', '.cache', 'out', 'vendor']);
29
+
30
+ const REGISTRY_FILE = path.join('.atris', 'strings.json');
31
+ const MAX_LOCATIONS = 12; // cap stored locations per string so the registry stays readable
32
+
33
+ function walk(target, out, exts) {
34
+ let stat;
35
+ try { stat = fs.statSync(target); } catch { return out; }
36
+ if (stat.isFile()) {
37
+ if (exts.has(path.extname(target))) out.push(target);
38
+ return out;
39
+ }
40
+ if (stat.isDirectory()) {
41
+ if (SKIP_DIRS.has(path.basename(target))) return out;
42
+ for (const name of fs.readdirSync(target)) {
43
+ if (name.startsWith('.') && name !== '.') continue;
44
+ walk(path.join(target, name), out, exts);
45
+ }
46
+ }
47
+ return out;
48
+ }
49
+
50
+ // Is this captured literal a user-facing string, or just code/classes/identifiers?
51
+ // High precision on purpose: a noisy registry gets ignored. minWords lets JSX text
52
+ // nodes (>Settings<) keep single-word labels while quoted literals require a phrase.
53
+ function isUserFacing(s, minWords) {
54
+ if (s.length < 2 || s.length > 200) return false;
55
+ if (!/[A-Za-z]/.test(s)) return false;
56
+ if (/[<>{}=]|\$\{|=>|&&|\|\||::|\/\/|\/\*|\*\//.test(s)) return false; // code
57
+ if (/^https?:/i.test(s) || /\bwww\.[a-z]/i.test(s)) return false; // urls
58
+ if (/^[./~#@\\]/.test(s)) return false; // path/anchor/hex/handle
59
+ if (/^#[0-9a-fA-F]{3,8}$/.test(s)) return false; // hex color
60
+ if (/^[A-Z0-9_]{2,}$/.test(s)) return false; // CONSTANT_CASE
61
+ const words = s.split(/\s+/);
62
+ // className / token-list reject: multi-token where most tokens look like classes
63
+ if (words.length > 1
64
+ && words.every((w) => /^[\w:[\]\-./%@]+$/.test(w))
65
+ && words.filter((w) => w.includes('-')).length >= Math.ceil(words.length / 2)) return false;
66
+ if (words.length === 1) {
67
+ if (minWords > 1) return false; // quoted single tokens are usually identifiers
68
+ return /^[A-Z][a-zA-Z]{2,}$/.test(s); // a real Label word, e.g. "Settings"
69
+ }
70
+ const prose = (s.match(/[A-Za-z ]/g) || []).length;
71
+ return prose / s.length >= 0.55; // reads like a sentence, not a payload
72
+ }
73
+
74
+ // Extract candidate user-facing strings from one file's text.
75
+ function extractStrings(text) {
76
+ const out = [];
77
+ const lines = text.split('\n');
78
+ for (let i = 0; i < lines.length; i++) {
79
+ const line = lines[i];
80
+ const push = (raw, minWords) => { const s = raw.trim(); if (isUserFacing(s, minWords)) out.push({ text: s, line: i + 1 }); };
81
+ let m;
82
+ const jsx = />([^<>{}\n]{2,})</g; while ((m = jsx.exec(line))) push(m[1], 1); // JSX/HTML text node
83
+ const dq = /"([^"\\\n]{2,}?)"/g; while ((m = dq.exec(line))) push(m[1], 2); // "double"
84
+ const sq = /'([^'\\\n]{2,}?)'/g; while ((m = sq.exec(line))) push(m[1], 2); // 'single'
85
+ const tq = /`([^`$\\\n]{2,}?)`/g; while ((m = tq.exec(line))) push(m[1], 2); // `template` (no ${})
86
+ }
87
+ return out;
88
+ }
89
+
90
+ // Canonical form for variant detection: same meaning, different surface (case / punct / spacing).
91
+ function normalize(s) {
92
+ return s.toLowerCase()
93
+ .replace(/…/g, '').replace(/\.\.\.$/, '')
94
+ .replace(/\s+/g, ' ')
95
+ .replace(/[.!?,;:]+$/, '')
96
+ .trim();
97
+ }
98
+
99
+ function loadRegistry(root = process.cwd()) {
100
+ try { return JSON.parse(fs.readFileSync(path.join(root, REGISTRY_FILE), 'utf8')); }
101
+ catch { return { version: 1, scannedAt: null, root: null, strings: [], terms: [] }; }
102
+ }
103
+
104
+ function saveRegistry(reg, root = process.cwd()) {
105
+ const file = path.join(root, REGISTRY_FILE);
106
+ fs.mkdirSync(path.dirname(file), { recursive: true });
107
+ fs.writeFileSync(file, JSON.stringify(reg, null, 2) + '\n');
108
+ return file;
109
+ }
110
+
111
+ // Group strings by normalized form; a cluster with >1 distinct surface form is an inconsistency.
112
+ function variantClusters(strings) {
113
+ const byNorm = new Map();
114
+ for (const s of strings) {
115
+ if (!byNorm.has(s.norm)) byNorm.set(s.norm, []);
116
+ byNorm.get(s.norm).push(s);
117
+ }
118
+ const clusters = [];
119
+ for (const [norm, group] of byNorm) {
120
+ const surfaces = [...new Set(group.map((g) => g.text))];
121
+ if (surfaces.length > 1) {
122
+ clusters.push({ norm, surfaces, count: group.reduce((n, g) => n + g.count, 0) });
123
+ }
124
+ }
125
+ return clusters.sort((a, b) => b.count - a.count);
126
+ }
127
+
128
+ function scan(argv) {
129
+ const json = argv.includes('--json');
130
+ const target = argv.find((a) => !a.startsWith('-')) || '.';
131
+ const files = walk(path.resolve(target), [], CODE_EXTS);
132
+
133
+ const byText = new Map(); // text -> { text, norm, count, locations[] }
134
+ for (const file of files) {
135
+ let text; try { text = fs.readFileSync(file, 'utf8'); } catch { continue; }
136
+ const rel = path.relative(process.cwd(), file);
137
+ for (const hit of extractStrings(text)) {
138
+ let entry = byText.get(hit.text);
139
+ if (!entry) { entry = { text: hit.text, norm: normalize(hit.text), count: 0, locations: [] }; byText.set(hit.text, entry); }
140
+ entry.count++;
141
+ if (entry.locations.length < MAX_LOCATIONS) entry.locations.push(`${rel}:${hit.line}`);
142
+ }
143
+ }
144
+
145
+ const reg = loadRegistry();
146
+ reg.scannedAt = new Date().toISOString();
147
+ reg.root = path.relative(process.cwd(), path.resolve(target)) || '.';
148
+ reg.strings = [...byText.values()].sort((a, b) => b.count - a.count);
149
+ const file = saveRegistry(reg);
150
+ const clusters = variantClusters(reg.strings);
151
+
152
+ if (json) {
153
+ console.log(JSON.stringify({
154
+ ok: true, scanned: files.length, strings: reg.strings.length,
155
+ occurrences: reg.strings.reduce((n, s) => n + s.count, 0),
156
+ variantClusters: clusters.length, registry: path.relative(process.cwd(), file),
157
+ }, null, 2));
158
+ return 0;
159
+ }
160
+ console.log(`\n scanned ${files.length} file${files.length === 1 ? '' : 's'} -> ${reg.strings.length} unique string${reg.strings.length === 1 ? '' : 's'}`);
161
+ console.log(` registry: ${path.relative(process.cwd(), file)}`);
162
+ if (clusters.length) console.log(`\n ⚠ ${clusters.length} variant cluster${clusters.length === 1 ? '' : 's'} (same string, different casing/punctuation) — run: atris strings variants`);
163
+ else console.log(`\n ✓ no inconsistent variants`);
164
+ console.log('');
165
+ return 0;
166
+ }
167
+
168
+ function variants(argv) {
169
+ const json = argv.includes('--json');
170
+ const reg = loadRegistry();
171
+ if (!reg.strings.length) { console.error(' no registry yet — run: atris strings scan'); return 2; }
172
+ const clusters = variantClusters(reg.strings);
173
+ if (json) { console.log(JSON.stringify({ ok: clusters.length === 0, clusters }, null, 2)); return clusters.length ? 1 : 0; }
174
+ if (!clusters.length) { console.log(`\n ✓ clean — every string is written one way\n`); return 0; }
175
+ console.log(`\n ${clusters.length} variant cluster${clusters.length === 1 ? '' : 's'} — same string, inconsistent surface form:\n`);
176
+ for (const c of clusters) {
177
+ console.log(` ⚠ ${c.surfaces.map((s) => JSON.stringify(s)).join(' vs ')} (${c.count}×)`);
178
+ }
179
+ console.log(`\n pick one per cluster, then: atris strings term --ban "<wrong>" --prefer "<right>"\n`);
180
+ return 1;
181
+ }
182
+
183
+ function term(argv) {
184
+ const get = (flag) => { const i = argv.indexOf(flag); return i >= 0 ? argv[i + 1] : null; };
185
+ const ban = get('--ban');
186
+ const prefer = get('--prefer');
187
+ const why = get('--why') || '';
188
+ if (argv.includes('--list') || (!ban && !argv.includes('--remove'))) {
189
+ const reg = loadRegistry();
190
+ if (!reg.terms.length) { console.log('\n no terms yet — add one: atris strings term --ban "live" --prefer "active"\n'); return 0; }
191
+ console.log('\n preferred terms (enforced by: atris strings check):\n');
192
+ for (const t of reg.terms) console.log(` ✗ "${t.ban}" → "${t.prefer}"${t.why ? ` (${t.why})` : ''}`);
193
+ console.log('');
194
+ return 0;
195
+ }
196
+ const reg = loadRegistry();
197
+ if (argv.includes('--remove')) {
198
+ const before = reg.terms.length;
199
+ reg.terms = reg.terms.filter((t) => t.ban.toLowerCase() !== String(get('--remove') || ban || '').toLowerCase());
200
+ saveRegistry(reg);
201
+ console.log(` ${before === reg.terms.length ? 'no match' : 'removed'}: "${get('--remove') || ban}"`);
202
+ return 0;
203
+ }
204
+ if (!ban || !prefer) { console.error(' usage: atris strings term --ban <word> --prefer <word> [--why "..."]'); return 2; }
205
+ reg.terms = reg.terms.filter((t) => t.ban.toLowerCase() !== ban.toLowerCase());
206
+ reg.terms.push({ ban, prefer, why });
207
+ const file = saveRegistry(reg);
208
+ console.log(` ✓ "${ban}" → "${prefer}" added to ${path.relative(process.cwd(), file)}`);
209
+ console.log(` enforce it: atris strings check --staged`);
210
+ return 0;
211
+ }
212
+
213
+ function check(argv) {
214
+ const json = argv.includes('--json');
215
+ const quiet = argv.includes('--quiet');
216
+ const staged = argv.includes('--staged');
217
+ const diffMode = staged || argv.includes('--diff');
218
+ const reg = loadRegistry();
219
+ if (!reg.terms.length) {
220
+ if (json) { console.log(JSON.stringify({ ok: true, terms: 0, findings: [] }, null, 2)); }
221
+ else if (!quiet) console.log('\n no terms to enforce — add one: atris strings term --ban "live" --prefer "active"\n');
222
+ return 0;
223
+ }
224
+
225
+ let files, changed = null;
226
+ if (diffMode) {
227
+ changed = gitChangedLines(staged);
228
+ files = [...changed.keys()].filter((f) => TEXT_EXTS.has(path.extname(f)) && fs.existsSync(f));
229
+ } else {
230
+ const target = argv.find((a) => !a.startsWith('-')) || '.';
231
+ files = walk(path.resolve(target), [], TEXT_EXTS);
232
+ }
233
+ const regAbs = path.resolve(process.cwd(), REGISTRY_FILE);
234
+
235
+ const matchers = reg.terms.map((t) => ({ ...t, re: new RegExp(`\\b${t.ban.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i') }));
236
+ const findings = [];
237
+ for (const file of files) {
238
+ if (path.resolve(file) === regAbs) continue; // never flag the registry itself
239
+ let text; try { text = fs.readFileSync(file, 'utf8'); } catch { continue; }
240
+ const lines = text.split('\n');
241
+ for (let i = 0; i < lines.length; i++) {
242
+ if (diffMode && changed && !(changed.get(path.resolve(file)) && changed.get(path.resolve(file)).has(i + 1))) continue;
243
+ for (const t of matchers) {
244
+ if (t.re.test(lines[i])) findings.push({ file: path.relative(process.cwd(), file), line: i + 1, ban: t.ban, prefer: t.prefer, why: t.why });
245
+ }
246
+ }
247
+ }
248
+
249
+ if (json) { console.log(JSON.stringify({ ok: findings.length === 0, scanned: files.length, terms: reg.terms.length, findings }, null, 2)); return findings.length ? 1 : 0; }
250
+ if (!findings.length) { if (!quiet) console.log(`\n ✓ clean — no banned terms in ${files.length} file${files.length === 1 ? '' : 's'}\n`); else console.log(' ✓ clean · exit 0'); return 0; }
251
+ if (!quiet) {
252
+ console.log('');
253
+ const w = Math.max(...findings.map((f) => `${f.file}:${f.line}`.length));
254
+ for (const f of findings) console.log(` ✗ ${`${f.file}:${f.line}`.padEnd(w)} "${f.ban}" → "${f.prefer}"${f.why ? ` ${f.why}` : ''}`);
255
+ }
256
+ console.log(`\n ${findings.length} banned-term use${findings.length === 1 ? '' : 's'} · exit 1\n`);
257
+ return 1;
258
+ }
259
+
260
+ function list(argv) {
261
+ const json = argv.includes('--json');
262
+ const reg = loadRegistry();
263
+ if (json) { console.log(JSON.stringify(reg, null, 2)); return 0; }
264
+ if (!reg.strings.length) { console.error(' no registry yet — run: atris strings scan'); return 2; }
265
+ const top = Number((argv[argv.indexOf('--top') + 1]) || 30);
266
+ console.log(`\n ${reg.strings.length} strings (scanned ${reg.scannedAt || '?'}), top ${Math.min(top, reg.strings.length)} by use:\n`);
267
+ for (const s of reg.strings.slice(0, top)) console.log(` ${String(s.count).padStart(3)}× ${JSON.stringify(s.text)}`);
268
+ console.log('');
269
+ return 0;
270
+ }
271
+
272
+ function stringsCommand(argv) {
273
+ const sub = argv[0];
274
+ const rest = argv.slice(1);
275
+ if (sub === 'scan') return scan(rest);
276
+ if (sub === 'variants' || sub === 'dupes') return variants(rest);
277
+ if (sub === 'term' || sub === 'terms') return term(rest);
278
+ if (sub === 'check' || sub === 'gate') return check(rest);
279
+ if (sub === 'list' || sub === 'ls') return list(rest);
280
+ console.log(`
281
+ atris strings — a content design system from your live codebase (no LLM)
282
+
283
+ atris strings scan [path] extract user-facing strings -> .atris/strings.json
284
+ atris strings variants the same string written N different ways (pick one)
285
+ atris strings term --ban <a> --prefer <b> [--why "..."] codify a preferred term
286
+ atris strings term --list show the preferred terms
287
+ atris strings check [--staged] gate: flag banned terms in changed lines (exit 1)
288
+ atris strings list [--top N] the registry, most-used first
289
+ add --json to scan/variants/check/list for machine output
290
+
291
+ Pairs with 'atris slop' (how copy reads) — strings governs what words you ship.
292
+ The registry lives in .atris/strings.json. Wire 'check --staged' into the pre-commit gate.
293
+ `);
294
+ return 0;
295
+ }
296
+
297
+ module.exports = {
298
+ stringsCommand, scan, variants, term, check, list,
299
+ isUserFacing, extractStrings, normalize, variantClusters,
300
+ loadRegistry, saveRegistry, walk, CODE_EXTS, TEXT_EXTS,
301
+ };