atris 3.82.12 → 3.82.15
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/atris/skills/README.md +1 -0
- package/atris/skills/clean-copy/SKILL.md +74 -0
- package/bin/atris.js +5 -0
- package/commands/autoland.js +25 -1
- package/commands/business-connect.js +148 -0
- package/commands/business-scoreboard.js +216 -0
- package/commands/business.js +30 -108
- package/commands/computer-login.js +327 -0
- package/commands/computer.js +13 -0
- package/commands/growth.js +151 -0
- package/commands/memory-live.js +16 -1
- package/commands/push.js +17 -3
- package/lib/auto-accept-certified.js +15 -6
- package/lib/known-commands.js +1 -1
- package/lib/memory-live.js +27 -3
- package/lib/skill-eval-gate.js +38 -3
- package/package.json +1 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('node:child_process');
|
|
4
|
+
const { apiRequestJson } = require('../utils/api');
|
|
5
|
+
const { loadCredentials } = require('../utils/auth');
|
|
6
|
+
const { NOT_LOGGED_IN, wantsHelp, wantsJson, tokenFrom, oneLine, printResult } = require('../lib/developer-api');
|
|
7
|
+
|
|
8
|
+
const PAGE_URL = 'https://atris.ai/dashboard/growth';
|
|
9
|
+
const NOT_FOUNDER = 'the growth room is only for the Atris team. sign in with a team account: atris login';
|
|
10
|
+
|
|
11
|
+
function showGrowthHelp() {
|
|
12
|
+
console.log('usage: atris growth [--json | --open]');
|
|
13
|
+
console.log(' atris growth note "what you did" [--day YYYY-MM-DD]');
|
|
14
|
+
console.log('signups, people, paying, revenue, cli installs and countries, the same numbers as the growth page.');
|
|
15
|
+
console.log(' --open open the growth page in your browser');
|
|
16
|
+
console.log(' --json print the full numbers once as one json object');
|
|
17
|
+
console.log(' note save a dated note that shows on the growth charts');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const count = (value) => Number(value || 0).toLocaleString('en-US');
|
|
21
|
+
|
|
22
|
+
/** "up 50%" against the week before; empty when there is nothing to compare with. */
|
|
23
|
+
function change(now, before) {
|
|
24
|
+
const a = Number(now || 0);
|
|
25
|
+
const b = Number(before || 0);
|
|
26
|
+
if (!b) return '';
|
|
27
|
+
const percent = Math.round(((a - b) / b) * 100);
|
|
28
|
+
if (percent === 0) return ', flat';
|
|
29
|
+
return `, ${percent > 0 ? 'up' : 'down'} ${Math.abs(percent)}%`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function shortDay(day) {
|
|
33
|
+
const date = new Date(`${day}T12:00:00Z`);
|
|
34
|
+
if (Number.isNaN(date.getTime())) return String(day || '');
|
|
35
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The growth numbers as a few short lines. */
|
|
39
|
+
function growthText(data = {}) {
|
|
40
|
+
const totals = data.totals || {};
|
|
41
|
+
const lines = [
|
|
42
|
+
`signups ${count(totals.signups_today)} today, ${count(totals.signups_7d)} last 7 days${change(totals.signups_7d, totals.signups_prev_7d)}, ${count(totals.signups_30d)} last 30`,
|
|
43
|
+
`people ${count(totals.people)} total, ${count(totals.paying)} paying`,
|
|
44
|
+
`revenue $${count(Math.round(Number(totals.mrr_usd || 0)))} a month`,
|
|
45
|
+
];
|
|
46
|
+
const cli = data.cli_installs;
|
|
47
|
+
if (cli) lines.push(`installs ${count(cli.last_7d)} last 7 days${change(cli.last_7d, cli.prev_7d)}`);
|
|
48
|
+
|
|
49
|
+
const countries = Array.isArray(data.countries) ? data.countries.slice(0, 5) : [];
|
|
50
|
+
lines.push(countries.length
|
|
51
|
+
? `places ${countries.map((row) => `${row.country} ${count(row.people)}`).join(', ')}`
|
|
52
|
+
: 'places none recorded yet');
|
|
53
|
+
|
|
54
|
+
const marks = Array.isArray(data.marks) ? data.marks.slice(0, 5) : [];
|
|
55
|
+
if (marks.length) {
|
|
56
|
+
lines.push(`marked ${marks.map((mark) => (mark.people ? `${mark.label} ${count(mark.people)}` : mark.label)).join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
const notes = Array.isArray(data.notes) ? data.notes.slice(0, 3) : [];
|
|
59
|
+
notes.forEach((note, index) => {
|
|
60
|
+
lines.push(`${index === 0 ? 'notes ' : ' '} ${shortDay(note.day)} ${oneLine(note.text)}`);
|
|
61
|
+
});
|
|
62
|
+
lines.push(`page ${PAGE_URL}`);
|
|
63
|
+
return lines.join('\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function openPage(url) {
|
|
67
|
+
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
68
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
69
|
+
try {
|
|
70
|
+
const child = spawn(opener, args, { stdio: 'ignore', detached: true });
|
|
71
|
+
child.on('error', () => {});
|
|
72
|
+
child.unref();
|
|
73
|
+
} catch {
|
|
74
|
+
// The address is printed either way.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Pull `note "text" [--day YYYY-MM-DD]` apart; null text means the note is missing. */
|
|
79
|
+
function readNote(args) {
|
|
80
|
+
const rest = args.slice(1).filter((arg) => arg !== '--json');
|
|
81
|
+
const at = rest.indexOf('--day');
|
|
82
|
+
let day;
|
|
83
|
+
if (at !== -1) {
|
|
84
|
+
day = rest[at + 1];
|
|
85
|
+
rest.splice(at, 2);
|
|
86
|
+
}
|
|
87
|
+
return { text: rest.join(' ').trim(), day };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function failure(result) {
|
|
91
|
+
const status = Number(result && result.status);
|
|
92
|
+
if (status === 401 || status === 403) return NOT_FOUNDER;
|
|
93
|
+
return oneLine(result && result.error);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function growthCommand(args = [], deps = {}) {
|
|
97
|
+
if (wantsHelp(args)) {
|
|
98
|
+
showGrowthHelp();
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
const json = wantsJson(args);
|
|
102
|
+
const io = { log: deps.log || console.log, err: deps.err || console.error };
|
|
103
|
+
const fail = (error, extra = {}) => {
|
|
104
|
+
printResult({ json, ok: false, error, payload: { ok: false, error, ...extra } }, io);
|
|
105
|
+
return 1;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (args.includes('--open')) {
|
|
109
|
+
(deps.openPage || openPage)(PAGE_URL);
|
|
110
|
+
printResult({ json, ok: true, text: PAGE_URL, payload: { ok: true, url: PAGE_URL } }, io);
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const token = tokenFrom((deps.loadCredentials || loadCredentials)());
|
|
115
|
+
if (!token) return fail(NOT_LOGGED_IN);
|
|
116
|
+
const request = deps.apiRequestJson || apiRequestJson;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
if (args[0] === 'note') {
|
|
120
|
+
const { text, day } = readNote(args);
|
|
121
|
+
if (!text) return fail('say what you did: atris growth note "posted the launch video"');
|
|
122
|
+
if (text.length > 280) return fail('keep a note under 280 characters');
|
|
123
|
+
if (day !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(String(day))) return fail('write the day as YYYY-MM-DD');
|
|
124
|
+
const result = await request('/admin/growth/notes', {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
token,
|
|
127
|
+
retries: 0, // a retry could save the same note twice
|
|
128
|
+
body: day ? { text, day } : { text },
|
|
129
|
+
});
|
|
130
|
+
if (!result || !result.ok) return fail(failure(result), { status: result && result.status });
|
|
131
|
+
const saved = result.data || {};
|
|
132
|
+
printResult({
|
|
133
|
+
json,
|
|
134
|
+
ok: true,
|
|
135
|
+
text: `noted for ${shortDay(saved.day || day)}: ${oneLine(saved.text || text)}`,
|
|
136
|
+
payload: { ok: true, note: saved },
|
|
137
|
+
}, io);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const result = await request('/admin/growth/overview?days=28', { method: 'GET', token });
|
|
142
|
+
if (!result || !result.ok) return fail(failure(result), { status: result && result.status });
|
|
143
|
+
const data = result.data || {};
|
|
144
|
+
printResult({ json, ok: true, text: growthText(data), payload: { ok: true, ...data } }, io);
|
|
145
|
+
return 0;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return fail(oneLine(error && error.message));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { growthCommand, growthText, showGrowthHelp };
|
package/commands/memory-live.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// atris memory show, search, read, ls, remember, write, rm, usage, log, lesson
|
|
3
|
+
// atris memory show, search, ask, read, ls, remember, write, rm, usage, log, lesson
|
|
4
4
|
// and agents: the person's Atris memory from any terminal, so any agent can
|
|
5
5
|
// read it, search it and save to it. One request per command. The login and
|
|
6
6
|
// network modules load only when a command needs the server.
|
|
@@ -13,6 +13,7 @@ const { NOT_LOGGED_IN, oneLine, tokenFrom } = require('../lib/developer-api');
|
|
|
13
13
|
const MAX_WRITE_BYTES = 1000000;
|
|
14
14
|
const STDIN_WAIT_MS = 5000;
|
|
15
15
|
const REMEMBER_MAX_CHARS = 1000;
|
|
16
|
+
const ASK_MAX_CHARS = 500;
|
|
16
17
|
const REMEMBER_FILES = ['journal', 'now', 'learning'];
|
|
17
18
|
const LEARNING_SECTIONS = ['want', 'understand', 'shaky'];
|
|
18
19
|
|
|
@@ -138,6 +139,20 @@ const COMMANDS = {
|
|
|
138
139
|
},
|
|
139
140
|
},
|
|
140
141
|
|
|
142
|
+
ask: {
|
|
143
|
+
check(ctx) {
|
|
144
|
+
const question = ctx.words.join(' ').replace(/\s+/g, ' ').trim();
|
|
145
|
+
if (!question) throw new UsageError('ask a question, like atris memory ask "where does Sam work?"');
|
|
146
|
+
if (question.length > ASK_MAX_CHARS) throw new UsageError('keep the question under 500 characters');
|
|
147
|
+
return { question, folder: ctx.flags['--in'] || '' };
|
|
148
|
+
},
|
|
149
|
+
async run(ctx, { question, folder }) {
|
|
150
|
+
// The server reads for up to 75 seconds; asking twice would count twice.
|
|
151
|
+
const data = await ctx.call(`/memory/ask?q=${enc(question)}${folder ? `&folder=${enc(folder)}` : ''}`, { timeoutMs: 90000, retries: 0 });
|
|
152
|
+
return { data, lines: live.askLines(data) };
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
|
|
141
156
|
read: {
|
|
142
157
|
check(ctx) {
|
|
143
158
|
const rel = onePath(ctx, 'file to read');
|
package/commands/push.js
CHANGED
|
@@ -9,6 +9,17 @@ const { normalizeWikiOnlyPrefix } = require('../lib/wiki');
|
|
|
9
9
|
const { emitSyncEvent, startTimer } = require('../lib/sync-telemetry');
|
|
10
10
|
const { assertSafeWorkspaceRoot } = require('../lib/workspace-safety');
|
|
11
11
|
|
|
12
|
+
// The server's own words for a refused request. It used to be dropped, so a
|
|
13
|
+
// refusal for any reason read as "you can only push to your team/ folder".
|
|
14
|
+
function refusalText(result) {
|
|
15
|
+
if (!result) return '';
|
|
16
|
+
const raw = result.errorMessage || result.error || (result.data && result.data.detail) || '';
|
|
17
|
+
if (!raw) return '';
|
|
18
|
+
if (typeof raw === 'string') return raw === 'Request failed' ? '' : raw;
|
|
19
|
+
if (typeof raw === 'object' && typeof raw.message === 'string') return raw.message;
|
|
20
|
+
try { return JSON.stringify(raw); } catch { return String(raw); }
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
function resolvePushSourceDir({ slug, argv = process.argv, cwd = process.cwd() }) {
|
|
13
24
|
const fromIdx = argv.indexOf('--from');
|
|
14
25
|
if (fromIdx !== -1 && argv[fromIdx + 1]) {
|
|
@@ -858,13 +869,15 @@ async function pushAtris() {
|
|
|
858
869
|
pushed = landedPaths.size;
|
|
859
870
|
break;
|
|
860
871
|
} else {
|
|
861
|
-
console.error(`\n Push failed: ${retry
|
|
872
|
+
console.error(`\n Push failed: ${refusalText(retry) || retry.status}`);
|
|
873
|
+
if (refusalText(result)) console.error(` The first refusal said: ${refusalText(result)}`);
|
|
862
874
|
await emit('access_denied', { error_detail: `403 retry failed: ${retry.status}` });
|
|
863
875
|
process.exit(1);
|
|
864
876
|
}
|
|
865
877
|
} else {
|
|
866
|
-
|
|
867
|
-
|
|
878
|
+
const reason = refusalText(result);
|
|
879
|
+
console.error(`\n Access denied: ${reason || 'the server refused this push and gave no reason.'}`);
|
|
880
|
+
await emit('access_denied', reason ? { error_detail: reason } : undefined);
|
|
868
881
|
process.exit(1);
|
|
869
882
|
}
|
|
870
883
|
} else if (result.status === 409) {
|
|
@@ -1052,6 +1065,7 @@ async function pushAtris() {
|
|
|
1052
1065
|
}
|
|
1053
1066
|
|
|
1054
1067
|
module.exports = {
|
|
1068
|
+
refusalText,
|
|
1055
1069
|
pushAtris,
|
|
1056
1070
|
buildPushChangePlan,
|
|
1057
1071
|
buildPushUploadBatches,
|
|
@@ -715,12 +715,12 @@ function runVerifyCommandCached(verify, workspaceRoot, cache = null) {
|
|
|
715
715
|
// root: one scan covers every task in a sweep.
|
|
716
716
|
const repoHygieneCache = new Map();
|
|
717
717
|
|
|
718
|
-
function repoHygieneGate(workspaceRoot) {
|
|
718
|
+
function repoHygieneGate(workspaceRoot, skillScope = null) {
|
|
719
719
|
const root = path.resolve(workspaceRoot || process.cwd());
|
|
720
720
|
// Skill receipts are mutable workspace state, so recompute this gate before
|
|
721
721
|
// consulting the repo-wide dead-code cache. A new receipt can unblock a
|
|
722
722
|
// landing, while a later SKILL.md edit must make an older receipt stale.
|
|
723
|
-
const skillEval = skillEvalGate(root);
|
|
723
|
+
const skillEval = skillEvalGate(root, skillScope || {});
|
|
724
724
|
if (!skillEval.ok) return skillEval;
|
|
725
725
|
if (repoHygieneCache.has(root)) return repoHygieneCache.get(root);
|
|
726
726
|
let result = { ok: true, skipped: true };
|
|
@@ -755,8 +755,17 @@ function repoHygieneGate(workspaceRoot) {
|
|
|
755
755
|
|
|
756
756
|
// Shared by both landing lanes so the check cannot ship in one dispatch branch
|
|
757
757
|
// and silently skip the sibling (lesson: parallel-paths-drift).
|
|
758
|
-
|
|
759
|
-
|
|
758
|
+
// A job answers for the skills it changed. When its exact file list is on
|
|
759
|
+
// record (the same list the taste check just read), only those files count,
|
|
760
|
+
// read where the job's branch is checked out. With no list on record the
|
|
761
|
+
// workspace-wide check stays, so an unevaluated skill is never waved through.
|
|
762
|
+
function jobSkillScope(task, candidateGate) {
|
|
763
|
+
if (!candidateGate || candidateGate.advisory_only || !Array.isArray(candidateGate.paths)) return null;
|
|
764
|
+
return { changeRoot: candidateWorkspaceRoot(task), paths: candidateGate.paths };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function hygieneBlockResult(task, ref, candidateGate = null) {
|
|
768
|
+
const hygiene = repoHygieneGate(task.workspace_root || process.cwd(), jobSkillScope(task, candidateGate));
|
|
760
769
|
if (hygiene.ok) return null;
|
|
761
770
|
return {
|
|
762
771
|
eligible: false,
|
|
@@ -1360,7 +1369,7 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1360
1369
|
if (executeVerify) {
|
|
1361
1370
|
candidateGate = candidatePolicyGate(task, { verifyCache, executeDetectors: true });
|
|
1362
1371
|
if (!candidateGate.ok) return { ...candidateGate, ref };
|
|
1363
|
-
const hygieneBlock = hygieneBlockResult(task, ref);
|
|
1372
|
+
const hygieneBlock = hygieneBlockResult(task, ref, candidateGate.gate);
|
|
1364
1373
|
if (hygieneBlock) return hygieneBlock;
|
|
1365
1374
|
}
|
|
1366
1375
|
return {
|
|
@@ -1444,7 +1453,7 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1444
1453
|
if (executeVerify) {
|
|
1445
1454
|
candidateGate = candidatePolicyGate(task, { verifyCache, executeDetectors: true });
|
|
1446
1455
|
if (!candidateGate.ok) return { ...candidateGate, ref };
|
|
1447
|
-
const hygieneBlock = hygieneBlockResult(task, ref);
|
|
1456
|
+
const hygieneBlock = hygieneBlockResult(task, ref, candidateGate.gate);
|
|
1448
1457
|
if (hygieneBlock) return hygieneBlock;
|
|
1449
1458
|
}
|
|
1450
1459
|
|
package/lib/known-commands.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const knownCommands = ['init', 'guide', 'doctor', 'doc-health', 'install', 'workspace', 'log', 'logs', 'later', 'idea', 'done', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', '99', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'revisions', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
|
|
4
4
|
'activate', '_activate', 'agent', 'team', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
5
5
|
'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'taste', 'teach', 'plugin', 'experiments', 'bench', 'router', 'tree', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube', 'x-search',
|
|
6
|
-
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'campaign', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'sentinel', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'scoreboard', 'scorecard', 'rating', 'sync-checkout', 'zen',
|
|
6
|
+
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'campaign', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'sentinel', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'scoreboard', 'growth', 'scorecard', 'rating', 'sync-checkout', 'zen',
|
|
7
7
|
'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'memory', 'balance', 'usage', 'spend', 'api-key', 'agent-client', 'topup', 'buy', 'transactions', 'hire', 'hires', 'mail', 'design', 'mcp', 'gmail', 'google', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
8
8
|
'fork', 'browse', 'publish', 'pack', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'fleet-report', 'loops', 'self-improve', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines', 'link', 'playbook', 'feed', 'brief', 'social', 'host', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join', 'rsi', 'pay', 'trust'];
|
|
9
9
|
|
package/lib/memory-live.js
CHANGED
|
@@ -9,7 +9,7 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
|
|
11
11
|
const LIVE_COMMANDS = new Set([
|
|
12
|
-
'show', 'search', 'read', 'ls', 'remember', 'write', 'rm', 'usage', 'log', 'lesson', 'agents', 'setup',
|
|
12
|
+
'show', 'search', 'ask', 'read', 'ls', 'remember', 'write', 'rm', 'usage', 'log', 'lesson', 'agents', 'setup',
|
|
13
13
|
]);
|
|
14
14
|
|
|
15
15
|
function isLiveCommand(word) {
|
|
@@ -19,10 +19,11 @@ function isLiveCommand(word) {
|
|
|
19
19
|
// One line per command, shown together by `atris memory` and one at a time by
|
|
20
20
|
// `atris memory <command> --help`.
|
|
21
21
|
const COLUMN = 38;
|
|
22
|
-
const row = (form, ...what) => [` ${form}`.padEnd(COLUMN) + what[0], ...what.slice(1).map((more) => ' '.repeat(COLUMN) + more)].join('\n');
|
|
22
|
+
const row = (form, ...what) => [` ${form} `.padEnd(COLUMN) + what[0], ...what.slice(1).map((more) => ' '.repeat(COLUMN) + more)].join('\n');
|
|
23
23
|
const USAGE_LINES = {
|
|
24
24
|
show: row('show', 'who they are and what matters now; read it before helping'),
|
|
25
25
|
search: row('search "rent|lease" [--in folder]', 'lines with any of the words, split by |'),
|
|
26
|
+
ask: row('ask "a question" [--in folder]', 'answered from their memory, with the lines it rests on'),
|
|
26
27
|
read: row('read <path> [--from N] [--lines N]', 'numbered lines of one file; --raw prints it as stored'),
|
|
27
28
|
ls: row('ls [folder]', 'what is in a folder; MAP.md says what is where'),
|
|
28
29
|
remember: row('remember "one short line"', 'a lasting fact or preference about the person, when they',
|
|
@@ -43,6 +44,7 @@ const LIVE_HELP = [
|
|
|
43
44
|
'read',
|
|
44
45
|
USAGE_LINES.show,
|
|
45
46
|
USAGE_LINES.search,
|
|
47
|
+
USAGE_LINES.ask,
|
|
46
48
|
USAGE_LINES.read,
|
|
47
49
|
USAGE_LINES.ls,
|
|
48
50
|
'',
|
|
@@ -62,6 +64,7 @@ const LIVE_HELP = [
|
|
|
62
64
|
// Flags that take a value, per command. --json, --raw and --help take none.
|
|
63
65
|
const VALUE_FLAGS = {
|
|
64
66
|
search: ['--in'],
|
|
67
|
+
ask: ['--in'],
|
|
65
68
|
read: ['--from', '--lines'],
|
|
66
69
|
ls: [],
|
|
67
70
|
show: [],
|
|
@@ -205,6 +208,26 @@ function searchLines(query, data) {
|
|
|
205
208
|
return lines;
|
|
206
209
|
}
|
|
207
210
|
|
|
211
|
+
// The answer first, then each line it rests on, then how sure it is. With no
|
|
212
|
+
// answer (the model was busy) the search hits it started from are shown.
|
|
213
|
+
function askLines(data) {
|
|
214
|
+
const sources = (data && data.sources) || [];
|
|
215
|
+
const cite = sources.map((s) => ` ${s.path}:${s.n}: ${s.text}`);
|
|
216
|
+
if (data && data.answer && data.found) {
|
|
217
|
+
return [String(data.answer), ...(cite.length ? ['', 'From:', ...cite] : [])];
|
|
218
|
+
}
|
|
219
|
+
if (data && data.answer) {
|
|
220
|
+
const lines = [String(data.answer)];
|
|
221
|
+
if (cite.length) lines.push('', 'Closest lines:', ...cite);
|
|
222
|
+
lines.push('', 'Not found for certain. Search it yourself: atris memory search "word|other word"');
|
|
223
|
+
return lines;
|
|
224
|
+
}
|
|
225
|
+
const hits = (data && data.search && data.search.hits) || [];
|
|
226
|
+
const lines = [String((data && data.note) || 'No answer right now.')];
|
|
227
|
+
if (hits.length) lines.push('', 'Lines that may hold it:', ...hits.slice(0, 8).map((h) => ` ${h.path}:${h.n}: ${h.text}`));
|
|
228
|
+
return lines;
|
|
229
|
+
}
|
|
230
|
+
|
|
208
231
|
function readLines(data) {
|
|
209
232
|
const page = (data && data.lines) || [];
|
|
210
233
|
const total = Number(data && data.total) || 0;
|
|
@@ -244,7 +267,7 @@ function rulesBlock(agent) {
|
|
|
244
267
|
'',
|
|
245
268
|
'This person keeps one memory in Atris that every app and agent shares. Use it.',
|
|
246
269
|
'- Start each task with `atris memory show`: who they are and what matters now.',
|
|
247
|
-
'- Before you ask them anything they may have said before, search: `atris memory search "word|other word"`.',
|
|
270
|
+
'- Before you ask them anything they may have said before, search: `atris memory search "word|other word"`, or ask in plain words: `atris memory ask "..."`.',
|
|
248
271
|
'- Open what you find with `atris memory read <path>`. `atris memory ls` shows the folder.',
|
|
249
272
|
'- When they say remember, note, save or don\'t forget, save one short line: `atris memory remember "..."`.',
|
|
250
273
|
'- Save only lasting facts about them, never logs or your own work. Never rewrite their about.md.',
|
|
@@ -338,6 +361,7 @@ module.exports = {
|
|
|
338
361
|
END,
|
|
339
362
|
LIVE_HELP,
|
|
340
363
|
USAGE_LINES,
|
|
364
|
+
askLines,
|
|
341
365
|
isLiveCommand,
|
|
342
366
|
lsLines,
|
|
343
367
|
mcpLines,
|
package/lib/skill-eval-gate.js
CHANGED
|
@@ -187,12 +187,47 @@ function validateSkillEvalReceipt(receipt, skill) {
|
|
|
187
187
|
return { ok: true, receipt_at_ms: receiptAtMs };
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
// The learner writes these notes itself after verified runs (see
|
|
191
|
+
// writeAutoLearnedSkill in lib/mission-core.js). No job authors them, so they
|
|
192
|
+
// never count against a job whose own file list is unknown.
|
|
193
|
+
const LEARNER_NOTE_PATTERN = /(^|\/)skills\/auto-learned\/SKILL\.md$/;
|
|
194
|
+
|
|
195
|
+
function normalizedScopePaths(paths) {
|
|
196
|
+
if (!Array.isArray(paths)) return null;
|
|
197
|
+
return paths
|
|
198
|
+
.map((entry) => String(entry || '').trim().replace(/\\/g, '/').replace(/\/+$/, ''))
|
|
199
|
+
.filter(Boolean);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// A job answers for the skills it changed, not for every skill file that
|
|
203
|
+
// happens to sit changed in the workspace. Eight learner notes nobody had
|
|
204
|
+
// evaluated held seven finished jobs for five days (2026-11-04 to 11-09)
|
|
205
|
+
// while none of those jobs touched a skill.
|
|
206
|
+
// scope.paths the job's exact file list, when it is on record. Only
|
|
207
|
+
// skills inside it count, learner notes included.
|
|
208
|
+
// scope.changeRoot where the job's branch is checked out, when that is not
|
|
209
|
+
// the workspace itself.
|
|
210
|
+
// With no file list every changed skill still counts, except learner notes.
|
|
211
|
+
function skillsInScope(root, scope = {}) {
|
|
212
|
+
const changeRoot = path.resolve(scope.changeRoot || root);
|
|
213
|
+
const jobPaths = normalizedScopePaths(scope.paths);
|
|
214
|
+
return changedSkillFiles(changeRoot).filter((skill) => {
|
|
215
|
+
if (!jobPaths) return !LEARNER_NOTE_PATTERN.test(skill.path);
|
|
216
|
+
return jobPaths.some((entry) => skill.path === entry || skill.path.startsWith(`${entry}/`));
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function skillEvalGate(workspaceRoot = process.cwd(), scope = {}) {
|
|
191
221
|
const root = path.resolve(workspaceRoot);
|
|
192
|
-
const
|
|
222
|
+
const changeRoot = path.resolve((scope && scope.changeRoot) || root);
|
|
223
|
+
const skills = skillsInScope(root, scope || {});
|
|
193
224
|
if (!skills.length) return { ok: true, skipped: true, skills: [] };
|
|
194
225
|
|
|
195
|
-
|
|
226
|
+
// Evaluations are saved in the workspace; one run from the branch's own
|
|
227
|
+
// checkout is saved there instead. Either proves the skill was judged.
|
|
228
|
+
const receipts = changeRoot === root
|
|
229
|
+
? readSkillEvalReceipts(root)
|
|
230
|
+
: [...readSkillEvalReceipts(root), ...readSkillEvalReceipts(changeRoot)];
|
|
196
231
|
const missing = [];
|
|
197
232
|
const accepted = [];
|
|
198
233
|
for (const skill of skills) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atris",
|
|
3
|
-
"version": "3.82.
|
|
3
|
+
"version": "3.82.15",
|
|
4
4
|
"description": "you say what you want in plain words. atris builds it, checks it, and shows you proof. project memory your coding agents share across sessions.",
|
|
5
5
|
"mcpName": "io.github.atrislabs/atris",
|
|
6
6
|
"main": "bin/atris.js",
|