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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// atris theme — brand themes for the whole design system. Define your colors and
|
|
2
|
+
// fonts once in .atris/theme.json; every deck, HTML page, and site uses them.
|
|
3
|
+
//
|
|
4
|
+
// atris theme create guided interview -> your own theme (alias: new)
|
|
5
|
+
// atris theme edit <name> re-run the interview to tweak an existing theme
|
|
6
|
+
// atris theme init scaffold a starter .atris/theme.json
|
|
7
|
+
// atris theme list list built-in + project themes
|
|
8
|
+
// atris theme show <name> print a resolved theme
|
|
9
|
+
|
|
10
|
+
const readline = require('readline');
|
|
11
|
+
const {
|
|
12
|
+
mergedThemes, writeStarterTheme, loadProjectThemes, loadThemeRecipe, PROJECT_THEME_FILE,
|
|
13
|
+
MOODS, MOOD_NAMES, FONT_PERSONALITIES, buildTheme, themeWarnings, upsertProjectTheme,
|
|
14
|
+
resolveMode, isHex, normHex, hexToRgb,
|
|
15
|
+
} = require('../lib/theme');
|
|
16
|
+
const { THEMES: HTML_THEMES } = require('../lib/html-render');
|
|
17
|
+
|
|
18
|
+
// ---------- small terminal helpers ----------
|
|
19
|
+
function parseFlags(argv) {
|
|
20
|
+
const flags = {}; const pos = [];
|
|
21
|
+
for (let i = 0; i < argv.length; i++) {
|
|
22
|
+
const a = argv[i];
|
|
23
|
+
if (a === '-y' || a === '--yes') { flags.yes = true; continue; }
|
|
24
|
+
if (a.startsWith('--')) {
|
|
25
|
+
const key = a.slice(2);
|
|
26
|
+
const next = argv[i + 1];
|
|
27
|
+
if (next != null && !next.startsWith('--')) { flags[key] = next; i++; }
|
|
28
|
+
else flags[key] = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
pos.push(a);
|
|
32
|
+
}
|
|
33
|
+
return { flags, pos };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// a colored block when we're on a real terminal, two blank cells otherwise (clean pipes)
|
|
37
|
+
function sw(hex) {
|
|
38
|
+
const c = hexToRgb(hex);
|
|
39
|
+
if (!c || !process.stdout.isTTY) return ' ';
|
|
40
|
+
return `\x1b[48;2;${c.r};${c.g};${c.b}m \x1b[0m`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function printPreview(name, answers, theme) {
|
|
44
|
+
const c = theme.color;
|
|
45
|
+
const mode = resolveMode(answers.mood || 'editorial', answers.mode);
|
|
46
|
+
console.log(`\n ${name} · ${answers.mood || 'editorial'} ${mode}`);
|
|
47
|
+
const row = (label, hex) => console.log(` ${sw(hex)} ${label.padEnd(8)} ${hex}`);
|
|
48
|
+
row('bg', c.bg);
|
|
49
|
+
row('ink', c.ink);
|
|
50
|
+
row('accent', c.accent);
|
|
51
|
+
row('accent2', c.accent2);
|
|
52
|
+
console.log(` ${theme.fonts.display} display, ${theme.fonts.body} body`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------- the guided interview (tasteful vocabulary) ----------
|
|
56
|
+
async function interview(defaults = {}) {
|
|
57
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
58
|
+
const ask = (q) => new Promise((res) => rl.question(q, res));
|
|
59
|
+
const ans = {};
|
|
60
|
+
try {
|
|
61
|
+
console.log('\n let\'s make your theme. five quick choices, all changeable later.\n');
|
|
62
|
+
|
|
63
|
+
const dn = defaults.name || 'brand';
|
|
64
|
+
ans.name = (await ask(` 1/5 name it [${dn}]: `)).trim() || dn;
|
|
65
|
+
|
|
66
|
+
console.log('\n 2/5 what should it feel like?');
|
|
67
|
+
MOOD_NAMES.forEach((m, i) => console.log(` ${i + 1}. ${m.padEnd(10)} ${MOODS[m].blurb}`));
|
|
68
|
+
const dmIdx = Math.max(0, MOOD_NAMES.indexOf(defaults.mood || 'editorial'));
|
|
69
|
+
const moodPick = (await ask(` pick 1-${MOOD_NAMES.length} [${dmIdx + 1}]: `)).trim();
|
|
70
|
+
ans.mood = MOOD_NAMES[(parseInt(moodPick, 10) - 1)] || MOOD_NAMES[dmIdx];
|
|
71
|
+
const mood = MOODS[ans.mood];
|
|
72
|
+
|
|
73
|
+
if (mood.forceMode) {
|
|
74
|
+
ans.mode = mood.forceMode;
|
|
75
|
+
console.log(` (${ans.mood} is ${mood.forceMode}-only)`);
|
|
76
|
+
} else {
|
|
77
|
+
const dmode = defaults.mode || mood.defaultMode;
|
|
78
|
+
const mp = (await ask(`\n 3/5 light or dark? [${dmode[0]}]: `)).trim().toLowerCase();
|
|
79
|
+
ans.mode = mp.startsWith('l') ? 'light' : mp.startsWith('d') ? 'dark' : dmode;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log('\n 4/5 your accent — the one color that is unmistakably yours:');
|
|
83
|
+
mood.swatches.forEach((s, i) => console.log(` ${i + 1}. ${sw(s)} ${s}`));
|
|
84
|
+
const dac = normHex(defaults.accent) || mood.swatches[0];
|
|
85
|
+
const ap = (await ask(` pick 1-${mood.swatches.length}, or paste a hex [${dac}]: `)).trim();
|
|
86
|
+
if (!ap) ans.accent = dac;
|
|
87
|
+
else if (/^[1-9]$/.test(ap) && mood.swatches[parseInt(ap, 10) - 1]) ans.accent = mood.swatches[parseInt(ap, 10) - 1];
|
|
88
|
+
else if (isHex(ap)) ans.accent = normHex(ap);
|
|
89
|
+
else { console.log(` '${ap}' is not 1-${mood.swatches.length} or a hex, keeping ${dac}`); ans.accent = dac; }
|
|
90
|
+
|
|
91
|
+
console.log('\n 5/5 type personality:');
|
|
92
|
+
const fontKeys = Object.keys(FONT_PERSONALITIES);
|
|
93
|
+
fontKeys.forEach((k, i) => console.log(` ${i + 1}. ${k.padEnd(15)} ${FONT_PERSONALITIES[k].display} / ${FONT_PERSONALITIES[k].body}`));
|
|
94
|
+
const dfIdx = fontKeys.indexOf(typeof defaults.fonts === 'string' ? defaults.fonts : mood.fonts);
|
|
95
|
+
const fp = (await ask(` pick 1-${fontKeys.length} [${(dfIdx < 0 ? 0 : dfIdx) + 1}]: `)).trim();
|
|
96
|
+
ans.fonts = fontKeys[(parseInt(fp, 10) - 1)] || fontKeys[dfIdx < 0 ? 0 : dfIdx];
|
|
97
|
+
} finally {
|
|
98
|
+
rl.close();
|
|
99
|
+
}
|
|
100
|
+
return ans;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function confirmSync(q) {
|
|
104
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
105
|
+
return new Promise((res) => rl.question(q, (a) => { rl.close(); res(!String(a).trim().toLowerCase().startsWith('n')); }));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// shared create/edit flow. defaults seed an edit; flags override anything; interactive
|
|
109
|
+
// only when there's a TTY and no defining flags.
|
|
110
|
+
async function createFlow({ name, defaults = {}, flags }) {
|
|
111
|
+
const answers = { ...defaults };
|
|
112
|
+
if (flags.mood) answers.mood = flags.mood;
|
|
113
|
+
if (flags.mode) answers.mode = flags.mode;
|
|
114
|
+
if (flags.accent) answers.accent = flags.accent;
|
|
115
|
+
if (flags.accent2) answers.accent2 = flags.accent2;
|
|
116
|
+
if (flags.fonts) answers.fonts = flags.fonts;
|
|
117
|
+
let themeName = name || flags.name || defaults.name || 'brand';
|
|
118
|
+
|
|
119
|
+
const hasDefiningFlag = Boolean(flags.mood || flags.mode || flags.accent || flags.accent2 || flags.fonts || flags.name);
|
|
120
|
+
const interactive = Boolean(process.stdin.isTTY) && !flags.yes && !hasDefiningFlag;
|
|
121
|
+
|
|
122
|
+
if (interactive) {
|
|
123
|
+
const r = await interview({ name: themeName, ...answers });
|
|
124
|
+
Object.assign(answers, r);
|
|
125
|
+
themeName = r.name || themeName;
|
|
126
|
+
}
|
|
127
|
+
themeName = String(themeName).trim() || 'brand';
|
|
128
|
+
|
|
129
|
+
if (answers.mood && !MOODS[answers.mood]) { console.error(` unknown mood "${answers.mood}". try: ${MOOD_NAMES.join(', ')}`); return 2; }
|
|
130
|
+
if (answers.accent && !isHex(answers.accent)) { console.error(` not a hex color: ${answers.accent}`); return 2; }
|
|
131
|
+
if (answers.accent2 && !isHex(answers.accent2)) { console.error(` not a hex color: ${answers.accent2}`); return 2; }
|
|
132
|
+
if (answers.fonts && typeof answers.fonts === 'string' && !FONT_PERSONALITIES[answers.fonts]) {
|
|
133
|
+
console.error(` unknown font personality "${answers.fonts}". try: ${Object.keys(FONT_PERSONALITIES).join(', ')}`); return 2;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const theme = buildTheme(answers);
|
|
137
|
+
printPreview(themeName, answers, theme);
|
|
138
|
+
const warns = themeWarnings(theme);
|
|
139
|
+
if (warns.length) { console.log('\n heads up:'); warns.forEach((w) => console.log(` ! ${w}`)); }
|
|
140
|
+
|
|
141
|
+
if (interactive) {
|
|
142
|
+
const ok = await confirmSync('\n save this theme? [enter = yes, n = cancel] ');
|
|
143
|
+
if (!ok) { console.log(' cancelled, nothing written.\n'); return 0; }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const recipe = {
|
|
147
|
+
mood: answers.mood || 'editorial',
|
|
148
|
+
mode: resolveMode(answers.mood || 'editorial', answers.mode),
|
|
149
|
+
accent: theme.color.accent,
|
|
150
|
+
accent2: theme.color.accent2,
|
|
151
|
+
fonts: typeof answers.fonts === 'string' ? answers.fonts : (MOODS[answers.mood || 'editorial'].fonts),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
let res;
|
|
155
|
+
try { res = upsertProjectTheme(themeName, theme, process.cwd(), recipe); }
|
|
156
|
+
catch (e) { console.error(` ${e.message}`); return 1; }
|
|
157
|
+
|
|
158
|
+
console.log(`\n ✓ ${res.action} theme "${res.name}" in ${PROJECT_THEME_FILE} (${res.count} theme${res.count === 1 ? '' : 's'})`);
|
|
159
|
+
console.log(' use it anywhere:');
|
|
160
|
+
console.log(` atris deck from doc.md --html --theme ${res.name}`);
|
|
161
|
+
console.log(` atris site ./docs --theme ${res.name}`);
|
|
162
|
+
console.log(` tweak it later: atris theme edit ${res.name}\n`);
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function run(argv) {
|
|
167
|
+
const sub = argv[0];
|
|
168
|
+
const rest = argv.slice(1);
|
|
169
|
+
|
|
170
|
+
if (sub === 'create' || sub === 'new') {
|
|
171
|
+
const { flags, pos } = parseFlags(rest);
|
|
172
|
+
return createFlow({ name: pos[0], flags });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (sub === 'edit') {
|
|
176
|
+
const { flags, pos } = parseFlags(rest);
|
|
177
|
+
const name = pos[0] || flags.name;
|
|
178
|
+
if (!name) { console.error(' usage: atris theme edit <name>'); return 2; }
|
|
179
|
+
const existing = loadProjectThemes()[name];
|
|
180
|
+
if (!existing) { console.error(` no project theme "${name}". make one with: atris theme create ${name}`); return 2; }
|
|
181
|
+
// prefer the saved recipe; otherwise seed from the resolved colors/fonts
|
|
182
|
+
const recipe = loadThemeRecipe(name);
|
|
183
|
+
const defaults = recipe || { accent: existing.color.accent, accent2: existing.color.accent2, fonts: existing.fonts };
|
|
184
|
+
defaults.name = name;
|
|
185
|
+
return createFlow({ name, defaults, flags });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (sub === 'init') {
|
|
189
|
+
const { file, already } = writeStarterTheme();
|
|
190
|
+
console.log(already
|
|
191
|
+
? `\n already exists: ${file}\n customize it with: atris theme edit brand\n`
|
|
192
|
+
: `\n ✓ brand theme scaffolded: ${file}\n or build one by feel: atris theme create\n`);
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (sub === 'show') {
|
|
197
|
+
const name = rest[0];
|
|
198
|
+
const t = mergedThemes(HTML_THEMES)[name];
|
|
199
|
+
if (!t) { console.error(` unknown theme "${name}"`); return 2; }
|
|
200
|
+
console.log(JSON.stringify(t, null, 2));
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// list (default)
|
|
205
|
+
const project = loadProjectThemes();
|
|
206
|
+
const all = mergedThemes(HTML_THEMES);
|
|
207
|
+
console.log('\n atris themes:\n');
|
|
208
|
+
for (const name of Object.keys(all)) {
|
|
209
|
+
const tag = project[name] ? 'project' : 'built-in';
|
|
210
|
+
console.log(` ${sw(all[name].color.accent)} ${name.padEnd(12)} accent ${all[name].color.accent} bg ${all[name].color.bg} (${tag})`);
|
|
211
|
+
}
|
|
212
|
+
console.log(`\n make your own by feel: atris theme create`);
|
|
213
|
+
console.log(` themes live in ${PROJECT_THEME_FILE}. Used by deck, html, and site.\n`);
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = { run };
|
package/commands/verify.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { spawnSync } = require('child_process');
|
|
4
|
+
const escapeRegExp = require('../lib/escape-regexp');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* atris verify [task] - Validate work is actually done
|
|
@@ -354,7 +355,8 @@ function checkDocsVsChanges(cwd, atrisDir) {
|
|
|
354
355
|
*/
|
|
355
356
|
function findTaskInContent(content, taskId) {
|
|
356
357
|
// Try exact ID match (T1, T2, etc.)
|
|
357
|
-
const
|
|
358
|
+
const safeId = escapeRegExp(taskId);
|
|
359
|
+
const idPattern = new RegExp(`### (T${safeId}|Task ${safeId})[:\\s]([\\s\\S]*?)(?=\\n###|\\n##|$)`, 'i');
|
|
358
360
|
let match = content.match(idPattern);
|
|
359
361
|
|
|
360
362
|
if (match) {
|
|
@@ -506,7 +508,7 @@ function verifyRubric(slug, section, opts = {}) {
|
|
|
506
508
|
const content = fs.readFileSync(validateFile, 'utf8');
|
|
507
509
|
// Match "## <section>" (case-insensitive, anchored), skipping optional
|
|
508
510
|
// prose until the first ```bash or ```sh fence. Extract until the closing ```.
|
|
509
|
-
const escaped = section
|
|
511
|
+
const escaped = escapeRegExp(section);
|
|
510
512
|
const pattern = new RegExp(
|
|
511
513
|
`^##\\s+${escaped}\\s*$[\\s\\S]*?\\n\`\`\`(?:bash|sh)?\\s*\\n([\\s\\S]*?)\\n\`\`\``,
|
|
512
514
|
'mi'
|
|
@@ -531,5 +533,7 @@ function verifyRubric(slug, section, opts = {}) {
|
|
|
531
533
|
|
|
532
534
|
module.exports = {
|
|
533
535
|
verifyAtris,
|
|
534
|
-
verifyRubric
|
|
536
|
+
verifyRubric,
|
|
537
|
+
findTaskInContent,
|
|
538
|
+
escapeRegExp
|
|
535
539
|
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Activity stream: normalize the workspace's receipt channels into ONE
|
|
4
|
+
// time-ordered feed of what the agent actually did, plus a heartbeat summary.
|
|
5
|
+
// The task board shows a static inventory of tasks; this turns the same data
|
|
6
|
+
// into "what happened, in order" — the stream you watch for hours.
|
|
7
|
+
|
|
8
|
+
const TS_KEYS = ['ts', 'at', 'created_at', 'accepted_at', 'timestamp', 'updated_at'];
|
|
9
|
+
|
|
10
|
+
function pickTs(row) {
|
|
11
|
+
for (const k of TS_KEYS) {
|
|
12
|
+
if (row && row[k]) {
|
|
13
|
+
const ms = Date.parse(row[k]);
|
|
14
|
+
if (Number.isFinite(ms)) return { iso: String(row[k]), ms };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return { iso: null, ms: 0 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function clip(value, n = 100) {
|
|
21
|
+
const t = String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
|
|
22
|
+
return t.length > n ? `${t.slice(0, n - 1)}…` : t;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function num(v) {
|
|
26
|
+
return v == null || v === '' || Number.isNaN(Number(v)) ? null : Number(v);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// --- per-channel normalizers (each returns a uniform event or null) ---
|
|
30
|
+
|
|
31
|
+
function normalizePulse(row) {
|
|
32
|
+
if (!row || row.phase !== 'finished') return null; // only completed ticks are events
|
|
33
|
+
const { iso, ms } = pickTs(row);
|
|
34
|
+
const reward = num(row.reward);
|
|
35
|
+
return {
|
|
36
|
+
ts: iso, ms,
|
|
37
|
+
source: 'pulse',
|
|
38
|
+
kind: row.actor === 'autopilot' ? 'autopilot' : 'heartbeat',
|
|
39
|
+
title: clip(row.what || `pulse tick #${row.tick_index}`),
|
|
40
|
+
detail: row.verify_passed == null ? '' : (row.verify_passed ? 'verify pass' : 'verify FAIL'),
|
|
41
|
+
status: row.verify_passed === false ? 'bad' : (reward > 0 ? 'good' : 'neutral'),
|
|
42
|
+
reward,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeScorecard(row) {
|
|
47
|
+
if (!row) return null;
|
|
48
|
+
const { iso, ms } = pickTs(row);
|
|
49
|
+
const reward = num(row.reward);
|
|
50
|
+
return {
|
|
51
|
+
ts: iso, ms,
|
|
52
|
+
source: 'reward',
|
|
53
|
+
kind: row.source || row.member || 'tick',
|
|
54
|
+
title: clip(row.what_shipped || 'tick scored'),
|
|
55
|
+
detail: reward == null ? '' : `reward ${reward}`,
|
|
56
|
+
status: reward != null && reward > 0 ? 'good' : (reward != null && reward < 0 ? 'bad' : 'neutral'),
|
|
57
|
+
reward,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeTaskEpisode(row) {
|
|
62
|
+
if (!row) return null;
|
|
63
|
+
const { iso, ms } = pickTs(row);
|
|
64
|
+
const state = row.state || {};
|
|
65
|
+
const action = row.action || state.status || 'updated';
|
|
66
|
+
const reward = num(row.reward);
|
|
67
|
+
return {
|
|
68
|
+
ts: iso, ms,
|
|
69
|
+
source: 'task',
|
|
70
|
+
kind: state.status || action,
|
|
71
|
+
title: clip(state.title || row.task_id),
|
|
72
|
+
detail: row.lesson ? clip(`lesson: ${row.lesson}`, 70) : clip(action, 24),
|
|
73
|
+
status: reward != null && reward < 0 ? 'bad' : (state.status === 'review' ? 'review' : 'neutral'),
|
|
74
|
+
reward,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeXp(row) {
|
|
79
|
+
if (!row) return null;
|
|
80
|
+
const { iso, ms } = pickTs(row);
|
|
81
|
+
return {
|
|
82
|
+
ts: iso, ms,
|
|
83
|
+
source: 'xp',
|
|
84
|
+
kind: row.outcome || 'accepted',
|
|
85
|
+
title: clip(row.title || 'work accepted'),
|
|
86
|
+
detail: `+${row.xp || 0} XP${row.actor ? ` · ${row.actor}` : ''}`,
|
|
87
|
+
status: 'good',
|
|
88
|
+
reward: num(row.reward),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeMissionEvent(row) {
|
|
93
|
+
if (!row) return null;
|
|
94
|
+
const { iso, ms } = pickTs(row);
|
|
95
|
+
const type = row.type || 'event';
|
|
96
|
+
const p = row.payload || {};
|
|
97
|
+
return {
|
|
98
|
+
ts: iso, ms,
|
|
99
|
+
source: 'mission',
|
|
100
|
+
kind: type,
|
|
101
|
+
title: clip(p.summary || p.objective || p.reason || p.next_action || type),
|
|
102
|
+
detail: clip(row.actor || '', 24),
|
|
103
|
+
status: /error|fail|halt|stop|paused/i.test(type) ? 'bad' : 'neutral',
|
|
104
|
+
reward: null,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- the feed ---
|
|
109
|
+
|
|
110
|
+
function buildActivityStream(sources = {}, opts = {}) {
|
|
111
|
+
const limit = opts.limit || 60;
|
|
112
|
+
const events = [];
|
|
113
|
+
const push = (rows, fn) => {
|
|
114
|
+
for (const r of rows || []) {
|
|
115
|
+
const e = fn(r);
|
|
116
|
+
if (e && e.ms) events.push(e);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
push(sources.pulseReceipts, normalizePulse);
|
|
120
|
+
push(sources.scorecards, normalizeScorecard);
|
|
121
|
+
push(sources.taskEpisodes, normalizeTaskEpisode);
|
|
122
|
+
push(sources.xpReceipts, normalizeXp);
|
|
123
|
+
push(sources.missionEvents, normalizeMissionEvent);
|
|
124
|
+
events.sort((a, b) => (b.ms - a.ms) || a.source.localeCompare(b.source)); // newest first, stable
|
|
125
|
+
return events.slice(0, limit);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- heartbeat: is the agent alive, and what did it just do ---
|
|
129
|
+
|
|
130
|
+
function buildHeartbeat(pulseReceipts, now = Date.now()) {
|
|
131
|
+
const { summarizePulse } = require('./pulse');
|
|
132
|
+
const s = summarizePulse(pulseReceipts || [], now);
|
|
133
|
+
const lastMs = s.last_tick_ts ? Date.parse(s.last_tick_ts) : null;
|
|
134
|
+
const ageMin = Number.isFinite(lastMs) ? Math.max(0, Math.round((now - lastMs) / 60000)) : null;
|
|
135
|
+
const finished = (pulseReceipts || []).filter((r) => r && r.phase === 'finished');
|
|
136
|
+
const last = finished.length ? finished[finished.length - 1] : null;
|
|
137
|
+
let state = 'idle';
|
|
138
|
+
if (s.stale.stale) state = 'stale';
|
|
139
|
+
else if (s.total_ticks > 0) state = 'alive';
|
|
140
|
+
return {
|
|
141
|
+
state, // 'alive' | 'stale' | 'idle'
|
|
142
|
+
alive: state === 'alive',
|
|
143
|
+
stale_reason: s.stale.stale ? s.stale.reason : null,
|
|
144
|
+
last_tick_ts: s.last_tick_ts,
|
|
145
|
+
last_tick_age_min: ageMin,
|
|
146
|
+
last_what: last ? last.what : null,
|
|
147
|
+
last_reward: last ? num(last.reward) : null,
|
|
148
|
+
total_ticks: s.total_ticks,
|
|
149
|
+
reward_sum: s.reward_sum,
|
|
150
|
+
verify_pass: s.verify_pass,
|
|
151
|
+
verify_fail: s.verify_fail,
|
|
152
|
+
orphan_ticks: s.orphan_ticks,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
pickTs,
|
|
158
|
+
clip,
|
|
159
|
+
normalizePulse,
|
|
160
|
+
normalizeScorecard,
|
|
161
|
+
normalizeTaskEpisode,
|
|
162
|
+
normalizeXp,
|
|
163
|
+
normalizeMissionEvent,
|
|
164
|
+
buildActivityStream,
|
|
165
|
+
buildHeartbeat,
|
|
166
|
+
};
|
|
@@ -54,6 +54,28 @@ function safeNodePathArgs(args) {
|
|
|
54
54
|
return args.every(token => safeRelativePathToken(token));
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function safeNodeTestArgs(args) {
|
|
58
|
+
let expectPattern = false;
|
|
59
|
+
for (const token of args) {
|
|
60
|
+
if (expectPattern) {
|
|
61
|
+
if (!safeVerifyToken(token) || hasUnsafePathSegment(token)) return false;
|
|
62
|
+
expectPattern = false;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (token === '--test-name-pattern') {
|
|
66
|
+
expectPattern = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (String(token || '').startsWith('--test-name-pattern=')) {
|
|
70
|
+
const pattern = String(token).slice('--test-name-pattern='.length);
|
|
71
|
+
if (!pattern || !safeVerifyToken(pattern) || hasUnsafePathSegment(pattern)) return false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!safeRelativePathToken(token)) return false;
|
|
75
|
+
}
|
|
76
|
+
return !expectPattern;
|
|
77
|
+
}
|
|
78
|
+
|
|
57
79
|
function safeGitDiffCheckArgs(args) {
|
|
58
80
|
return args.length === 0 || (args.length <= 2 && args.every(safeGitRevToken));
|
|
59
81
|
}
|
|
@@ -182,7 +204,7 @@ function parseVerifyCommand(verify) {
|
|
|
182
204
|
|| (first === 'run' && Boolean(second) && !second.startsWith('-') && argv.length === 3)
|
|
183
205
|
))
|
|
184
206
|
|| (bin === 'node' && (
|
|
185
|
-
(first === '--test' &&
|
|
207
|
+
(first === '--test' && safeNodeTestArgs(argv.slice(2)))
|
|
186
208
|
|| (first === '--check' && argv.length === 3 && safeRelativePathToken(second))
|
|
187
209
|
|| (/^scripts\/[a-zA-Z0-9_./-]+$/.test(first || '') && safeNodePathArgs(argv.slice(1)))
|
|
188
210
|
))
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const PROFILE_REL_PATH = path.join('.atris', 'state', 'context_profile.json');
|
|
7
|
+
|
|
8
|
+
function profilePath(root = process.cwd()) {
|
|
9
|
+
return path.join(root, PROFILE_REL_PATH);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function loadContextProfile(root = process.cwd()) {
|
|
13
|
+
const target = profilePath(root);
|
|
14
|
+
if (!fs.existsSync(target)) return null;
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
17
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hasContextProfile(root = process.cwd()) {
|
|
24
|
+
const profile = loadContextProfile(root);
|
|
25
|
+
return Boolean(profile && String(profile.first_answer || '').trim());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compactText(value, max = 160) {
|
|
29
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
30
|
+
if (!text) return '';
|
|
31
|
+
return text.length > max ? `${text.slice(0, Math.max(0, max - 3)).trim()}...` : text;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function inferDomain(answer) {
|
|
35
|
+
const text = String(answer || '').toLowerCase();
|
|
36
|
+
if (/college|application|essay|common app|school/.test(text)) return 'school';
|
|
37
|
+
if (/code|coding|program|website|app|project/.test(text)) return 'building';
|
|
38
|
+
if (/week|schedule|calendar|plan|homework/.test(text)) return 'planning';
|
|
39
|
+
return 'general';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeQuestionText(value) {
|
|
43
|
+
return String(value || '')
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/[^\w\s']/g, ' ')
|
|
46
|
+
.replace(/\s+/g, ' ')
|
|
47
|
+
.trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isAtrisMetaQuestion(value) {
|
|
51
|
+
const text = normalizeQuestionText(value);
|
|
52
|
+
if (!text) return false;
|
|
53
|
+
|
|
54
|
+
const taskVerb = /\b(add|audit|build|change|create|debug|deploy|edit|fix|implement|make|patch|refactor|remove|review|run|ship|test|update|write)\b/;
|
|
55
|
+
if (taskVerb.test(text)) return false;
|
|
56
|
+
|
|
57
|
+
return [
|
|
58
|
+
/^(what'?s|what is|what are|who is|who are)\s+(atris|you|this)\b/,
|
|
59
|
+
/^what\s+atris\s+is\b/,
|
|
60
|
+
/^(what|how)\s+(does|do|can)\s+(atris|you|this)\b/,
|
|
61
|
+
/^(explain|describe|define)\s+(atris|this)\b/,
|
|
62
|
+
/^tell me\s+(about|what)\s+(atris|this)\b/,
|
|
63
|
+
/^why\s+atris\b/,
|
|
64
|
+
].some((pattern) => pattern.test(text));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function starterTaskTitle(answer) {
|
|
68
|
+
const summary = compactText(answer, 80) || 'first useful path';
|
|
69
|
+
return `First useful step: ${summary}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function saveContextProfile(root, answer, { source = 'first_contact' } = {}) {
|
|
73
|
+
const text = compactText(answer, 500);
|
|
74
|
+
if (!text) return null;
|
|
75
|
+
const target = profilePath(root);
|
|
76
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
77
|
+
const existing = loadContextProfile(root) || {};
|
|
78
|
+
const profile = {
|
|
79
|
+
schema: 'atris.context_profile.v1',
|
|
80
|
+
created_at: existing.created_at || new Date().toISOString(),
|
|
81
|
+
updated_at: new Date().toISOString(),
|
|
82
|
+
source,
|
|
83
|
+
first_answer: text,
|
|
84
|
+
inferred_domain: inferDomain(text),
|
|
85
|
+
};
|
|
86
|
+
fs.writeFileSync(target, `${JSON.stringify(profile, null, 2)}\n`, 'utf8');
|
|
87
|
+
return profile;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createStarterTask(root, answer) {
|
|
91
|
+
const atrisDir = path.join(root, 'atris');
|
|
92
|
+
if (!fs.existsSync(atrisDir)) return null;
|
|
93
|
+
try {
|
|
94
|
+
const taskDb = require('./task-db');
|
|
95
|
+
const db = taskDb.open();
|
|
96
|
+
const workspaceRoot = taskDb.workspaceRoot(root);
|
|
97
|
+
const title = starterTaskTitle(answer);
|
|
98
|
+
const sourceKey = taskDb.sourceKey('context-gatherer:first-task', title);
|
|
99
|
+
const added = taskDb.addTask(db, {
|
|
100
|
+
title,
|
|
101
|
+
tag: 'onboarding',
|
|
102
|
+
workspaceRoot,
|
|
103
|
+
sourceKey,
|
|
104
|
+
metadata: {
|
|
105
|
+
source: 'context_gatherer',
|
|
106
|
+
first_answer: compactText(answer, 500),
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
const rows = taskDb.listTasks(db, { workspaceRoot });
|
|
110
|
+
const displayRows = taskDb.withTaskDisplayRefs(rows);
|
|
111
|
+
const task = displayRows.find(row => row.id === added.id) || null;
|
|
112
|
+
try {
|
|
113
|
+
const todoPath = path.join(root, 'atris', 'TODO.md');
|
|
114
|
+
fs.writeFileSync(todoPath, taskDb.renderTodoMarkdown(rows, { title: 'TODO.md' }), 'utf8');
|
|
115
|
+
} catch {}
|
|
116
|
+
return {
|
|
117
|
+
id: added.id,
|
|
118
|
+
inserted: added.inserted,
|
|
119
|
+
display_id: task && task.display_id || null,
|
|
120
|
+
title,
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return {
|
|
124
|
+
error: error && error.message ? error.message : String(error),
|
|
125
|
+
title: starterTaskTitle(answer),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function shouldGatherContext({
|
|
131
|
+
root = process.cwd(),
|
|
132
|
+
userInput = '',
|
|
133
|
+
mapStatus = 'ready',
|
|
134
|
+
liveMissionsCount = 0,
|
|
135
|
+
wipCount = 0,
|
|
136
|
+
backlogCount = 0,
|
|
137
|
+
inboxCount = 0,
|
|
138
|
+
} = {}) {
|
|
139
|
+
if (hasContextProfile(root)) return false;
|
|
140
|
+
if (String(userInput || '').trim()) return true;
|
|
141
|
+
if (mapStatus !== 'ready') return true;
|
|
142
|
+
if (liveMissionsCount > 0 || wipCount > 0 || backlogCount > 0 || inboxCount > 0) return false;
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderPrompt({ projectName = 'this workspace' } = {}) {
|
|
147
|
+
return [
|
|
148
|
+
'',
|
|
149
|
+
'Context gatherer',
|
|
150
|
+
'----------------',
|
|
151
|
+
`Hi. I am Atris, and I want to understand ${projectName} before I suggest a path.`,
|
|
152
|
+
'',
|
|
153
|
+
'What are you trying to make easier right now: school, college apps, coding, a personal project, or something else?',
|
|
154
|
+
'Answer in one sentence. I will turn it into the first useful step.',
|
|
155
|
+
].join('\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
PROFILE_REL_PATH,
|
|
160
|
+
profilePath,
|
|
161
|
+
loadContextProfile,
|
|
162
|
+
hasContextProfile,
|
|
163
|
+
isAtrisMetaQuestion,
|
|
164
|
+
saveContextProfile,
|
|
165
|
+
createStarterTask,
|
|
166
|
+
shouldGatherContext,
|
|
167
|
+
renderPrompt,
|
|
168
|
+
starterTaskTitle,
|
|
169
|
+
inferDomain,
|
|
170
|
+
};
|