atris 3.58.6 → 3.58.7

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/lib/apply-gate.js CHANGED
@@ -26,6 +26,17 @@ function isFilledApply(fields) {
26
26
  return !empty(fields.change) && !empty(fields.receipt);
27
27
  }
28
28
 
29
+ function isLearnerKeepApply(fields) {
30
+ const change = String((fields && fields.change) || '');
31
+ const receipt = String((fields && fields.receipt) || '');
32
+ return /^apply\s+atris\/experiments\//i.test(change)
33
+ || /keep only if measure\.py moves 0/.test(receipt);
34
+ }
35
+
36
+ function isFilledHumanApply(fields) {
37
+ return isFilledApply(fields) && !isLearnerKeepApply(fields);
38
+ }
39
+
29
40
  function applySlug(text) {
30
41
  const slug = String(text || '')
31
42
  .toLowerCase()
@@ -46,7 +57,7 @@ function readApplyReceipt({ cwd, rel } = {}) {
46
57
  return { rel, text: fs.readFileSync(abs, 'utf8') };
47
58
  }
48
59
 
49
- function writeApplyStub({ cwd, source, rel, now, change, receipt, journalLine } = {}) {
60
+ function writeApplyStub({ cwd, source, rel, now, change, receipt, journalLine, force } = {}) {
50
61
  try {
51
62
  if (!rel || !cwd) return null;
52
63
  const wikiDir = path.join(cwd, 'atris', 'wiki');
@@ -56,7 +67,7 @@ function writeApplyStub({ cwd, source, rel, now, change, receipt, journalLine }
56
67
  fs.mkdirSync(path.dirname(abs), { recursive: true });
57
68
  const changeText = change || 'fill this';
58
69
  const receiptText = receipt || 'fill this';
59
- let shouldWrite = !fs.existsSync(abs);
70
+ let shouldWrite = force === true || !fs.existsSync(abs);
60
71
  if (!shouldWrite && change && receipt) {
61
72
  shouldWrite = !isFilledApply(parseApplyFields(fs.readFileSync(abs, 'utf8')));
62
73
  }
@@ -86,11 +97,16 @@ function writeApplyStub({ cwd, source, rel, now, change, receipt, journalLine }
86
97
 
87
98
  function ensureApply({
88
99
  cwd, source, rel, now, output, incompleteMessage, required = true, change, receipt, journalLine,
100
+ human = false,
89
101
  } = {}) {
90
102
  const print = typeof output === 'function' ? output : (line = '') => console.error(line);
91
103
  const existing = readApplyReceipt({ cwd, rel });
92
- if (existing && isFilledApply(parseApplyFields(existing.text))) return 0;
93
- if (!(required && existing)) writeApplyStub({ cwd, source, rel, now, change, receipt, journalLine });
104
+ const fields = existing ? parseApplyFields(existing.text) : null;
105
+ const keepSidecar = Boolean(human && fields && isLearnerKeepApply(fields));
106
+ if (existing && isFilledApply(fields) && !keepSidecar) return 0;
107
+ if (!(required && existing) || keepSidecar) {
108
+ writeApplyStub({ cwd, source, rel, now, change, receipt, journalLine, force: keepSidecar });
109
+ }
94
110
  print(incompleteMessage);
95
111
  return required ? 2 : 0;
96
112
  }
@@ -112,4 +128,6 @@ module.exports = {
112
128
  ensureApply,
113
129
  ephemeralApplyMessage,
114
130
  hintEphemeralApply,
131
+ isFilledHumanApply,
132
+ isLearnerKeepApply,
115
133
  };
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ // Two-level daily logging. Member logs (atris/team/<member>/logs/<date>.md)
4
+ // keep the detailed workstream: claims, notes, result receipts, completions.
5
+ // The master journal (atris/logs/<YYYY>/<date>.md) stays curated and receives
6
+ // only consequential entries: terminal task events and explicitly major
7
+ // updates such as decisions, handoffs, and shipped work.
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ function todayLogName(now = new Date()) {
13
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}.md`;
14
+ }
15
+
16
+ function logStamp(now = new Date()) {
17
+ return now.toTimeString().slice(0, 5);
18
+ }
19
+
20
+ function compactLogText(value, max = 240) {
21
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
22
+ if (!text) return '';
23
+ return text.length > max ? `${text.slice(0, Math.max(0, max - 3)).trim()}...` : text;
24
+ }
25
+
26
+ function logFieldRows(fields) {
27
+ return Object.entries(fields)
28
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
29
+ .map(([key, value]) => `- ${key}: ${compactLogText(value, 500)}`);
30
+ }
31
+
32
+ function appendDailyEntry(logPath, title, fields) {
33
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
34
+ fs.appendFileSync(logPath, [
35
+ `## ${logStamp()} · ${title}`,
36
+ ...logFieldRows(fields),
37
+ '',
38
+ ].join('\n'), 'utf8');
39
+ return logPath;
40
+ }
41
+
42
+ function memberSlug(root, member) {
43
+ const slug = String(member || '').trim();
44
+ if (!/^[A-Za-z0-9._-]+$/.test(slug)) return null;
45
+ if (!fs.existsSync(path.join(root, 'atris', 'team', slug, 'MEMBER.md'))) return null;
46
+ return slug;
47
+ }
48
+
49
+ function appendMemberDailyEntry(root, member, title, fields) {
50
+ const slug = memberSlug(root, member);
51
+ if (!slug || !fs.existsSync(path.join(root, 'atris'))) return null;
52
+ const logPath = path.join(root, 'atris', 'team', slug, 'logs', todayLogName());
53
+ appendDailyEntry(logPath, title, { ...fields, member: slug });
54
+ return logPath;
55
+ }
56
+
57
+ function appendMasterDailyEntry(root, title, fields) {
58
+ if (!fs.existsSync(path.join(root, 'atris'))) return null;
59
+ const logName = todayLogName();
60
+ const logPath = path.join(root, 'atris', 'logs', logName.slice(0, 4), logName);
61
+ appendDailyEntry(logPath, title, fields);
62
+ return logPath;
63
+ }
64
+
65
+ // Notes a person deliberately escalates to the day record. Anything else stays
66
+ // in the member's own log so the master journal does not become a transcript.
67
+ const MASTER_NOTE = /^(decision|decided|milestone|handoff|shipped|launched|landed|blocked)\b\s*[:\-]/i;
68
+
69
+ function noteTitle(content) {
70
+ const key = String(content || '').trim().match(/^([a-z]+)\s*[:\-]/i);
71
+ const word = key ? key[1].toLowerCase() : '';
72
+ if (word === 'decision' || word === 'decided') return 'Task decision';
73
+ if (word === 'handoff') return 'Task handoff';
74
+ if (word === 'shipped' || word === 'launched' || word === 'landed') return 'Task shipped';
75
+ if (word === 'blocked') return 'Task blocked';
76
+ if (word === 'milestone') return 'Task milestone';
77
+ return 'Task update';
78
+ }
79
+
80
+ module.exports = {
81
+ todayLogName,
82
+ compactLogText,
83
+ memberSlug,
84
+ appendMemberDailyEntry,
85
+ appendMasterDailyEntry,
86
+ MASTER_NOTE,
87
+ noteTitle,
88
+ };
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ // Shared client for the Atris design API. Used by `atris design` and the
4
+ // `atris mcp` stdio server so both resolve keys and bill the same way.
5
+ //
6
+ // Key resolution order:
7
+ // 1. ATRIS_API_KEY env var
8
+ // 2. login credentials token (atris login)
9
+ // 3. ~/.atris/design-api-key file (raw key text, saved by hand)
10
+ //
11
+ // Endpoints (see https://api.atris.ai/llms.txt):
12
+ // POST /design/extractions {"url"} -> job; poll GET /design/extractions/{id}
13
+ // POST /design/adherence {source, reference} -> job; poll GET /design/adherence/{id}
14
+ // POST /design/search {"query", "limit"} -> sync result
15
+
16
+ const fs = require('fs');
17
+ const os = require('os');
18
+ const path = require('path');
19
+ const { getApiBaseUrl, httpRequest } = require('../utils/api');
20
+ const { loadCredentials } = require('../utils/auth');
21
+
22
+ const DESIGN_KEY_FILE = path.join(os.homedir(), '.atris', 'design-api-key');
23
+
24
+ const POLL_INTERVAL_MS = 3000;
25
+ const POLL_TIMEOUT_MS = 3 * 60 * 1000;
26
+
27
+ function readKeyFile(file = DESIGN_KEY_FILE) {
28
+ try {
29
+ const text = fs.readFileSync(file, 'utf8').trim();
30
+ return text || null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ // Returns the bearer token for design calls, or null when nothing is set up.
37
+ function resolveDesignKey(env = process.env, deps = {}) {
38
+ const fromEnv = env.ATRIS_API_KEY && env.ATRIS_API_KEY.trim();
39
+ if (fromEnv) return fromEnv;
40
+ const load = deps.loadCredentials || loadCredentials;
41
+ const creds = load();
42
+ const token = creds && typeof creds.token === 'string' ? creds.token.trim() : '';
43
+ if (token) return token;
44
+ return (deps.readKeyFile || readKeyFile)();
45
+ }
46
+
47
+ // One JSON call against the design API. Returns { ok, status, data, error }.
48
+ async function designRequest(pathname, options = {}) {
49
+ const key = options.key;
50
+ const url = `${getApiBaseUrl()}${pathname.startsWith('/') ? pathname : `/${pathname}`}`;
51
+ const headers = {
52
+ 'Authorization': `Bearer ${key}`,
53
+ 'Accept': 'application/json',
54
+ ...(options.headers || {}),
55
+ };
56
+ let body;
57
+ if (options.body !== undefined && options.body !== null) {
58
+ body = JSON.stringify(options.body);
59
+ headers['Content-Type'] = 'application/json';
60
+ }
61
+ const res = await httpRequest(url, {
62
+ method: options.method || 'GET',
63
+ headers,
64
+ body,
65
+ timeoutMs: options.timeoutMs != null ? options.timeoutMs : 30000,
66
+ });
67
+ const text = res.body.toString('utf8');
68
+ let data = null;
69
+ try { data = text ? JSON.parse(text) : null; } catch { data = null; }
70
+ const ok = res.status >= 200 && res.status < 300;
71
+ const error = !ok
72
+ ? (data && typeof data === 'object' && (data.detail || data.error || data.message)) || text.slice(0, 200) || `http ${res.status}`
73
+ : undefined;
74
+ return { ok, status: res.status, data, error };
75
+ }
76
+
77
+ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
78
+
79
+ // Poll a design job until it finishes. `fetchJob` returns the parsed job
80
+ // object; `onTick` fires once per wait so callers can draw a spinner.
81
+ // Resolves { ok, job, timedOut } and never throws on a bad status.
82
+ async function pollDesignJob(fetchJob, options = {}) {
83
+ const intervalMs = options.intervalMs != null ? options.intervalMs : POLL_INTERVAL_MS;
84
+ const timeoutMs = options.timeoutMs != null ? options.timeoutMs : POLL_TIMEOUT_MS;
85
+ const wait = options.sleep || sleep;
86
+ const onTick = typeof options.onTick === 'function' ? options.onTick : null;
87
+ const start = Date.now();
88
+ let job = await fetchJob();
89
+ while (job && job.status && !isTerminalStatus(job.status)) {
90
+ if (Date.now() - start >= timeoutMs) return { ok: false, job, timedOut: true };
91
+ if (onTick) onTick(Date.now() - start, job);
92
+ await wait(intervalMs);
93
+ job = await fetchJob();
94
+ }
95
+ const ok = Boolean(job) && job.status === 'completed' && !job.error;
96
+ return { ok, job, timedOut: false };
97
+ }
98
+
99
+ function isTerminalStatus(status) {
100
+ const s = String(status || '').toLowerCase();
101
+ return s === 'completed' || s === 'failed' || s === 'error' || s === 'succeeded';
102
+ }
103
+
104
+ // Pull the billing block off any design response. Top-level credits_charged on
105
+ // jobs is the job's price; atris.credits_charged is what this call billed.
106
+ function billingOf(data = {}) {
107
+ const raw = data && typeof data === 'object' ? data : {};
108
+ const atris = raw.atris && typeof raw.atris === 'object' ? raw.atris : {};
109
+ const charged = Number(atris.credits_charged != null ? atris.credits_charged : raw.credits_charged);
110
+ const balance = Number(atris.balance_remaining_usd);
111
+ return {
112
+ credits: Number.isFinite(charged) ? charged : null,
113
+ balanceUsd: Number.isFinite(balance) ? balance : null,
114
+ };
115
+ }
116
+
117
+ function creditLine(data) {
118
+ const bill = billingOf(data);
119
+ const charged = bill.credits == null ? '?' : `${bill.credits} credit${bill.credits === 1 ? '' : 's'}`;
120
+ const left = bill.balanceUsd == null ? '' : ` $${bill.balanceUsd.toFixed(2)} left.`;
121
+ return `${charged} charged.${left}`;
122
+ }
123
+
124
+ module.exports = {
125
+ resolveDesignKey,
126
+ designRequest,
127
+ pollDesignJob,
128
+ billingOf,
129
+ creditLine,
130
+ };
package/lib/engine-ask.js CHANGED
@@ -283,7 +283,7 @@ function buildReadOnlyEngineInvocation(engineName, prompt, modelName = '', {
283
283
  if (engine === 'opencode') {
284
284
  // `run` with a message is headless print mode; the built-in plan agent
285
285
  // keeps the ask read-only (verified live 2026-08-21, ~7s per lookup).
286
- return { engine, bin: profile.bin, args: ['--agent', 'plan', ...(model ? ['-m', model] : []), request] };
286
+ return { engine, bin: profile.bin, args: ['run', '--agent', 'plan', ...(model ? ['-m', model] : []), request] };
287
287
  }
288
288
  throw new Error(`engine ask has no read-only command for ${engine}`);
289
289
  }
@@ -168,12 +168,7 @@ function titlesFromTodoSection(root, section) {
168
168
  if (!match) return null;
169
169
  const body = String(match[1] || '').trim();
170
170
  if (!body || /\(empty|\(see /i.test(body)) return [];
171
- return body
172
- .split('\n')
173
- .map((line) => line.trim())
174
- .filter((line) => /^-\s+/.test(line) && !/\(empty/i.test(line))
175
- .map((line) => line.replace(/^-+\s*/, '').trim())
176
- .filter(Boolean);
171
+ return require('./todo-fallback').parseSection(text, section).map(task => task.title);
177
172
  } catch {
178
173
  return null;
179
174
  }
@@ -1,11 +1,11 @@
1
1
  'use strict';
2
2
 
3
- const knownCommands = ['init', 'guide', 'doctor', 'install', 'workspace', 'log', 'logs', 'later', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'revisions', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
3
+ const knownCommands = ['init', 'guide', 'doctor', 'doc-health', 'install', 'workspace', 'log', 'logs', 'later', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', '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
6
  'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'sync-checkout',
7
- 'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'api-key', 'topup', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
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', 'playbook', 'feed', 'brief', 'social', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join'];
7
+ 'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'api-key', 'topup', 'design', 'mcp', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
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', 'playbook', 'feed', 'brief', 'social', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join', 'rsi'];
9
9
 
10
10
  // Damerau-Levenshtein edit distance between two short strings. Counts an
11
11
  // adjacent transposition (e.g. "taks" -> "task") as a single edit, since
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const MEMBER_PROCESS_PATH = 'atris/team/MEMBER_PROCESS.md';
7
+ const MAX_MEMBER_PROCESS_BYTES = 32 * 1024;
8
+
9
+ // Read from the executing workspace only. Never fall back to the launcher,
10
+ // an ancestor workspace, or a bundled policy belonging to another owner.
11
+ function memberProcessPrompt(cwd = process.cwd()) {
12
+ const file = path.join(cwd, MEMBER_PROCESS_PATH);
13
+ let fd;
14
+ try {
15
+ const stat = fs.statSync(file);
16
+ if (!stat.isFile()) throw new Error('expected a regular file');
17
+ fd = fs.openSync(file, 'r');
18
+ const buffer = Buffer.alloc(MAX_MEMBER_PROCESS_BYTES + 1);
19
+ const count = fs.readSync(fd, buffer, 0, buffer.length, 0);
20
+ if (count > MAX_MEMBER_PROCESS_BYTES) {
21
+ throw new Error(`keep the shared process within ${MAX_MEMBER_PROCESS_BYTES} bytes`);
22
+ }
23
+ const body = buffer.subarray(0, count).toString('utf8').trim();
24
+ if (!body) return '';
25
+ return [
26
+ '## Shared member process',
27
+ `Source: ${MEMBER_PROCESS_PATH} in this execution workspace.`,
28
+ 'Apply these defaults alongside the member identity. They do not expand role permissions or override operator direction, task scope, output format, or frozen mission constraints.',
29
+ '',
30
+ body,
31
+ '',
32
+ 'End of shared member process. The current task and its constraints follow.',
33
+ ].join('\n');
34
+ } catch (error) {
35
+ if (error.code === 'ENOENT') return '';
36
+ throw new Error(`Cannot load ${MEMBER_PROCESS_PATH}: ${error.message}`);
37
+ } finally {
38
+ if (fd !== undefined) fs.closeSync(fd);
39
+ }
40
+ }
41
+
42
+ module.exports = { memberProcessPrompt, MEMBER_PROCESS_PATH, MAX_MEMBER_PROCESS_BYTES };