atris 3.16.1 → 3.17.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 +400 -30
- 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 +135 -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 +55 -25
- package/commands/run.js +615 -22
- package/commands/slop.js +173 -0
- package/commands/spaceship.js +39 -0
- package/commands/sync.js +0 -2
- package/commands/task.js +429 -37
- 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/escape-regexp.js +13 -0
- package/lib/file-ops.js +6 -3
- package/lib/journal.js +1 -1
- package/lib/lesson-contradiction.js +113 -0
- package/lib/policy-lessons.js +3 -2
- package/lib/pulse.js +401 -0
- package/lib/runner-command.js +156 -0
- package/lib/slides-deck.js +236 -0
- package/lib/state-detection.js +1 -4
- package/lib/task-db.js +101 -4
- package/lib/task-proof.js +1 -1
- 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
|
@@ -0,0 +1,236 @@
|
|
|
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) {
|
|
224
|
+
const theme = THEMES[spec.theme] || THEMES.terminal;
|
|
225
|
+
const ctx = makeCtx(theme);
|
|
226
|
+
const slideIds = [];
|
|
227
|
+
(spec.slides || []).forEach((s, i) => {
|
|
228
|
+
const sid = `deck_slide_${i + 1}`; slideIds.push(sid);
|
|
229
|
+
ctx.createSlide(sid);
|
|
230
|
+
ctx.bg(sid, theme.color.bg);
|
|
231
|
+
(ARCHE[s.type] || ARCHE.statement)(ctx, sid, s, spec);
|
|
232
|
+
});
|
|
233
|
+
return { requests: ctx.requests, slideIds };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
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;
|
package/lib/todo-fallback.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
|
+
const escapeRegExp = require('./escape-regexp');
|
|
11
12
|
|
|
12
13
|
function parseTodoFile(todoPath) {
|
|
13
14
|
if (!fs.existsSync(todoPath)) return { backlog: [], inProgress: [], review: [], completed: [] };
|
|
@@ -37,7 +38,7 @@ function cleanTaskTitle(text) {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
function parseSection(content, sectionName) {
|
|
40
|
-
const escaped = sectionName
|
|
41
|
+
const escaped = escapeRegExp(sectionName);
|
|
41
42
|
const match = content.match(new RegExp(`(?:^|\\n)##\\s+${escaped}[^\\n]*\\n([\\s\\S]*?)(?=\\n##(?!#)\\s+|$)`, 'i'));
|
|
42
43
|
if (!match) return [];
|
|
43
44
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Canonical TODO/journal section classification. The same emoji-brittle logic
|
|
4
|
+
// existed (and was bug-fixed) independently in commands/now.js (CLI-287) and
|
|
5
|
+
// commands/brain.js (CLI-288); centralizing it here so a third copy can't drift.
|
|
6
|
+
//
|
|
7
|
+
// Headings may carry trailing decoration ("## In Progress 🔄", "## Completed ✅"),
|
|
8
|
+
// so detection uses \b (not \s*$) and section names match by prefix — "In Progress 🔄"
|
|
9
|
+
// counts as "In Progress", while "Backlogged Notes" does NOT count as "Backlog".
|
|
10
|
+
|
|
11
|
+
const RENDERED_SECTION_RE = /^##\s+(Backlog|In Progress|Blocked|Completed)\b/m;
|
|
12
|
+
|
|
13
|
+
// True when the document uses rendered task sections (vs a flat/legacy bullet list).
|
|
14
|
+
function hasRenderedSections(text) {
|
|
15
|
+
return RENDERED_SECTION_RE.test(String(text || ''));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// A parsed heading name matches a canonical section if it equals it or is that
|
|
19
|
+
// name followed by decoration (a space then emoji/text).
|
|
20
|
+
function sectionMatches(name, target) {
|
|
21
|
+
const n = String(name || '');
|
|
22
|
+
return n === target || n.startsWith(`${target} `);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isOpenSection(name) {
|
|
26
|
+
return sectionMatches(name, 'Backlog') || sectionMatches(name, 'In Progress');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isDoneSection(name) {
|
|
30
|
+
return sectionMatches(name, 'Completed');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { hasRenderedSections, sectionMatches, isOpenSection, isDoneSection, RENDERED_SECTION_RE };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atris",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.17.0",
|
|
4
4
|
"main": "bin/atris.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"atris": "bin/atris.js",
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"atris.md",
|
|
20
20
|
"GETTING_STARTED.md",
|
|
21
21
|
"PERSONA.md",
|
|
22
|
-
"atris/atrisDev.md",
|
|
23
22
|
"atris/CLAUDE.md",
|
|
24
23
|
"atris/GEMINI.md",
|
|
25
24
|
"atris/GETTING_STARTED.md",
|
package/utils/api.js
CHANGED
|
@@ -216,6 +216,7 @@ function streamProChat(url, token, body, showTools = false) {
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
let buffer = '';
|
|
219
|
+
let emittedText = false;
|
|
219
220
|
|
|
220
221
|
res.on('data', (chunk) => {
|
|
221
222
|
buffer += chunk.toString();
|
|
@@ -232,10 +233,16 @@ function streamProChat(url, token, body, showTools = false) {
|
|
|
232
233
|
|
|
233
234
|
if (msg.type === 'system_init' && showTools) {
|
|
234
235
|
console.log(`[System] Tools available: ${msg.tools?.join(', ') || 'none'}`);
|
|
236
|
+
} else if (msg.type === 'text_delta') {
|
|
237
|
+
if (msg.content) {
|
|
238
|
+
emittedText = true;
|
|
239
|
+
process.stdout.write(msg.content);
|
|
240
|
+
}
|
|
235
241
|
} else if (msg.type === 'assistant') {
|
|
236
242
|
if (msg.content && Array.isArray(msg.content)) {
|
|
237
243
|
for (const block of msg.content) {
|
|
238
244
|
if (block.type === 'text') {
|
|
245
|
+
emittedText = true;
|
|
239
246
|
process.stdout.write(block.text);
|
|
240
247
|
}
|
|
241
248
|
}
|
|
@@ -245,11 +252,14 @@ function streamProChat(url, token, body, showTools = false) {
|
|
|
245
252
|
} else if (msg.type === 'tool_result' && showTools) {
|
|
246
253
|
const preview = msg.content?.substring(0, 100) || '';
|
|
247
254
|
console.log(`[✓ Result]: ${preview}${msg.content?.length > 100 ? '...' : ''}`);
|
|
248
|
-
} else if (msg.type === 'result') {
|
|
255
|
+
} else if (msg.type === 'result' && !emittedText) {
|
|
249
256
|
if (msg.result) {
|
|
250
257
|
process.stdout.write(msg.result);
|
|
251
258
|
}
|
|
259
|
+
} else if (msg.type === 'error') {
|
|
260
|
+
reject(new Error(msg.error || 'Atris stream error'));
|
|
252
261
|
} else if (msg.chunk) {
|
|
262
|
+
emittedText = true;
|
|
253
263
|
process.stdout.write(msg.chunk);
|
|
254
264
|
}
|
|
255
265
|
} catch (e) {
|
|
@@ -270,7 +280,9 @@ function streamProChat(url, token, body, showTools = false) {
|
|
|
270
280
|
const msg = JSON.parse(data);
|
|
271
281
|
if (msg.chunk) {
|
|
272
282
|
process.stdout.write(msg.chunk);
|
|
273
|
-
} else if (msg.type === '
|
|
283
|
+
} else if (msg.type === 'text_delta' && msg.content) {
|
|
284
|
+
process.stdout.write(msg.content);
|
|
285
|
+
} else if (msg.type === 'result' && msg.result && !emittedText) {
|
|
274
286
|
process.stdout.write(msg.result);
|
|
275
287
|
}
|
|
276
288
|
} catch (e) {
|