atris 3.30.1 → 3.30.3

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 (67) hide show
  1. package/AGENTS.md +6 -0
  2. package/atris/AGENTS.md +11 -0
  3. package/atris/CLAUDE.md +5 -0
  4. package/atris/atris.md +19 -4
  5. package/atris/policies/atris-design.md +71 -0
  6. package/atris/policies/design-seed.md +187 -0
  7. package/atris/skills/atris/SKILL.md +26 -3
  8. package/atris/skills/design/SKILL.md +3 -2
  9. package/atris/skills/loop/SKILL.md +5 -3
  10. package/atris/team/_template/MEMBER.md +19 -0
  11. package/atris.md +63 -23
  12. package/ax +1434 -1698
  13. package/bin/atris.js +5 -1
  14. package/commands/agent-spawn.js +2 -2
  15. package/commands/brain.js +92 -7
  16. package/commands/brainstorm.js +62 -22
  17. package/commands/business-sync.js +92 -8
  18. package/commands/business.js +13 -7
  19. package/commands/chat-scan.js +102 -0
  20. package/commands/computer.js +758 -15
  21. package/commands/deck.js +505 -105
  22. package/commands/init.js +6 -1
  23. package/commands/launchpad.js +638 -0
  24. package/commands/log-sync.js +44 -13
  25. package/commands/loop-front.js +290 -0
  26. package/commands/member.js +124 -3
  27. package/commands/mission.js +717 -32
  28. package/commands/pull.js +37 -28
  29. package/commands/pulse.js +11 -8
  30. package/commands/run.js +79 -39
  31. package/commands/sync.js +36 -17
  32. package/commands/task.js +342 -66
  33. package/commands/xp.js +3 -0
  34. package/commands/youtube.js +221 -7
  35. package/decks/README.md +89 -0
  36. package/decks/archetype-catalog.json +180 -0
  37. package/decks/atris-antislop-pitch.json +48 -0
  38. package/decks/atris-archetypes-v2.json +83 -0
  39. package/decks/atris-one-loop-pitch.json +80 -0
  40. package/decks/atris-seed-pitch-v3.json +118 -0
  41. package/decks/atris-seed-pitch-v4-skeleton.json +106 -0
  42. package/decks/atris-seed-pitch-v5.json +109 -0
  43. package/decks/atris-seed-pitch-v6.json +137 -0
  44. package/decks/atris-seed-pitch-v7.json +133 -0
  45. package/decks/atris-single-shot-proof.json +74 -0
  46. package/decks/mark-pincus-narrative.json +102 -0
  47. package/decks/mark-pincus-sourcery.json +94 -0
  48. package/decks/yash-applied-compute-detailed.json +150 -0
  49. package/decks/yash-applied-compute-generalist.json +82 -0
  50. package/decks/yash-applied-compute-narrative.json +54 -0
  51. package/lib/ax-prefs.js +5 -12
  52. package/lib/chat-log-scan.js +377 -0
  53. package/lib/context-gatherer.js +35 -1
  54. package/lib/deck-compose.js +145 -0
  55. package/lib/deck-history.js +64 -0
  56. package/lib/deck-layout.js +169 -0
  57. package/lib/deck-review.js +431 -0
  58. package/lib/deck-schema.js +154 -0
  59. package/lib/file-ops.js +2 -2
  60. package/lib/functional-owner.js +189 -0
  61. package/lib/slides-deck.js +512 -58
  62. package/lib/task-db.js +109 -2
  63. package/package.json +2 -1
  64. package/templates/business-starter/team/START_HERE.md +12 -8
  65. package/utils/auth.js +4 -0
  66. package/utils/config.js +4 -0
  67. package/atris/atrisDev.md +0 -717
