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.
Files changed (65) hide show
  1. package/README.md +32 -7
  2. package/atris/skills/atris/SKILL.md +15 -2
  3. package/atris/skills/atris-feedback/SKILL.md +7 -0
  4. package/atris/skills/design/SKILL.md +29 -2
  5. package/atris/skills/engines/SKILL.md +44 -0
  6. package/atris/skills/flow/SKILL.md +1 -1
  7. package/atris/skills/wake/SKILL.md +37 -0
  8. package/atris/skills/youtube/SKILL.md +13 -39
  9. package/atris/team/validator/MEMBER.md +1 -0
  10. package/atris/wiki/concepts/agent-activation-contract.md +3 -3
  11. package/atris/wiki/concepts/workspace-initialization-contract.md +3 -3
  12. package/atris/wiki/index.md +1 -0
  13. package/atris.md +43 -19
  14. package/bin/atris.js +413 -31
  15. package/commands/agent-spawn.js +480 -0
  16. package/commands/analytics.js +6 -3
  17. package/commands/apps.js +11 -0
  18. package/commands/autopilot.js +42 -18
  19. package/commands/brain.js +74 -7
  20. package/commands/brainstorm.js +9 -58
  21. package/commands/clean.js +1 -4
  22. package/commands/compile.js +9 -4
  23. package/commands/console.js +8 -3
  24. package/commands/deck.js +184 -0
  25. package/commands/init.js +22 -11
  26. package/commands/lesson.js +76 -0
  27. package/commands/member.js +252 -48
  28. package/commands/mission.js +405 -13
  29. package/commands/now.js +4 -2
  30. package/commands/probe.js +105 -27
  31. package/commands/pulse.js +504 -0
  32. package/commands/radar.js +1 -0
  33. package/commands/recap.js +71 -25
  34. package/commands/run.js +615 -22
  35. package/commands/site.js +48 -0
  36. package/commands/slop.js +307 -0
  37. package/commands/spaceship.js +39 -0
  38. package/commands/sync.js +0 -2
  39. package/commands/task.js +429 -37
  40. package/commands/theme.js +217 -0
  41. package/commands/verify.js +7 -3
  42. package/lib/activity-stream.js +166 -0
  43. package/lib/auto-accept-certified.js +23 -1
  44. package/lib/context-gatherer.js +170 -0
  45. package/lib/deck-from-md.js +110 -0
  46. package/lib/escape-regexp.js +13 -0
  47. package/lib/file-ops.js +6 -3
  48. package/lib/html-render.js +257 -0
  49. package/lib/journal.js +1 -1
  50. package/lib/lesson-contradiction.js +113 -0
  51. package/lib/memory-view.js +95 -0
  52. package/lib/policy-lessons.js +3 -2
  53. package/lib/pulse.js +401 -0
  54. package/lib/runner-command.js +156 -0
  55. package/lib/site.js +114 -0
  56. package/lib/slides-deck.js +237 -0
  57. package/lib/state-detection.js +1 -4
  58. package/lib/task-db.js +101 -4
  59. package/lib/task-proof.js +1 -1
  60. package/lib/theme.js +264 -0
  61. package/lib/todo-fallback.js +2 -1
  62. package/lib/todo-sections.js +33 -0
  63. package/package.json +1 -2
  64. package/utils/api.js +14 -2
  65. package/atris/atrisDev.md +0 -717
