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/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 };
|
package/lib/state-detection.js
CHANGED
|
@@ -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
|
|
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:
|
|
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;
|