package/commands/xp.js CHANGED
@@ -2048,6 +2048,7 @@ async function syncAgentXp(args = []) {
2048
2048
  if (dryRun) return preview;
2049
2049
 
2050
2050
  const token = readFlag(args, '--token', process.env.ATRIS_AGENTXP_SYNC_TOKEN || process.env.AGENTXP_SYNC_TOKEN || '');
2051
+ const envToken = process.env.ATRIS_TOKEN && process.env.ATRIS_TOKEN.trim() ? process.env.ATRIS_TOKEN.trim() : '';
2051
2052
  const options = {
2052
2053
  method: 'POST',
2053
2054
  body: preview.packet,
@@ -2055,6 +2056,8 @@ async function syncAgentXp(args = []) {
2055
2056
  };
2056
2057
  if (token) {
2057
2058
  options.headers = { 'X-AgentXP-Sync-Token': token };
2059
+ } else if (envToken) {
2060
+ options.token = envToken;
2058
2061
  } else {
2059
2062
  const ensured = await ensureValidCredentials(apiRequestJson);
2060
2063
  if (ensured.error) {
@@ -1,15 +1,24 @@
1
1
  const { apiRequestJson } = require('../utils/api');
2
2
  const { ensureValidCredentials } = require('../utils/auth');
3
+ const { spawnSync } = require('child_process');
4
+ const https = require('https');
3
5
 
4
6
  const DEFAULT_QUERY = 'Extract main topics, key insights, and actionable takeaways.';
5
7
  const DEFAULT_TIMEOUT_MS = 300000;
8
+ const LOCAL_TRANSCRIPT_MAX_BYTES = 5 * 1024 * 1024;
9
+ const LOCAL_TRANSCRIPT_MAX_CHARS = 250000;
10
+ const ALLOWED_CAPTION_HOST_SUFFIXES = [
11
+ 'youtube.com',
12
+ 'googlevideo.com',
13
+ 'youtubei.googleapis.com',
14
+ ];
6
15
 
7
16
  function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
8
17
  output('');
9
18
  output(`Usage: ${commandName} process <youtube-url> [options]`);
10
19
  output(` ${commandName} <youtube-url> [options]`);
11
20
  output('');
12
- output('Process a YouTube video through Atris using Gemini native video analysis.');
21
+ output('Process a YouTube video through Atris using transcript-first analysis.');
13
22
  output('');
14
23
  output('Options:');
15
24
  output(' --query, -q <text> Focus question for the analysis');
@@ -108,9 +117,190 @@ function buildYoutubePayload(options) {
108
117
  };
109
118
  if (options.agentId) payload.agent_id = options.agentId;
110
119
  if (options.storeAsKnowledge) payload.store_as_knowledge = true;
120
+ if (options.localTranscript?.transcriptText) {
121
+ payload.transcript_text = options.localTranscript.transcriptText;
122
+ if (options.localTranscript.language) payload.transcript_language = options.localTranscript.language;
123
+ if (options.localTranscript.durationSeconds) payload.duration_seconds = options.localTranscript.durationSeconds;
124
+ }
125
+ if (options.cacheTranscript !== undefined) {
126
+ payload.cache_transcript = Boolean(options.cacheTranscript);
127
+ }
111
128
  return payload;
112
129
  }
113
130
 
131
+ function resultErrorText(result) {
132
+ const raw = result?.error || result?.text || 'unknown error';
133
+ if (typeof raw === 'string') return raw;
134
+ try {
135
+ return JSON.stringify(raw);
136
+ } catch {
137
+ return String(raw);
138
+ }
139
+ }
140
+
141
+ function shouldRetryWithLocalTranscript(result) {
142
+ if (!result || result.ok) return false;
143
+ if (result.status === 502) return true;
144
+ if (result.status !== 400) return false;
145
+ return /YouTube video is not publicly accessible|oEmbed|metadata lookup failed/i.test(resultErrorText(result));
146
+ }
147
+
148
+ function youtubeFailureError(result) {
149
+ const hint = result.status === 401
150
+ ? ' Run "atris login --force".'
151
+ : result.status === 402
152
+ ? ' Check Atris credits.'
153
+ : '';
154
+ return new Error(`YouTube processing failed (${result.status}): ${resultErrorText(result)}.${hint}`);
155
+ }
156
+
157
+ function captionHostAllowed(urlString) {
158
+ try {
159
+ const parsed = new URL(urlString);
160
+ if (parsed.protocol !== 'https:') return false;
161
+ const hostname = parsed.hostname.toLowerCase().replace(/\.$/, '');
162
+ return ALLOWED_CAPTION_HOST_SUFFIXES.some((suffix) => (
163
+ hostname === suffix || hostname.endsWith(`.${suffix}`)
164
+ ));
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
169
+
170
+ function chooseCaptionTrack(info = {}) {
171
+ const preferred = ['en', 'en-orig', 'en-US', 'en-GB'];
172
+ const chooseFrom = (trackSets = {}) => {
173
+ for (const language of preferred) {
174
+ for (const track of trackSets[language] || []) {
175
+ if (track?.url && ['json3', 'vtt', 'srv3', 'ttml'].includes(track.ext)) return { language, track };
176
+ }
177
+ }
178
+ for (const [language, tracks] of Object.entries(trackSets)) {
179
+ for (const track of tracks || []) {
180
+ if (track?.url) return { language, track };
181
+ }
182
+ }
183
+ return null;
184
+ };
185
+
186
+ return chooseFrom(info.subtitles) || chooseFrom(info.automatic_captions);
187
+ }
188
+
189
+ function fetchCaptionText(urlString, redirects = 0) {
190
+ if (!captionHostAllowed(urlString)) {
191
+ return Promise.resolve(null);
192
+ }
193
+
194
+ return new Promise((resolve) => {
195
+ const req = https.get(urlString, { timeout: 30000 }, (res) => {
196
+ if ([301, 302, 307, 308].includes(res.statusCode) && res.headers.location && redirects < 3) {
197
+ res.resume();
198
+ const nextUrl = new URL(res.headers.location, urlString).toString();
199
+ resolve(fetchCaptionText(nextUrl, redirects + 1));
200
+ return;
201
+ }
202
+
203
+ if (res.statusCode !== 200 || !captionHostAllowed(res.responseUrl || urlString)) {
204
+ res.resume();
205
+ resolve(null);
206
+ return;
207
+ }
208
+
209
+ const contentLength = Number(res.headers['content-length'] || 0);
210
+ if (contentLength > LOCAL_TRANSCRIPT_MAX_BYTES) {
211
+ res.resume();
212
+ resolve(null);
213
+ return;
214
+ }
215
+
216
+ const chunks = [];
217
+ let total = 0;
218
+ res.on('data', (chunk) => {
219
+ total += chunk.length;
220
+ if (total > LOCAL_TRANSCRIPT_MAX_BYTES) {
221
+ req.destroy();
222
+ resolve(null);
223
+ return;
224
+ }
225
+ chunks.push(chunk);
226
+ });
227
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
228
+ });
229
+ req.on('timeout', () => req.destroy());
230
+ req.on('error', () => resolve(null));
231
+ });
232
+ }
233
+
234
+ function parseCaptionText(raw) {
235
+ const trimmed = String(raw || '').trimStart();
236
+ if (!trimmed) return '';
237
+
238
+ if (trimmed.startsWith('{')) {
239
+ try {
240
+ const payload = JSON.parse(trimmed);
241
+ const segments = [];
242
+ for (const event of payload.events || []) {
243
+ const text = (event.segs || [])
244
+ .map((piece) => piece.utf8 || '')
245
+ .join('')
246
+ .replace(/\s+/g, ' ')
247
+ .trim();
248
+ if (!text) continue;
249
+ if (segments[segments.length - 1] === text) continue;
250
+ segments.push(text);
251
+ }
252
+ return segments.join(' ');
253
+ } catch {
254
+ return '';
255
+ }
256
+ }
257
+
258
+ const segments = [];
259
+ for (const line of String(raw).split(/\r?\n/)) {
260
+ const stripped = line.trim();
261
+ if (!stripped) continue;
262
+ if (/^(WEBVTT|Kind:|Language:|NOTE|STYLE|REGION)/.test(stripped)) continue;
263
+ if (stripped.includes('-->')) continue;
264
+ if (/^\d+$/.test(stripped)) continue;
265
+ if (stripped.includes('<c>') || stripped.includes('</c>')) continue;
266
+ const text = stripped.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
267
+ if (!text) continue;
268
+ if (segments[segments.length - 1] === text) continue;
269
+ segments.push(text);
270
+ }
271
+ return segments.join(' ');
272
+ }
273
+
274
+ async function extractLocalTranscript(youtubeUrl, deps = {}) {
275
+ if (process.env.ATRIS_YOUTUBE_LOCAL_TRANSCRIPT === '0') return null;
276
+ const runner = deps.spawnSync || spawnSync;
277
+ const result = runner('yt-dlp', ['-J', '--skip-download', '--no-warnings', youtubeUrl], {
278
+ encoding: 'utf8',
279
+ timeout: 20000,
280
+ maxBuffer: 10 * 1024 * 1024,
281
+ });
282
+ if (result.error || result.status !== 0 || !result.stdout) return null;
283
+
284
+ let info;
285
+ try {
286
+ info = JSON.parse(result.stdout);
287
+ } catch {
288
+ return null;
289
+ }
290
+
291
+ const selected = chooseCaptionTrack(info);
292
+ if (!selected?.track?.url) return null;
293
+ const rawCaption = await (deps.fetchCaptionText || fetchCaptionText)(selected.track.url);
294
+ const transcript = parseCaptionText(rawCaption);
295
+ if (!transcript) return null;
296
+
297
+ return {
298
+ transcriptText: transcript.slice(0, LOCAL_TRANSCRIPT_MAX_CHARS),
299
+ language: selected.language || 'unknown',
300
+ durationSeconds: Number(info.duration || 0) || undefined,
301
+ };
302
+ }
303
+
114
304
  async function processYoutube(options, deps = {}) {
115
305
  const apiFn = deps.apiRequestJson || apiRequestJson;
116
306
  const ensureFn = deps.ensureValidCredentials || ensureValidCredentials;
@@ -121,20 +311,42 @@ async function processYoutube(options, deps = {}) {
121
311
  throw new Error(detail ? `Authentication failed: ${detail}. Run "atris login".` : 'Not logged in. Run "atris login".');
122
312
  }
123
313
 
314
+ const localExtractor = deps.extractLocalTranscript || extractLocalTranscript;
315
+ let localTranscript = null;
316
+ try {
317
+ localTranscript = await localExtractor(options.youtubeUrl, deps);
318
+ } catch {
319
+ localTranscript = null;
320
+ }
321
+
322
+ if (localTranscript?.transcriptText) {
323
+ const transcriptResult = await apiFn('/agent/process_youtube', {
324
+ method: 'POST',
325
+ token: creds.token,
326
+ timeoutMs: options.timeoutMs,
327
+ retries: 0,
328
+ body: buildYoutubePayload({ ...options, localTranscript, cacheTranscript: false }),
329
+ });
330
+
331
+ if (transcriptResult.ok) {
332
+ return transcriptResult.data;
333
+ }
334
+
335
+ if (transcriptResult.status === 401 || transcriptResult.status === 402 || transcriptResult.status === 400) {
336
+ throw youtubeFailureError(transcriptResult);
337
+ }
338
+ }
339
+
124
340
  const result = await apiFn('/agent/process_youtube', {
125
341
  method: 'POST',
126
342
  token: creds.token,
127
343
  timeoutMs: options.timeoutMs,
344
+ retries: 0,
128
345
  body: buildYoutubePayload(options),
129
346
  });
130
347
 
131
348
  if (!result.ok) {
132
- const hint = result.status === 401
133
- ? ' Run "atris login --force".'
134
- : result.status === 402
135
- ? ' Check Atris credits.'
136
- : '';
137
- throw new Error(`YouTube processing failed (${result.status}): ${result.error || result.text || 'unknown error'}.${hint}`);
349
+ throw youtubeFailureError(result);
138
350
  }
139
351
 
140
352
  return result.data;
@@ -177,7 +389,9 @@ module.exports = {
177
389
  showYoutubeHelp,
178
390
  parseYoutubeArgs,
179
391
  buildYoutubePayload,
392
+ extractLocalTranscript,
180
393
  processYoutube,
394
+ shouldRetryWithLocalTranscript,
181
395
  formatYoutubeResult,
182
396
  youtubeCommand,
183
397
  };
@@ -0,0 +1,89 @@
1
+ # Deck specs
2
+
3
+ Build anti-slop Google Slides decks from JSON specs.
4
+
5
+ ## Loop
6
+
7
+ ```text
8
+ notes.md / video -> atris deck compose (analysis -> spec, picks layouts)
9
+ spec.json -> atris deck lint (cheap pre-check: shape + taste)
10
+ -> atris deck build --review (publish + thumbnail fetch)
11
+ -> agent reads review.json + PNG thumbnails (or --review-auto)
12
+ -> fix spec/engine if needed
13
+ -> atris deck review confirm <id> (mark ready + emit receipt)
14
+ ```
15
+
16
+ One-shot: `atris deck video <youtube-url> --review` runs compose -> lint -> build -> review.
17
+
18
+ ## Commands
19
+
20
+ ```bash
21
+ # Compose a spec from a markdown outline (offline) or a video (cloud)
22
+ atris deck compose --md notes.md --out decks/my.json --style narrative
23
+ atris deck compose --url https://youtu.be/VIDEO --out decks/my.json --theme ink
24
+
25
+ # Pre-build lint (schema shape + taste/clip)
26
+ atris deck lint decks/atris-seed-pitch-v3.json
27
+
28
+ # Build + open visual review packet (--review-auto also flags blank thumbnails)
29
+ atris deck build decks/atris-seed-pitch-v3.json --title "Atris Seed Pitch v3" --review
30
+
31
+ # Rebuild an existing deck in place (URL/id stay stable)
32
+ atris deck build decks/atris-seed-pitch-v3.json --update 139zTe9cPOGttzYbW04adRU26VJz0SqWRwaZmkoVibP8
33
+
34
+ # Batch-build several specs
35
+ atris deck build decks/atris-seed-pitch-v3.json decks/mark-pincus-narrative.json
36
+
37
+ # One command: video -> reviewed deck
38
+ atris deck video https://youtu.be/VIDEO --theme noir --review
39
+
40
+ # Re-fetch thumbnails for an existing deck
41
+ atris deck review 139zTe9cPOGttzYbW04adRU26VJz0SqWRwaZmkoVibP8 --spec decks/atris-seed-pitch-v3.json
42
+
43
+ # After thumbnails pass review (emits a receipt for the action queue)
44
+ atris deck review 139zTe9cPOGttzYbW04adRU26VJz0SqWRwaZmkoVibP8 --confirm "slides 5+8 spacing fixed"
45
+
46
+ # Builds recorded for a spec
47
+ atris deck history decks/atris-seed-pitch-v3.json
48
+ ```
49
+
50
+ ## Themes
51
+
52
+ | Theme | Vibe | When |
53
+ |-------|------|------|
54
+ | `paper` | warm light cream | default editorial |
55
+ | `terminal` | warm dark brown | wedge / antislop |
56
+ | `ink` | stark white + red accent | press / magazine |
57
+ | `noir` | cool dark + blue accent | podcast / interview |
58
+
59
+ Pick in spec: `"theme": "noir"`, or override at build: `atris deck build spec.json --theme ink`
60
+
61
+ List all: `atris deck themes`
62
+
63
+ ## Quality bar
64
+
65
+ - `build` and `lint` **block on errors**: bad shape (unknown type, missing field), "it is not X, it is Y" copy, or `content-truncated` (more list items than the engine renders)
66
+ - Agent reads every thumbnail before `--confirm` (or `--review-auto` flags blank/failed renders)
67
+ - Stack slides: clean cards, no side accent bars
68
+ - State claims directly; avoid contrast framing ("not taste, it's proof")
69
+ - Prefer narrative types (`interstitial`, `lede`, `prose`, `split`, `bullets`) when content is a story, not a dashboard
70
+ - Lint warns on **template fatigue** (4+ boxed slides: panel, receipt, versus, metricgrid, stack, chips)
71
+ - Dense statement/columns/prose/lede/quote/split text auto-shrinks to stay on-slide
72
+ - `review.json` is the manifest for agents; `receipt.json` is emitted on confirm
73
+ - `slide-01.png` ... `slide-N.png` are the thumbnails to inspect
74
+
75
+ ## Specs
76
+
77
+ | File | Purpose |
78
+ |------|---------|
79
+ | `atris-seed-pitch-v3.json` | Main investor pitch (10 slides, paper theme) |
80
+ | `atris-one-loop-pitch.json` | Outcome wedge deck (7 slides, terminal theme) |
81
+ | `yash-applied-compute-generalist.json` | Yash Patil / Own or Be Owned (ink, boxed template) |
82
+ | `yash-applied-compute-narrative.json` | Same story, typography-first (ink, no widgets) |
83
+ | `yash-applied-compute-detailed.json` | Full episode notes, 18 slides, bullets + cases |
84
+ | `mark-pincus-sourcery.json` | Mark Pincus / Life at the Speed of Play (noir, boxed) |
85
+ | `mark-pincus-narrative.json` | Same story, box-free narrative (12 slides, noir) |
86
+ | `atris-single-shot-proof.json` | Review loop proof deck (6 slides, single-pass quality bar) |
87
+ | `atris-archetypes-v2.json` | New archetypes showcase (8 slides, paper theme) |
88
+ | `atris-antislop-pitch.json` | 3-slide antislop product pitch |
89
+ | `archetype-catalog.json` | Every slide type once (19 slides, paper theme), design reference for writers |
@@ -0,0 +1,180 @@
1
+ {
2
+ "theme": "paper",
3
+ "brand": { "name": "Atris", "accent": "." },
4
+ "slides": [
5
+ {
6
+ "type": "title",
7
+ "headline": "The deck **archetype** catalog",
8
+ "sub": "Every slide type Atris renders, shown once, so writers can pick the right one by eye.",
9
+ "panel": {
10
+ "header": { "title": "Reference deck", "meta": "19 types" },
11
+ "rows": [
12
+ { "title": "Narrative", "sub": "story slides", "value": "7" },
13
+ { "title": "Structured", "sub": "data slides", "value": "8" },
14
+ { "title": "Framing", "sub": "open and close", "value": "4" }
15
+ ],
16
+ "footer": { "left": "one of each", "right": "paper" }
17
+ }
18
+ },
19
+ {
20
+ "type": "statement",
21
+ "text": "Pick the **shape** before you write the words.",
22
+ "sub": "A statement is one sentence that earns the whole slide. Keep the sub short and it carries the claim."
23
+ },
24
+ {
25
+ "type": "columns",
26
+ "heading": "When to reach for columns",
27
+ "columns": [
28
+ { "h": "Compare", "b": "Set two or three peers side by side so the reader scans the difference fast." },
29
+ { "h": "Group", "b": "Bucket related ideas under short headers instead of one long paragraph." },
30
+ { "h": "Balance", "b": "Give each idea equal weight when no single one should win the slide." }
31
+ ]
32
+ },
33
+ {
34
+ "type": "panel",
35
+ "heading": "Panel reads like a status board",
36
+ "sub": "Use it for rows of named items with a value and a state dot. Header and footer are optional.",
37
+ "panel": {
38
+ "header": { "title": "Loop health", "meta": "live" },
39
+ "rows": [
40
+ { "title": "Plan", "sub": "navigator", "value": "ok", "sev": 0 },
41
+ { "title": "Build", "sub": "executor", "value": "ok", "active": true },
42
+ { "title": "Review", "sub": "validator", "value": "queue", "sev": 1 },
43
+ { "title": "Ship", "sub": "launcher", "value": "hold", "sev": 2 }
44
+ ],
45
+ "footer": { "left": "four rows max", "right": "verified" }
46
+ }
47
+ },
48
+ {
49
+ "type": "chips",
50
+ "heading": "Chips for a quick taxonomy",
51
+ "sub": "Drop a row of short labels when the point is breadth, not depth.",
52
+ "chips": ["title", "statement", "columns", "panel", "timeline", "versus", "receipt", "stack", "quote", "hero"],
53
+ "mono": "one accent, sentence case, no shouting"
54
+ },
55
+ {
56
+ "type": "bignumber",
57
+ "number": "19",
58
+ "label": "slide types in one design system",
59
+ "sub": "Bignumber exists for the single figure that should land before anything else on the slide."
60
+ },
61
+ {
62
+ "type": "timeline",
63
+ "heading": "Timeline walks a process",
64
+ "steps": [
65
+ { "label": "spec", "sub": "write json" },
66
+ { "label": "lint", "sub": "cheap check" },
67
+ { "label": "build", "sub": "publish", "active": true },
68
+ { "label": "review", "sub": "thumbnails" },
69
+ { "label": "confirm", "sub": "mark ready" }
70
+ ],
71
+ "sub": "Five steps fit cleanly. More than that and the labels start to crowd the edges."
72
+ },
73
+ {
74
+ "type": "versus",
75
+ "heading": "Versus splits two stances",
76
+ "left": {
77
+ "label": "Default slop",
78
+ "items": ["arial on white", "purple gradients", "em dash everywhere", "five accent colors"]
79
+ },
80
+ "right": {
81
+ "label": "Atris deck",
82
+ "items": ["committed type", "one warm accent", "sanitized copy", "real data panels"]
83
+ }
84
+ },
85
+ {
86
+ "type": "metricgrid",
87
+ "heading": "Metricgrid for four headline figures",
88
+ "metrics": [
89
+ { "value": "4", "label": "cards is the cap before the grid gets cramped" },
90
+ { "value": "2x2", "label": "fixed layout keeps the eye moving in a square" },
91
+ { "value": "32pt", "label": "value type so the number reads from the back row" },
92
+ { "value": "1", "label": "accent hue across every card on the slide" }
93
+ ]
94
+ },
95
+ {
96
+ "type": "receipt",
97
+ "heading": "Receipt makes a claim auditable",
98
+ "sub": "Use it when the slide needs evidence fields, not prose. Up to six rows plus a stamp.",
99
+ "receiptTitle": "catalog build #001",
100
+ "fields": [
101
+ { "k": "spec", "v": "archetype-catalog.json" },
102
+ { "k": "theme", "v": "paper, warm editorial" },
103
+ { "k": "types", "v": "all 19, one each" },
104
+ { "k": "lint", "v": "zero errors, zero truncation" },
105
+ { "k": "use", "v": "design reference for writers" }
106
+ ],
107
+ "stamp": "lint clean"
108
+ },
109
+ {
110
+ "type": "stack",
111
+ "heading": "Stack shows layers in order",
112
+ "layers": [
113
+ { "title": "Read the catalog", "sub": "find the type that fits the content" },
114
+ { "title": "Copy the fields", "sub": "match the names the engine expects" },
115
+ { "title": "Lint before build", "sub": "catch clips and caps locally" }
116
+ ],
117
+ "sub": "Three layers leave room for a closing line beneath the cards."
118
+ },
119
+ {
120
+ "type": "quote",
121
+ "text": "Write the slide the reader **already wishes** they had.",
122
+ "author": "Atris deck engine",
123
+ "role": "design note"
124
+ },
125
+ {
126
+ "type": "hero",
127
+ "headline": "Hero centers one **big idea**",
128
+ "sub": "Reach for it when the slide is a moment, not a table. Everything centers under one rule.",
129
+ "mono": "atris deck build decks/archetype-catalog.json --review",
130
+ "footer": "atris.ai · archetype catalog · 2026"
131
+ },
132
+ {
133
+ "type": "interstitial",
134
+ "kicker": "section break",
135
+ "text": "Now the **narrative** types",
136
+ "sub": "Interstitial is a chapter card. Big type, lots of air, a kicker to mark the turn."
137
+ },
138
+ {
139
+ "type": "lede",
140
+ "headline": "Lede opens like a feature story",
141
+ "dek": "A magazine intro slide: one strong headline, a deck of supporting text, and a byline that grounds it.",
142
+ "byline": "atris-cli · deck-compose · paper theme"
143
+ },
144
+ {
145
+ "type": "prose",
146
+ "heading": "Prose is for paragraphs",
147
+ "paragraphs": [
148
+ "When the content is genuinely a few sentences of argument, do not force it into cards. Prose gives the reader room to follow a line of thought without a widget in the way.",
149
+ "The engine fits one font size across every paragraph and floats them down to the slide floor, so two or three short paragraphs stay readable and balanced."
150
+ ]
151
+ },
152
+ {
153
+ "type": "split",
154
+ "heading": "Split pairs text with one figure",
155
+ "body": "Put the explanation on the left and let a single number or short phrase carry the right side. It reads as claim plus proof without a full panel.",
156
+ "accent": "60%",
157
+ "accentSub": "of the slide is text; the accent is the punchline the reader remembers."
158
+ },
159
+ {
160
+ "type": "bullets",
161
+ "heading": "Bullets, done with restraint",
162
+ "items": [
163
+ { "text": "Keep it to seven or fewer", "sub": "the engine shrinks type as the list grows" },
164
+ { "text": "Lead each item with a verb or a noun", "sub": "not a full sentence" },
165
+ { "text": "Add a sub only when it earns its line", "sub": "optional second tier per item" },
166
+ { "text": "Prefer a real list over a wall of cards", "sub": "typography, not boxes" }
167
+ ],
168
+ "sub": "A true bullet list: accent dot, text, no card around each line."
169
+ },
170
+ {
171
+ "type": "close",
172
+ "tagline": "One design system, nineteen ways to say it well.",
173
+ "buttons": [
174
+ { "label": "Read the engine", "primary": true },
175
+ { "label": "Lint your spec" }
176
+ ],
177
+ "footer": "atris.ai · archetype catalog · 2026"
178
+ }
179
+ ]
180
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "theme": "terminal",
3
+ "brand": { "name": "Atris", "accent": "." },
4
+ "slides": [
5
+ {
6
+ "type": "title",
7
+ "headline": "Your agents ship **slop.** Atris catches it first.",
8
+ "sub": "Deterministic gates on UI, copy, and outbound sends. Facts at file:line, not opinions. You stay the center; nothing irreversible ships without proof.",
9
+ "panel": {
10
+ "header": { "title": "Antislop scan", "meta": "live" },
11
+ "rows": [
12
+ { "title": "ai-purple-gradient", "sub": "src/Hero.tsx:12", "value": "blocked", "valueSub": "error", "sev": 0, "active": true },
13
+ { "title": "hype-copy", "sub": "outbound/body.txt:3", "value": "blocked", "valueSub": "error", "sev": 0 },
14
+ { "title": "raw-html-in-plain-body", "sub": "email draft", "value": "blocked", "valueSub": "error", "sev": 1 },
15
+ { "title": "deck engine", "sub": "slides batch", "value": "clean", "valueSub": "pass", "sev": 2 }
16
+ ],
17
+ "footer": { "left": "3 blocked before send", "right": "exit 1" }
18
+ }
19
+ },
20
+ {
21
+ "type": "columns",
22
+ "heading": "Three gates. One operating system.",
23
+ "columns": [
24
+ {
25
+ "h": "Slop detect",
26
+ "b": "atris slop scans code and prose for AI tells: purple gradients, glass blur, em dashes, hype copy. Zero LLM. Built for CI and review."
27
+ },
28
+ {
29
+ "h": "Outbound gate",
30
+ "b": "Before your name goes out: no raw HTML, no fenced source as body, render proof for visual sends, copy kill-list enforced."
31
+ },
32
+ {
33
+ "h": "Your judgment",
34
+ "b": "Atris2 Fast prepares on the periphery. Sends, posts, and money defer to you with a judgment packet and receipt trail."
35
+ }
36
+ ]
37
+ },
38
+ {
39
+ "type": "close",
40
+ "tagline": "Prepare fast. Block slop. You approve consequence.",
41
+ "buttons": [
42
+ { "label": "Try Atris2 Fast", "primary": true },
43
+ { "label": "Run slop detect" }
44
+ ],
45
+ "footer": "atris.ai · antislop by design · 2026"
46
+ }
47
+ ]
48
+ }