package/lib/site.js ADDED
@@ -0,0 +1,114 @@
1
+ // Static site generator — point it at a folder of markdown (docs, your wiki,
2
+ // memory) and get a beautiful, navigable HTML site in the design system.
3
+ // Builds on lib/deck-from-md (parse) + lib/html-render (render).
4
+ //
5
+ // buildSite(input, opts) -> { outDir, indexPath, pages }. Pure file I/O.
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const http = require('http');
10
+ const { parseMarkdownToSpec } = require('./deck-from-md');
11
+ const { renderHtml, THEMES: BUILTIN } = require('./html-render');
12
+ const { mergedThemes } = require('./theme');
13
+
14
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'out', 'coverage', '.cache']);
15
+ const MD_EXTS = new Set(['.md', '.mdx']);
16
+
17
+ function collectMd(input) {
18
+ const stat = fs.statSync(input);
19
+ if (stat.isFile()) return MD_EXTS.has(path.extname(input)) ? [input] : [];
20
+ const out = [];
21
+ (function walk(dir) {
22
+ for (const name of fs.readdirSync(dir).sort()) {
23
+ if (name.startsWith('.')) continue;
24
+ const full = path.join(dir, name);
25
+ let st; try { st = fs.statSync(full); } catch { continue; }
26
+ if (st.isDirectory()) { if (!SKIP_DIRS.has(name)) walk(full); }
27
+ else if (MD_EXTS.has(path.extname(name))) out.push(full);
28
+ }
29
+ })(input);
30
+ return out;
31
+ }
32
+
33
+ function slugFor(file, root) {
34
+ // base = the dir the site was built from (root may be a dir or a single .md file)
35
+ const base = MD_EXTS.has(path.extname(root)) ? path.dirname(root) : root;
36
+ const rel = path.relative(base, file).replace(/\.[^.]+$/, '');
37
+ return rel.replace(/[\\/]+/g, '-').replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'page';
38
+ }
39
+
40
+ function firstHeading(md) {
41
+ const m = md.match(/^#{1,2}\s+(.+)$/m);
42
+ return m ? m[1].replace(/\*\*/g, '').trim() : null;
43
+ }
44
+ function firstParagraph(md) {
45
+ const body = md.replace(/^---\n[\s\S]*?\n---\n/, '');
46
+ for (const line of body.split('\n')) {
47
+ const t = line.trim();
48
+ if (!t || /^[#<>]/.test(t) || /^[-*+]/.test(t) || /^\|/.test(t) || /^```/.test(t)) continue;
49
+ return t.replace(/\*\*/g, '');
50
+ }
51
+ return '';
52
+ }
53
+ function clip(s, n) { s = String(s || ''); return s.length > n ? s.slice(0, n - 1).trimEnd() + '…' : s; }
54
+ function titleFromSlug(slug) { return slug.replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); }
55
+
56
+ function buildSite(input, opts = {}) {
57
+ const THEMES = mergedThemes(BUILTIN, opts.root || process.cwd());
58
+ const theme = THEMES[opts.theme] ? opts.theme : 'atris';
59
+ const siteTitle = opts.title || 'Atris';
60
+ const accent = opts.accent || '.';
61
+ const outDir = opts.out || 'dist';
62
+ const files = collectMd(input);
63
+ fs.mkdirSync(outDir, { recursive: true });
64
+ const nav = { home: 'index.html', label: siteTitle, accent };
65
+
66
+ const pages = [];
67
+ const seen = new Set();
68
+ for (const file of files) {
69
+ const md = fs.readFileSync(file, 'utf8');
70
+ const spec = parseMarkdownToSpec(md, { theme });
71
+ if (!THEMES[spec.theme]) spec.theme = theme;
72
+ let slug = slugFor(file, input);
73
+ while (seen.has(slug)) slug += '-1';
74
+ seen.add(slug);
75
+ const title = firstHeading(md) || titleFromSlug(slug);
76
+ const summary = firstParagraph(md);
77
+ const outFile = path.join(outDir, slug + '.html');
78
+ fs.writeFileSync(outFile, renderHtml(spec, { title, nav, themes: THEMES }));
79
+ pages.push({ src: file, out: outFile, href: slug + '.html', title, summary });
80
+ }
81
+
82
+ const indexSpec = {
83
+ theme, brand: { name: siteTitle, accent },
84
+ slides: [
85
+ { type: 'title', headline: opts.headline || `${siteTitle} **docs**`, sub: opts.sub || `${pages.length} page${pages.length === 1 ? '' : 's'}, one design system.` },
86
+ { type: 'toc', heading: 'Pages', items: pages.map((p) => ({ href: p.href, title: p.title, summary: clip(p.summary, 120) })) },
87
+ { type: 'close', tagline: opts.tagline || 'One workspace, one design system.', footer: siteTitle },
88
+ ],
89
+ };
90
+ const indexPath = path.join(outDir, 'index.html');
91
+ fs.writeFileSync(indexPath, renderHtml(indexSpec, { title: siteTitle, themes: THEMES }));
92
+ return { outDir, indexPath, pages };
93
+ }
94
+
95
+ const MIME = { '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': 'text/javascript', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml' };
96
+
97
+ // minimal static preview server for a built site dir (no deps)
98
+ function serveSite(dir, port = 4321) {
99
+ const root = path.resolve(dir);
100
+ const server = http.createServer((req, res) => {
101
+ let rel = decodeURIComponent((req.url || '/').split('?')[0]);
102
+ if (rel.endsWith('/')) rel += 'index.html';
103
+ const file = path.normalize(path.join(root, rel));
104
+ if (!file.startsWith(root)) { res.writeHead(403); res.end('forbidden'); return; }
105
+ fs.readFile(file, (err, data) => {
106
+ if (err) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('not found'); return; }
107
+ res.writeHead(200, { 'Content-Type': MIME[path.extname(file)] || 'application/octet-stream' });
108
+ res.end(data);
109
+ });
110
+ });
111
+ return new Promise((resolve) => server.listen(port, () => resolve({ server, url: `http://localhost:${port}` })));
112
+ }
113
+
114
+ module.exports = { buildSite, serveSite, collectMd, slugFor, firstHeading, firstParagraph };
@@ -0,0 +1,237 @@
1
+ // Atris deck engine — turn a plain content spec into a premium, anti-slop
2
+ // Google Slides deck. Pure: spec -> batch-update requests (no network here).
3
+ //
4
+ // The product idea: PMs open Slides and get Arial-on-white slop by default.
5
+ // This engine gives them a described deck rendered in a committed design system
6
+ // (own backgrounds, distinctive fonts, one accent, real data panels). It is
7
+ // built so it CANNOT emit the usual AI tells: em dashes are sanitized, labels
8
+ // stay sentence case, no gradient text, no glassmorphism, one accent hue.
9
+ //
10
+ // Spec shape (see commands/deck.js for the CLI):
11
+ // { theme: 'terminal'|'paper',
12
+ // brand: { name: 'Sentinel', accent: '.' },
13
+ // slides: [ { type, ...fields } ] }
14
+ // Emphasis: wrap a phrase in **double asterisks** to render it in the accent.
15
+
16
+ // ---------- themes (OKLCH design system, flattened to sRGB hex) ----------
17
+ const THEMES = {
18
+ terminal: { // warm dark "premium terminal"
19
+ fonts: { display: 'Fraunces', body: 'Outfit', mono: 'Roboto Mono' },
20
+ color: { bg: '#1E1A16', panel: '#2A231C', panelAlt: '#2F271F', line: '#3A332B',
21
+ ink: '#ECE6DD', soft: '#BCB2A4', faint: '#968C7E',
22
+ accent: '#D98E5C', accent2: '#E3A06B', onAccent: '#1E1A16',
23
+ sev: ['#D98E5C', '#DBBE84', '#7F97A4'] },
24
+ },
25
+ paper: { // light "editorial paper instrument"
26
+ fonts: { display: 'Fraunces', body: 'Outfit', mono: 'Roboto Mono' },
27
+ color: { bg: '#FBF8F2', panel: '#FFFFFF', panelAlt: '#F4EEE4', line: '#E5DDCF',
28
+ ink: '#2B241B', soft: '#6B5F4F', faint: '#877B69',
29
+ accent: '#B5572E', accent2: '#9A4723', onAccent: '#FFFFFF',
30
+ sev: ['#B5572E', '#C0883A', '#5F7787'] },
31
+ },
32
+ };
33
+ const COLOR_ROLES = ['bg', 'panel', 'line', 'ink', 'soft', 'faint', 'accent', 'accent2', 'onAccent'];
34
+
35
+ const W = 720, H = 405, M = 48; // slide is 720 x 405 PT
36
+
37
+ // ---------- low-level builder ----------
38
+ function rgb(hex) { const n = parseInt(String(hex).slice(1), 16);
39
+ return { red: ((n >> 16) & 255) / 255, green: ((n >> 8) & 255) / 255, blue: (n & 255) / 255 }; }
40
+
41
+ // strip the AI tells the engine refuses to ship. Returns sanitized text.
42
+ function sanitize(t) {
43
+ return String(t == null ? '' : t)
44
+ .replace(/\s*[—]\s*/g, ', ') // em dash -> comma (top AI-writing tell)
45
+ .replace(/\s-\s/g, ', ') // spaced hyphen used as a dash
46
+ .replace(/\bAI-powered\b/gi, 'built for')
47
+ .replace(/\s{2,}/g, ' ');
48
+ }
49
+
50
+ // parse **emphasis** -> { plain, ranges:[{start,end}] } (indices into plain)
51
+ function parseEmph(text) {
52
+ const ranges = []; let plain = ''; let i = 0;
53
+ while (i < text.length) {
54
+ if (text[i] === '*' && text[i + 1] === '*') {
55
+ const close = text.indexOf('**', i + 2);
56
+ if (close !== -1) { const inner = text.slice(i + 2, close);
57
+ ranges.push({ start: plain.length, end: plain.length + inner.length });
58
+ plain += inner; i = close + 2; continue; }
59
+ }
60
+ plain += text[i]; i++;
61
+ }
62
+ return { plain, ranges };
63
+ }
64
+
65
+ function makeCtx(theme) {
66
+ const C = theme.color, F = theme.fonts, requests = [];
67
+ let uid = 0; const nid = (p) => `${p}_${String(++uid).padStart(4, '0')}`;
68
+
69
+ const createSlide = (id) => requests.push({ createSlide: { objectId: id, slideLayoutReference: { predefinedLayout: 'BLANK' } } });
70
+ const bg = (slide, hex) => requests.push({ updatePageProperties: { objectId: slide,
71
+ pageProperties: { pageBackgroundFill: { solidFill: { color: { rgbColor: rgb(hex) } } } },
72
+ fields: 'pageBackgroundFill.solidFill.color' } });
73
+ const shape = (type, slide, x, y, w, h) => { const id = nid(type.slice(0, 2).toLowerCase());
74
+ requests.push({ createShape: { objectId: id, shapeType: type, elementProperties: {
75
+ pageObjectId: slide, size: { width: { magnitude: w, unit: 'PT' }, height: { magnitude: h, unit: 'PT' } },
76
+ transform: { scaleX: 1, scaleY: 1, translateX: x, translateY: y, unit: 'PT' } } } }); return id; };
77
+ const fill = (id, hex, outlineHex, weight) => {
78
+ const props = { shapeBackgroundFill: { solidFill: { color: { rgbColor: rgb(hex) } } } };
79
+ let fields = 'shapeBackgroundFill.solidFill.color';
80
+ if (outlineHex) { props.outline = { outlineFill: { solidFill: { color: { rgbColor: rgb(outlineHex) } } }, weight: { magnitude: weight || 1, unit: 'PT' } }; fields += ',outline.outlineFill.solidFill.color,outline.weight'; }
81
+ else { props.outline = { propertyState: 'NOT_RENDERED' }; fields += ',outline.propertyState'; }
82
+ requests.push({ updateShapeProperties: { objectId: id, shapeProperties: props, fields } }); return id; };
83
+ function styleRange(id, s, e, o) { if (e <= s || !o) return;
84
+ const style = {}, f = [];
85
+ if (o.family) { style.fontFamily = o.family; f.push('fontFamily'); }
86
+ if (o.size) { style.fontSize = { magnitude: o.size, unit: 'PT' }; f.push('fontSize'); }
87
+ if (o.color) { style.foregroundColor = { opaqueColor: { rgbColor: rgb(o.color) } }; f.push('foregroundColor'); }
88
+ if (o.bold) { style.bold = true; f.push('bold'); }
89
+ if (o.italic) { style.italic = true; f.push('italic'); }
90
+ if (!f.length) return;
91
+ requests.push({ updateTextStyle: { objectId: id, textRange: { type: 'FIXED_RANGE', startIndex: s, endIndex: e }, style, fields: f.join(',') } }); }
92
+ function box(slide, x, y, w, h, markup, opts = {}) {
93
+ const { plain, ranges } = parseEmph(sanitize(markup));
94
+ if (!plain.length) return null;
95
+ const id = shape('TEXT_BOX', slide, x, y, w, h);
96
+ requests.push({ insertText: { objectId: id, text: plain } });
97
+ styleRange(id, 0, plain.length, opts);
98
+ const accentColor = opts.accent || C.accent2;
99
+ ranges.forEach((r) => styleRange(id, r.start, r.end, { family: opts.family, size: opts.size, color: accentColor, italic: opts.emphItalic }));
100
+ if (opts.align || opts.line != null) requests.push({ updateParagraphStyle: { objectId: id, textRange: { type: 'ALL' },
101
+ style: { ...(opts.align ? { alignment: opts.align } : {}), ...(opts.line != null ? { lineSpacing: opts.line } : {}) },
102
+ fields: [opts.align && 'alignment', opts.line != null && 'lineSpacing'].filter(Boolean).join(',') } });
103
+ if (opts.vmid) requests.push({ updateShapeProperties: { objectId: id, shapeProperties: { contentAlignment: 'MIDDLE' }, fields: 'contentAlignment' } });
104
+ return id; }
105
+ const rule = (slide, x, y, w, hex) => fill(shape('RECTANGLE', slide, x, y, w, 2), hex || C.accent);
106
+
107
+ function wordmark(slide, x, y, size, brand, center) {
108
+ const name = (brand && brand.name) || 'Atris';
109
+ const ac = (brand && brand.accent) || '';
110
+ const id = box(slide, x, y, center ? W - x * 2 : size * 9, size * 1.6, name + ac,
111
+ { family: F.display, size, color: C.ink, align: center ? 'CENTER' : 'START' });
112
+ if (ac) styleRange(id, name.length, name.length + ac.length, { family: F.display, size, color: C.accent });
113
+ return id; }
114
+
115
+ // generalized data panel: header + rows + footer
116
+ function panel(slide, x, y, w, data) {
117
+ const rows = (data.rows || []).slice(0, 4);
118
+ const rowH = 38, headH = data.header ? 30 : 0, footH = data.footer ? 26 : 0;
119
+ const h = headH + rowH * rows.length + footH;
120
+ fill(shape('ROUND_RECTANGLE', slide, x, y, w, h), C.panel, C.line, 1);
121
+ if (data.header) {
122
+ box(slide, x + 14, y + 9, w * 0.6, 16, data.header.title || '', { family: F.body, size: 10.5, color: C.ink });
123
+ if (data.header.meta) box(slide, x + w - 120, y + 9, 106, 14, data.header.meta, { family: F.body, size: 8.5, color: C.faint, align: 'END' });
124
+ fill(shape('RECTANGLE', slide, x, y + headH, w, 0.75), C.line);
125
+ }
126
+ rows.forEach((r, i) => {
127
+ const ry = y + headH + i * rowH;
128
+ if (r.active) fill(shape('RECTANGLE', slide, x, ry, 2, rowH), C.accent);
129
+ const sev = C.sev[(r.sev != null ? r.sev : 0) % C.sev.length];
130
+ fill(shape('ELLIPSE', slide, x + 16, ry + rowH / 2 - 3.5, 7, 7), sev);
131
+ const nameTxt = sanitize(r.title || '') + (r.sub ? '\n' + sanitize(r.sub) : '');
132
+ const nb = box(slide, x + 30, ry + 6, w - 110, 28, nameTxt, { family: F.body, size: 11, color: C.ink, line: 108 });
133
+ if (r.sub && nb) styleRange(nb, sanitize(r.title || '').length + 1, nameTxt.length, { family: F.body, size: 9, color: C.faint });
134
+ if (r.value != null) {
135
+ const valTxt = String(r.value) + (r.valueSub ? '\n' + r.valueSub : '');
136
+ const bb = box(slide, x + w - 84, ry + 6, 70, 28, valTxt, { family: F.body, size: 11, color: C.ink, align: 'END' });
137
+ if (bb) { styleRange(bb, 0, String(r.value).length, { family: F.body, size: 13, color: C.ink, bold: true });
138
+ if (r.valueSub) styleRange(bb, String(r.value).length + 1, valTxt.length, { family: F.body, size: 8, color: C.faint }); }
139
+ }
140
+ if (i < rows.length - 1) fill(shape('RECTANGLE', slide, x, ry + rowH, w, 0.75), C.panelAlt);
141
+ });
142
+ if (data.footer) { const fy = y + headH + rowH * rows.length;
143
+ box(slide, x + 14, fy + 6, w * 0.62, 14, data.footer.left || '', { family: F.body, size: 9, color: C.faint });
144
+ if (data.footer.right) box(slide, x + w - 84, fy + 6, 70, 14, data.footer.right, { family: F.body, size: 9, color: C.accent2, align: 'END' }); }
145
+ return h; }
146
+
147
+ function chips(slide, x, y, list) { let cx = x;
148
+ (list || []).forEach((label) => { const t = sanitize(label); const w = 16 + t.length * 6.3;
149
+ fill(shape('ROUND_RECTANGLE', slide, cx, y, w, 24), C.panel, C.line, 1);
150
+ box(slide, cx, y, w, 24, t, { family: F.mono, size: 9.5, color: C.faint, align: 'CENTER', vmid: true });
151
+ cx += w + 10; }); }
152
+
153
+ function buttons(slide, y, list) {
154
+ const items = (list || []).slice(0, 3); const bw = 150, bh = 34, gap = 12;
155
+ const total = items.length * bw + (items.length - 1) * gap; let bx = (W - total) / 2;
156
+ items.forEach((b) => { const primary = b.primary;
157
+ fill(shape('ROUND_RECTANGLE', slide, bx, y, bw, bh), primary ? C.ink : C.bg, primary ? null : C.line, 1);
158
+ box(slide, bx, y, bw, bh, b.label || 'Button', { family: F.body, size: 12.5, color: primary ? C.bg : C.ink, align: 'CENTER', vmid: true });
159
+ bx += bw + gap; }); }
160
+
161
+ return { requests, C, F, nid, createSlide, bg, shape, fill, box, styleRange, rule, wordmark, panel, chips, buttons };
162
+ }
163
+
164
+ // ---------- slide archetypes ----------
165
+ const ARCHE = {
166
+ title(ctx, slide, s, spec) {
167
+ ctx.wordmark(slide, M, 34, 15, spec.brand);
168
+ ctx.rule(slide, M, 96, 40);
169
+ const hasPanel = !!s.panel;
170
+ ctx.box(slide, M, 110, hasPanel ? 360 : 600, 180, s.headline || s.title || '',
171
+ { family: ctx.F.display, size: 40, color: ctx.C.ink, line: 100, emphItalic: true });
172
+ if (s.sub) ctx.box(slide, M, hasPanel ? 300 : 300, hasPanel ? 350 : 540, 70, s.sub, { family: ctx.F.body, size: 12.5, color: ctx.C.soft, line: 120 });
173
+ if (hasPanel) ctx.panel(slide, 432, 96, 240, s.panel);
174
+ },
175
+ statement(ctx, slide, s, spec) {
176
+ ctx.wordmark(slide, M, 34, 13, spec.brand);
177
+ ctx.rule(slide, M, 150, 40);
178
+ ctx.box(slide, M, 164, 600, 120, s.text || s.headline || '', { family: ctx.F.display, size: 46, color: ctx.C.ink, line: 100, emphItalic: true });
179
+ if (s.sub) ctx.box(slide, M, 286, 480, 70, s.sub, { family: ctx.F.body, size: 14, color: ctx.C.soft, line: 130 });
180
+ },
181
+ columns(ctx, slide, s, spec) {
182
+ ctx.wordmark(slide, M, 34, 13, spec.brand);
183
+ if (s.heading) ctx.box(slide, M, 110, 560, 34, s.heading, { family: ctx.F.display, size: 26, color: ctx.C.ink });
184
+ const cols = (s.columns || []).slice(0, 4); const n = cols.length || 1;
185
+ const span = W - M * 2, gap = 16, colW = (span - (n - 1) * gap) / n, cy = s.heading ? 188 : 150;
186
+ cols.forEach((c, i) => { const cx = M + i * (colW + gap);
187
+ if (i > 0) ctx.fill(ctx.shape('RECTANGLE', slide, cx - gap / 2, cy, 0.75, 120), ctx.C.line);
188
+ ctx.box(slide, cx, cy, colW - 8, 30, c.h || c.title || '', { family: ctx.F.display, size: 17, color: ctx.C.ink });
189
+ ctx.box(slide, cx, cy + 34, colW - 8, 120, c.b || c.body || '', { family: ctx.F.body, size: 11.5, color: ctx.C.soft, line: 134 }); });
190
+ },
191
+ panel(ctx, slide, s, spec) {
192
+ ctx.wordmark(slide, M, 34, 13, spec.brand);
193
+ if (s.heading) ctx.box(slide, M, 120, 260, 50, s.heading, { family: ctx.F.display, size: 30, color: ctx.C.ink });
194
+ if (s.sub) ctx.box(slide, M, 178, 260, 160, s.sub, { family: ctx.F.body, size: 12.5, color: ctx.C.soft, line: 132 });
195
+ ctx.panel(slide, 360, 110, 312, s.panel || { rows: [] });
196
+ },
197
+ chips(ctx, slide, s, spec) {
198
+ ctx.wordmark(slide, M, 34, 13, spec.brand);
199
+ if (s.heading) ctx.box(slide, M, 110, 580, 34, s.heading, { family: ctx.F.display, size: 28, color: ctx.C.ink });
200
+ if (s.sub) ctx.box(slide, M, 162, 480, 64, s.sub, { family: ctx.F.body, size: 12.5, color: ctx.C.soft, line: 132 });
201
+ ctx.chips(slide, M, 250, s.chips || []);
202
+ if (s.mono) ctx.box(slide, M, 300, 520, 24, s.mono, { family: ctx.F.mono, size: 12, color: ctx.C.accent2 });
203
+ },
204
+ bignumber(ctx, slide, s, spec) {
205
+ ctx.wordmark(slide, M, 34, 13, spec.brand);
206
+ ctx.box(slide, M, 138, W - M * 2, 120, s.number || s.value || '', { family: ctx.F.display, size: 92, color: ctx.C.accent2, line: 100 });
207
+ if (s.label) ctx.box(slide, M, 268, 520, 30, s.label, { family: ctx.F.body, size: 16, color: ctx.C.ink });
208
+ if (s.sub) ctx.box(slide, M, 300, 480, 50, s.sub, { family: ctx.F.body, size: 12.5, color: ctx.C.soft, line: 130 });
209
+ },
210
+ close(ctx, slide, s, spec) {
211
+ ctx.rule(slide, (W - 48) / 2, 150, 48);
212
+ const name = (spec.brand && spec.brand.name) || 'Atris';
213
+ const ac = (spec.brand && spec.brand.accent) || '';
214
+ const id = ctx.box(slide, 0, 168, W, 64, name + ac, { family: ctx.F.display, size: 52, color: ctx.C.ink, align: 'CENTER' });
215
+ if (ac && id) ctx.styleRange(id, name.length, name.length + ac.length, { family: ctx.F.display, size: 52, color: ctx.C.accent });
216
+ if (s.tagline) ctx.box(slide, 0, 244, W, 24, s.tagline, { family: ctx.F.body, size: 14, color: ctx.C.soft, align: 'CENTER' });
217
+ if (s.buttons) ctx.buttons(slide, 296, s.buttons);
218
+ if (s.footer) ctx.box(slide, 0, 360, W, 20, s.footer, { family: ctx.F.body, size: 10, color: ctx.C.faint, align: 'CENTER' });
219
+ },
220
+ };
221
+
222
+ // ---------- public: spec -> requests ----------
223
+ function buildDeck(spec, opts = {}) {
224
+ const themes = opts.themes || THEMES;
225
+ const theme = themes[spec.theme] || THEMES.terminal;
226
+ const ctx = makeCtx(theme);
227
+ const slideIds = [];
228
+ (spec.slides || []).forEach((s, i) => {
229
+ const sid = `deck_slide_${i + 1}`; slideIds.push(sid);
230
+ ctx.createSlide(sid);
231
+ ctx.bg(sid, theme.color.bg);
232
+ (ARCHE[s.type] || ARCHE.statement)(ctx, sid, s, spec);
233
+ });
234
+ return { requests: ctx.requests, slideIds };
235
+ }
236
+
237
+ module.exports = { buildDeck, THEMES, ARCHE, sanitize, parseEmph, rgb, COLOR_ROLES };
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const escapeRegExp = require('./escape-regexp');
3
4
 
4
5
  const TASK_STATUS_BUCKETS = {
5
6
  backlog: new Set(['open']),
@@ -184,10 +185,6 @@ function getTaskCounts(atrisDir) {
184
185
  };
185
186
  }
186
187
 
187
- function escapeRegExp(value) {
188
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
189
- }
190
-
191
188
  function getTodayInboxItems(workspaceDir) {
192
189
  const atrisDir = path.join(workspaceDir, 'atris');
193
190
  const logsDir = path.join(atrisDir, 'logs');
package/lib/task-db.js CHANGED
@@ -59,6 +59,95 @@ const TASK_PLAN_TAGS = new Set([
59
59
  'ux',
60
60
  ]);
61
61
 
62
+ function todayLogName() {
63
+ const now = new Date();
64
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}.md`;
65
+ }
66
+
67
+ function compactLogText(value, max = 240) {
68
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
69
+ if (!text) return '';
70
+ return text.length > max ? `${text.slice(0, Math.max(0, max - 3)).trim()}...` : text;
71
+ }
72
+
73
+ function logFieldRows(fields) {
74
+ return Object.entries(fields)
75
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
76
+ .map(([key, value]) => `- ${key}: ${compactLogText(value, 500)}`);
77
+ }
78
+
79
+ function taskMemberCandidates(row, actor) {
80
+ const metadata = row && row.metadata && typeof row.metadata === 'object' ? row.metadata : {};
81
+ return [
82
+ metadata.assigned_to,
83
+ metadata.stage_owner,
84
+ metadata.planned_by,
85
+ metadata.agent_certified_by,
86
+ row && row.claimed_by,
87
+ actor,
88
+ ].map(value => String(value || '').trim())
89
+ .filter(Boolean)
90
+ .filter((value, index, list) => list.indexOf(value) === index);
91
+ }
92
+
93
+ function existingMemberSlug(workspaceRoot, row, actor) {
94
+ for (const candidate of taskMemberCandidates(row, actor)) {
95
+ if (!/^[a-zA-Z0-9._-]+$/.test(candidate)) continue;
96
+ const memberFile = path.join(workspaceRoot, 'atris', 'team', candidate, 'MEMBER.md');
97
+ if (fs.existsSync(memberFile)) return candidate;
98
+ }
99
+ return null;
100
+ }
101
+
102
+ function appendTaskCompletionLogs(db, row, { status, actor, action, proof } = {}) {
103
+ if (!row || !row.workspace_root || !fs.existsSync(path.join(row.workspace_root, 'atris'))) return {};
104
+ const logName = todayLogName();
105
+ const stamp = new Date().toTimeString().slice(0, 5);
106
+ const allRows = listTasks(db, { workspaceRoot: row.workspace_root });
107
+ const ref = taskDisplayRefMap(allRows).get(row.id) || shortestUniqueTaskRef(row.id, allRows.map(task => task.id), 8) || row.id;
108
+ const metadata = row.metadata && typeof row.metadata === 'object' ? row.metadata : {};
109
+ const proofText = compactLogText(proof || metadata.latest_agent_proof || metadata.verify || '', 500);
110
+ const member = existingMemberSlug(row.workspace_root, row, actor);
111
+ const title = status === 'failed' ? 'Task failed' : action === 'accepted' ? 'Task accepted' : 'Task completed';
112
+ const fields = {
113
+ task: ref,
114
+ title: row.title,
115
+ status,
116
+ action,
117
+ tag: row.tag,
118
+ member,
119
+ actor,
120
+ proof: proofText,
121
+ };
122
+
123
+ const projectDir = path.join(row.workspace_root, 'atris', 'logs', logName.slice(0, 4));
124
+ fs.mkdirSync(projectDir, { recursive: true });
125
+ const projectLogPath = path.join(projectDir, logName);
126
+ fs.appendFileSync(projectLogPath, [
127
+ `## ${stamp} · ${title}`,
128
+ ...logFieldRows(fields),
129
+ '',
130
+ ].join('\n'), 'utf8');
131
+
132
+ let memberLogPath = null;
133
+ if (member) {
134
+ const memberLogsDir = path.join(row.workspace_root, 'atris', 'team', member, 'logs');
135
+ fs.mkdirSync(memberLogsDir, { recursive: true });
136
+ memberLogPath = path.join(memberLogsDir, logName);
137
+ fs.appendFileSync(memberLogPath, [
138
+ `## ${stamp} · ${title}`,
139
+ ...logFieldRows({ ...fields, member }),
140
+ '',
141
+ ].join('\n'), 'utf8');
142
+ }
143
+
144
+ return {
145
+ project_log_path: projectLogPath,
146
+ member_log_path: memberLogPath,
147
+ member,
148
+ };
149
+ }
150
+
62
151
  const SCHEMA = `
63
152
  CREATE TABLE IF NOT EXISTS tasks (
64
153
  id TEXT PRIMARY KEY,
@@ -325,6 +414,7 @@ function listTasks(db, { workspaceRoot: ws, status, claimedBy, limit }) {
325
414
  ${where.length ? 'WHERE ' + where.join(' AND ') : ''}
326
415
  ORDER BY
327
416
  CASE status WHEN 'open' THEN 0 WHEN 'claimed' THEN 1 WHEN 'review' THEN 2 WHEN 'failed' THEN 3 WHEN 'done' THEN 4 ELSE 5 END,
417
+ CASE WHEN tag='endgame' THEN 0 ELSE 1 END,
328
418
  created_at DESC
329
419
  ${limit ? 'LIMIT ' + Number(limit) : ''}
330
420
  `;
@@ -379,7 +469,7 @@ function claimTask(db, { id, claimedBy }) {
379
469
  return { claimed: false, reason: 'already_' + row.status, claimed_by: row.claimed_by };
380
470
  }
381
471
 
382
- function doneTask(db, { id, status, actor, allowReview = false }) {
472
+ function doneTask(db, { id, status, actor, allowReview = false, action, proof } = {}) {
383
473
  if (!id) throw new Error('id required');
384
474
  const final = status || 'done';
385
475
  if (!['done', 'failed'].includes(final)) throw new Error('status must be done|failed');
@@ -392,16 +482,23 @@ function doneTask(db, { id, status, actor, allowReview = false }) {
392
482
  AND status IN (${allowedStatuses})
393
483
  `).run(final, now, now, id));
394
484
  if (result.changes === 1) {
395
- const row = db.prepare('SELECT id, workspace_root FROM tasks WHERE id = ?').get(id);
485
+ const row = getTask(db, id);
396
486
  appendTaskEvent(db, {
397
487
  taskId: id,
398
488
  workspaceRoot: row.workspace_root,
399
489
  actor: actor || process.env.ATRIS_AGENT_ID || process.env.USER || null,
400
490
  eventType: final === 'done' ? 'completed' : 'blocked',
401
- payload: { status: final },
491
+ payload: { status: final, action: action || final },
492
+ });
493
+ const logs = appendTaskCompletionLogs(db, row, {
494
+ status: final,
495
+ actor: actor || process.env.ATRIS_AGENT_ID || process.env.USER || null,
496
+ action: action || final,
497
+ proof,
402
498
  });
499
+ return { updated: true, logs };
403
500
  }
404
- return { updated: result.changes === 1 };
501
+ return { updated: false };
405
502
  }
406
503
 
407
504
  function readyTask(db, { id, actor, proof, lesson, nextTask }) {
package/lib/task-proof.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const GENERIC_COMPLETION_PROOF_RE = /^(?:done|done now|complete|completed|finished|fixed|handled|ship|shipped|ok|okay|yes|yep|looks good|looks good to me|all set|should be good|works now|approved|approve|lgtm|failed)$/i;
4
4
 
5
- const COMMAND_PROOF_RE = /\b(?:npm\s+run|npm\s+test|node\s+--test|node\s+scripts\/|pnpm\b|yarn\b|npx\b|pytest\b|python\s+-m|tsc\b|vite\s+build|git\s+diff\s+--check|curl\b|atris\s+task|\.\/ax\b|ax\s+--|test\s+-s)\b/i;
5
+ const COMMAND_PROOF_RE = /\b(?:npm\s+run|npm\s+test|node\s+--test|node\s+scripts\/|pnpm\b|yarn\b|npx\b|pytest\b|python\s+-m|tsc\b|vite\s+build|git\s+diff\s+--(?:check|exit-code|quiet)|grep\s+-[A-Za-z]*q[A-Za-z]*|rg\s+(?:-\S+\s+)*(?:"[^"]+"|'[^']+'|\S+)\s+(?:\.{0,2}\/|~\/|\/|[\w.-]+\/|[\w.-]+\.[A-Za-z0-9]|\b(?:atris|bin|commands|lib|scripts|src|test)\b)|diff\s+(?:-u|--brief)|cmp\s+-s|curl\b|atris\s+task|\.\/ax\b|ax\s+--|test\s+-s)\b/i;
6
6
  const FILE_PROOF_RE = /(?:^|[\s'"`])(?:\.{0,2}\/|~\/|\/Users\/|src\/|scripts\/|atris\/|backend\/|public\/|resources\/|package[.]json|main[.]js|preload[.]js|AGENTXP_PROOF[.]md)[^\s'"`,;)]*/i;
7
7
  const PATH_ONLY_PROOF_RE = /(?:^|[\s'"`])(?:\.{0,2}\/|~\/|\/Users\/|\/private\/|\/var\/|atris\/runs\/|\.atris\/state\/)[^\s'"`,;)]+(?:[.](?:json|jsonl|md|log|txt|png|jpg|jpeg|pdf))?/i;
8
8
  const RECEIPT_OR_ARTIFACT_RE = /\b(?:receipt|artifact|screenshot|log|trace|path=|file=|bytes=|model=|opened=|https?:\/\/)\b/i;