atris 3.52.0 → 3.55.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.
Files changed (131) hide show
  1. package/FOR_AGENTS.md +7 -0
  2. package/README.md +1 -1
  3. package/atris/policies/ANTISLOP.md +2 -1
  4. package/atris/skills/copy-editor/SKILL.md +2 -1
  5. package/atris/skills/design/SKILL.md +1 -1
  6. package/atris/skills/engines/SKILL.md +8 -7
  7. package/atris/skills/x-search/SKILL.md +24 -22
  8. package/atris/skills/youtube/SKILL.md +89 -40
  9. package/atris/team/_template/SOUL.md +40 -0
  10. package/atris/team/validator/MEMBER.md +1 -2
  11. package/ax +592 -91
  12. package/bin/atris +21 -0
  13. package/bin/atris.js +580 -378
  14. package/commands/api-key.js +170 -0
  15. package/commands/auth.js +453 -43
  16. package/commands/autoland.js +3 -1
  17. package/commands/autopilot-front.js +58 -11
  18. package/commands/avail.js +5 -4
  19. package/commands/balance.js +55 -0
  20. package/commands/bench.js +29 -1
  21. package/commands/brain.js +63 -12
  22. package/commands/brainstorm.js +18 -0
  23. package/commands/business.js +149 -1
  24. package/commands/chat-scan.js +17 -4
  25. package/commands/close.js +5 -1
  26. package/commands/computer.js +2 -2
  27. package/commands/doctor.js +174 -0
  28. package/commands/dream.js +7 -0
  29. package/commands/drill.js +34 -2
  30. package/commands/engine.js +47 -35
  31. package/commands/errors.js +58 -8
  32. package/commands/experiments.js +14 -0
  33. package/commands/feedback.js +61 -2
  34. package/commands/fleet-report.js +27 -7
  35. package/commands/fleet.js +98 -11
  36. package/commands/founder.js +60 -21
  37. package/commands/human-missions.js +79 -4
  38. package/commands/improve.js +54 -4
  39. package/commands/init.js +68 -92
  40. package/commands/install.js +40 -0
  41. package/commands/integrations.js +399 -30
  42. package/commands/interview.js +8 -0
  43. package/commands/land.js +48 -17
  44. package/commands/learn.js +29 -4
  45. package/commands/log.js +51 -0
  46. package/commands/member.js +68 -10
  47. package/commands/mission.js +339 -74
  48. package/commands/now.js +25 -6
  49. package/commands/orb.js +97 -2
  50. package/commands/pack.js +83 -9
  51. package/commands/playbook.js +381 -0
  52. package/commands/plugin.js +16 -0
  53. package/commands/radar.js +96 -40
  54. package/commands/recap.js +128 -21
  55. package/commands/release.js +102 -2
  56. package/commands/review.js +5 -10
  57. package/commands/revisions.js +55 -0
  58. package/commands/run-front.js +12 -4
  59. package/commands/scout.js +12 -1
  60. package/commands/search.js +123 -28
  61. package/commands/sign.js +2 -2
  62. package/commands/signup.js +12 -3
  63. package/commands/site-deploy.js +17 -4
  64. package/commands/site.js +2 -2
  65. package/commands/skill-eval.js +289 -0
  66. package/commands/skill.js +56 -6
  67. package/commands/social.js +603 -0
  68. package/commands/spaceship.js +69 -6
  69. package/commands/stream.js +8 -1
  70. package/commands/study.js +32 -19
  71. package/commands/sync.js +28 -22
  72. package/commands/task.js +418 -60
  73. package/commands/teach.js +72 -63
  74. package/commands/terminal.js +25 -9
  75. package/commands/topup.js +88 -0
  76. package/commands/truth.js +52 -6
  77. package/commands/usage.js +91 -0
  78. package/commands/verify.js +3 -2
  79. package/commands/version.js +3 -1
  80. package/commands/voice.js +5 -1
  81. package/commands/who.js +69 -11
  82. package/commands/wiki.js +7 -4
  83. package/commands/wish.js +76 -5
  84. package/commands/workflow.js +327 -145
  85. package/commands/worktree.js +1 -0
  86. package/commands/write.js +5 -0
  87. package/commands/x-search.js +340 -0
  88. package/commands/xp.js +5 -1
  89. package/commands/youtube.js +551 -33
  90. package/lib/account-bound.js +47 -0
  91. package/lib/auto-accept-certified.js +10 -2
  92. package/lib/auto-lessons.js +186 -0
  93. package/lib/autoland.js +4 -2
  94. package/lib/chat-log-scan.js +7 -4
  95. package/lib/cli-json.js +77 -0
  96. package/lib/cli-scope.js +26 -0
  97. package/lib/codex-flight.js +1 -0
  98. package/lib/conductor-artifacts.js +1 -1
  99. package/lib/context-gatherer.js +11 -0
  100. package/lib/developer-api.js +116 -0
  101. package/lib/engine-ask.js +11 -3
  102. package/lib/engine-registry.js +32 -7
  103. package/lib/feedback-triage.js +274 -0
  104. package/lib/first-minute.js +398 -0
  105. package/lib/fleet.js +115 -34
  106. package/lib/functional-owner.js +22 -0
  107. package/lib/known-commands.js +31 -5
  108. package/lib/member-scaffold.js +197 -0
  109. package/lib/mission-ledger-compact.js +127 -0
  110. package/lib/mission-root.js +4 -2
  111. package/lib/mission-runtime-loop.js +30 -2
  112. package/lib/next-moves.js +34 -34
  113. package/lib/noninteractive.js +88 -0
  114. package/lib/permission-grants.js +10 -1
  115. package/lib/policy-lessons.js +4 -1
  116. package/lib/revision-metric.js +279 -0
  117. package/lib/runner-command.js +5 -4
  118. package/lib/scratch-root.js +48 -0
  119. package/lib/skill-eval-gate.js +249 -0
  120. package/lib/task-db.js +22 -2
  121. package/lib/task-proof.js +75 -9
  122. package/lib/task-receipt.js +5 -1
  123. package/lib/verifier-quality.js +69 -0
  124. package/lib/wish-audit.js +2 -2
  125. package/lib/wish-delegate.js +10 -1
  126. package/lib/workspace-scaffold.js +270 -0
  127. package/package.json +4 -2
  128. package/scripts/det/checklist-score.js +191 -0
  129. package/scripts/det/ytsearch +31 -0
  130. package/scripts/outbound-artifact-gate.js +227 -0
  131. package/utils/update-check.js +15 -0
@@ -0,0 +1,398 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { isGenericScratchRoot } = require('./scratch-root');
7
+
8
+ const FIRST_USE_COMMAND = 'atris "help me choose the first useful step for this project"';
9
+ const INIT_COMMAND = 'atris init --minimal';
10
+ const BARE_ATRIS_FLAGS = new Set(['--yes', '-y', '--json', '--verbose']);
11
+ const NOT_A_PERSON = new Set([
12
+ 'root', 'node', 'ubuntu', 'admin', 'nobody', 'www', 'daemon', 'guest',
13
+ 'test', 'user', 'operator', 'tmp', 'temp', 'atris',
14
+ ]);
15
+
16
+ function clip(value, max = 72) {
17
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
18
+ if (!text) return '';
19
+ if (text.length <= max) return text;
20
+ const slice = text.slice(0, Math.max(0, max - 3));
21
+ const lastSpace = slice.lastIndexOf(' ');
22
+ const body = lastSpace > 0 ? slice.slice(0, lastSpace) : slice;
23
+ return `${body.replace(/[\s,;:.!?-]+$/, '')}...`;
24
+ }
25
+
26
+ function isBareAtrisFlag(token) {
27
+ return BARE_ATRIS_FLAGS.has(String(token || ''));
28
+ }
29
+
30
+ function givenNameFrom(raw) {
31
+ const text = String(raw || '').replace(/\s+/g, ' ').trim();
32
+ if (!text) return '';
33
+ const handle = text.includes('@') ? text.split('@')[0] : text;
34
+ const first = handle.split(/[._+\s/-]/)[0].replace(/\d+$/g, '');
35
+ if (!/^[A-Za-z][A-Za-z'-]{1,15}$/.test(first)) return '';
36
+ const name = first.toLowerCase();
37
+ if (NOT_A_PERSON.has(name)) return '';
38
+ return name;
39
+ }
40
+
41
+ function unixUser(env = process.env) {
42
+ const raw = String(env.USER || env.LOGNAME || '').trim();
43
+ let name = raw.split(/[\\/]/).pop();
44
+ if (!name) {
45
+ try {
46
+ name = String(os.userInfo().username || '').trim();
47
+ } catch {
48
+ name = '';
49
+ }
50
+ }
51
+ if (!name || NOT_A_PERSON.has(name.toLowerCase())) return '';
52
+ return name;
53
+ }
54
+
55
+ function loadSavedAccount() {
56
+ try {
57
+ const home = process.env.HOME || os.homedir();
58
+ const file = path.join(home, '.atris', 'credentials.json');
59
+ if (!fs.existsSync(file)) return null;
60
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
61
+ return parsed && typeof parsed === 'object' ? parsed : null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function personName(env = process.env, account) {
68
+ const fromOperator = givenNameFrom(env.ATRIS_OPERATOR);
69
+ if (fromOperator) return fromOperator;
70
+ const saved = account !== undefined ? account : loadSavedAccount();
71
+ if (saved) {
72
+ const fromAccount = givenNameFrom(saved.name || saved.display_name || saved.username || saved.handle)
73
+ || givenNameFrom(saved.email);
74
+ if (fromAccount) return fromAccount;
75
+ }
76
+ return givenNameFrom(env.USER || env.LOGNAME) || unixUser(env);
77
+ }
78
+
79
+ function isThrowawayFolderName(name) {
80
+ const text = String(name || '');
81
+ if (/^(tmp|temp|atris-)/i.test(text)) return true;
82
+ if (/^[0-9a-f]{8,}$/i.test(text)) return true;
83
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(text);
84
+ }
85
+
86
+ function isScratchFolder(root = process.cwd()) {
87
+ const resolved = path.resolve(root || '.');
88
+ const name = path.basename(resolved);
89
+ if (isThrowawayFolderName(name)) return true;
90
+ // /tmp, /private/tmp, /var/tmp, and os.tmpdir() are rooms, not project parents.
91
+ return isGenericScratchRoot(resolved);
92
+ }
93
+
94
+ function folderName(root = process.cwd()) {
95
+ const name = path.basename(path.resolve(root || '.'));
96
+ if (!name || name === '.' || name === path.sep) return 'this folder';
97
+ if (isScratchFolder(root)) return 'this folder';
98
+ return name;
99
+ }
100
+
101
+ function greet(person) {
102
+ return person ? `hey ${person}, ` : '';
103
+ }
104
+
105
+ function softTitle(title, maxWords = 5) {
106
+ const words = String(title || '').replace(/\s+/g, ' ').trim().split(' ').filter(Boolean);
107
+ if (!words.length) return '';
108
+ const text = words.slice(0, maxWords).join(' ').replace(/[.,;:!?]+$/g, '');
109
+ return `"${text.toLowerCase()}"`;
110
+ }
111
+
112
+ function taskRef(task) {
113
+ const ref = task && (task.display_id || task.legacy_ref || '');
114
+ return String(ref || '').trim();
115
+ }
116
+
117
+ function newest(tasks) {
118
+ return [...(Array.isArray(tasks) ? tasks : [])].sort((a, b) => (
119
+ Number(b && (b.updated_at || b.created_at) || 0)
120
+ - Number(a && (a.updated_at || a.created_at) || 0)
121
+ ))[0] || null;
122
+ }
123
+
124
+ function isCertifiedReview(task) {
125
+ const review = task && task.review || {};
126
+ const metadata = task && task.metadata || {};
127
+ if (review.agent_certified === true || metadata.agent_certified === true) return true;
128
+ return Number(review.agent_review_pass_count || metadata.agent_review_pass_count || 0) >= 2;
129
+ }
130
+
131
+ function loadLocalTasks(root = process.cwd()) {
132
+ try {
133
+ const taskDb = require('./task-db');
134
+ const db = taskDb.open();
135
+ const workspaceRoot = taskDb.workspaceRoot(root);
136
+ const rows = taskDb.listTasks(db, { workspaceRoot, limit: 200 });
137
+ if (Array.isArray(rows) && rows.length) return taskDb.withTaskDisplayRefs(rows);
138
+ } catch {
139
+ // Fall through to the local projection. Tests and fresh folders often have no db.
140
+ }
141
+ const projectionPath = path.join(root, '.atris', 'state', 'tasks.projection.json');
142
+ try {
143
+ const parsed = JSON.parse(fs.readFileSync(projectionPath, 'utf8'));
144
+ return Array.isArray(parsed.tasks) ? parsed.tasks : [];
145
+ } catch {
146
+ return [];
147
+ }
148
+ }
149
+
150
+ function loadLatestRecap(root = process.cwd()) {
151
+ const dir = path.join(root, 'atris', 'reports');
152
+ if (!fs.existsSync(dir)) return null;
153
+ let names = [];
154
+ try {
155
+ names = fs.readdirSync(dir);
156
+ } catch {
157
+ return null;
158
+ }
159
+ const recaps = names
160
+ .filter((name) => /\.md$/i.test(name) && /recap/i.test(name))
161
+ .map((name) => {
162
+ const file = path.join(dir, name);
163
+ let mtime = 0;
164
+ try {
165
+ mtime = fs.statSync(file).mtimeMs;
166
+ } catch {
167
+ mtime = 0;
168
+ }
169
+ return { file, name, mtime };
170
+ })
171
+ .sort((a, b) => b.mtime - a.mtime || a.name.localeCompare(b.name));
172
+ const hit = recaps[0];
173
+ if (!hit) return null;
174
+ let title = path.basename(hit.name, '.md').replace(/[-_]+/g, ' ');
175
+ try {
176
+ const head = fs.readFileSync(hit.file, 'utf8').slice(0, 2000);
177
+ const h1 = head.split('\n').find((line) => line.startsWith('# '));
178
+ if (h1) title = h1.slice(2).trim();
179
+ } catch {
180
+ // Keep the filename title.
181
+ }
182
+ title = clip(title, 72);
183
+ return title ? { title, file: hit.file } : null;
184
+ }
185
+
186
+ function taskCommand(task, person) {
187
+ const ref = taskRef(task);
188
+ const who = person || 'operator';
189
+ if (!task) return FIRST_USE_COMMAND;
190
+ if (task.status === 'review') {
191
+ if (isCertifiedReview(task)) return ref ? `atris task accept ${ref}` : 'atris task reviews --limit 5';
192
+ return ref ? `atris task review-chat ${ref} --as codex-review` : 'atris task reviews --limit 5';
193
+ }
194
+ if (task.status === 'claimed') return ref ? `atris task ready ${ref}` : 'atris do';
195
+ if (task.status === 'open') {
196
+ return ref ? `atris task claim ${ref} --as ${who}` : 'atris task next';
197
+ }
198
+ return 'atris do';
199
+ }
200
+
201
+ function deskNextCommand(tasks, person) {
202
+ const list = Array.isArray(tasks) ? tasks : [];
203
+ const picked = pickNext({ tasks: list, person });
204
+ if (!picked.task) return list.length ? 'atris task next' : 'atris task new';
205
+ return taskCommand(picked.task, person);
206
+ }
207
+
208
+ function taskNextCommand(tasks, person) {
209
+ const list = Array.isArray(tasks) ? tasks : [];
210
+ const picked = pickNext({ tasks: list, person });
211
+ if (!picked.task) return 'atris task new';
212
+ return picked.command;
213
+ }
214
+
215
+ function pickNext({
216
+ tasks = [],
217
+ missions = [],
218
+ person = '',
219
+ completedTitles = [],
220
+ backlogCount = 0,
221
+ inboxCount = 0,
222
+ wipCount = 0,
223
+ } = {}) {
224
+ const reviews = (Array.isArray(tasks) ? tasks : []).filter((task) => task && task.status === 'review');
225
+ const certified = newest(reviews.filter(isCertifiedReview));
226
+ if (certified) return { task: certified, command: taskCommand(certified, person) };
227
+ const claimed = newest(tasks.filter((task) => task && task.status === 'claimed'));
228
+ if (claimed) return { task: claimed, command: taskCommand(claimed, person) };
229
+ const review = newest(reviews);
230
+ if (review) return { task: review, command: taskCommand(review, person) };
231
+ const needTick = (missions || []).find((mission) => mission && mission.verifier && !mission.verifier_passed);
232
+ if (needTick) {
233
+ const id = String(needTick.id || '').trim();
234
+ return {
235
+ mission: needTick,
236
+ command: id
237
+ ? `atris mission tick ${id} --verify --complete-on-pass`
238
+ : 'atris mission status --status active',
239
+ };
240
+ }
241
+ const open = newest(tasks.filter((task) => task && task.status === 'open'));
242
+ if (open) return { task: open, command: taskCommand(open, person) };
243
+ if (wipCount > 0 || backlogCount > 0) return { command: 'atris do' };
244
+ if (inboxCount > 0) return { command: 'atris plan' };
245
+ if (completedTitles.length > 0) return { command: 'atris plan' };
246
+ return { command: FIRST_USE_COMMAND };
247
+ }
248
+
249
+ function renderFresh({ person = '', folder = 'this folder' } = {}) {
250
+ const room = folder || 'this folder';
251
+ return [
252
+ `${greet(person)}${room} is a clean start.`,
253
+ '',
254
+ `next: ${INIT_COMMAND}`,
255
+ ].join('\n');
256
+ }
257
+
258
+ function freshMinuteJson() {
259
+ return {
260
+ schema: 'atris.one_lap.v1',
261
+ ok: false,
262
+ status: 'stuck',
263
+ reason: 'this workspace is not initialized',
264
+ next_action: `${INIT_COMMAND} --yes`,
265
+ };
266
+ }
267
+
268
+ function renderWorkspace({
269
+ person = '',
270
+ folder = 'this folder',
271
+ task = null,
272
+ recap = null,
273
+ completedTitle = '',
274
+ nextCommand = FIRST_USE_COMMAND,
275
+ } = {}) {
276
+ const who = greet(person);
277
+ const title = task && softTitle(task.title);
278
+ let win = `${who}${folder || 'this folder'} is set up.`;
279
+ if (task && task.status === 'done' && title) {
280
+ win = `${who}you already shipped ${title}.`;
281
+ } else if (task && task.status === 'review' && title) {
282
+ win = isCertifiedReview(task)
283
+ ? `${who}${title} is waiting for your ok.`
284
+ : `${who}${title} is ready to look at.`;
285
+ } else if (task && task.status === 'claimed' && title) {
286
+ win = `${who}${title} is already yours.`;
287
+ } else if (task && task.status === 'open' && title) {
288
+ win = `${who}${title} is ready to claim.`;
289
+ } else if (completedTitle) {
290
+ win = `${who}you already shipped ${softTitle(completedTitle)}.`;
291
+ } else if (recap && recap.title) {
292
+ win = `${who}you already have a recap in ${folder || 'this folder'}: ${recap.title}.`;
293
+ }
294
+ const lines = [win];
295
+ if (recap && recap.title && task) lines.push(`last recap: ${recap.title}.`);
296
+ lines.push('');
297
+ lines.push(`next: ${nextCommand}`);
298
+ return lines.join('\n');
299
+ }
300
+
301
+ function buildFirstMinute({
302
+ root = process.cwd(),
303
+ fresh = false,
304
+ person,
305
+ folder,
306
+ context = {},
307
+ missions = [],
308
+ } = {}) {
309
+ const who = person != null ? person : personName();
310
+ const room = folder != null ? folder : folderName(root);
311
+ if (fresh) {
312
+ return {
313
+ kind: 'fresh',
314
+ text: renderFresh({ person: who, folder: room }),
315
+ nextCommand: INIT_COMMAND,
316
+ };
317
+ }
318
+ const tasks = loadLocalTasks(root);
319
+ const picked = pickNext({
320
+ tasks,
321
+ missions,
322
+ person: who,
323
+ completedTitles: Array.isArray(context.completedTasks) ? context.completedTasks : [],
324
+ backlogCount: Array.isArray(context.backlogTasks) ? context.backlogTasks.length : 0,
325
+ inboxCount: typeof context.inboxCount === 'number' ? context.inboxCount : 0,
326
+ wipCount: Array.isArray(context.inProgressTasks) ? context.inProgressTasks.length : 0,
327
+ });
328
+ const recap = loadLatestRecap(root);
329
+ const completedTitle = Array.isArray(context.completedTasks) ? context.completedTasks[0] : '';
330
+ return {
331
+ kind: 'workspace',
332
+ text: renderWorkspace({
333
+ person: who,
334
+ folder: room,
335
+ task: picked.task || null,
336
+ recap,
337
+ completedTitle,
338
+ nextCommand: picked.command,
339
+ }),
340
+ nextCommand: picked.command,
341
+ task: picked.task || null,
342
+ recap,
343
+ };
344
+ }
345
+
346
+ function shouldAutoInitFresh(args = process.argv.slice(2), _env = process.env) {
347
+ const list = Array.isArray(args) ? args : [];
348
+ if (list.includes('--json')) return false;
349
+ // --yes / -y is explicit consent. Headless env must not block it.
350
+ return list.includes('--yes') || list.includes('-y');
351
+ }
352
+
353
+ function spokenLineCount(text) {
354
+ return String(text || '')
355
+ .split(/\n+/)
356
+ .map((line) => line.trim())
357
+ .filter(Boolean).length;
358
+ }
359
+
360
+ function isFreshWorkspace(root = process.cwd()) {
361
+ return !fs.existsSync(path.join(root, 'atris'));
362
+ }
363
+
364
+ function speakFirstMinute({
365
+ root = process.cwd(),
366
+ fresh,
367
+ asJson = false,
368
+ log = console.log,
369
+ } = {}) {
370
+ const isFresh = fresh != null ? Boolean(fresh) : isFreshWorkspace(root);
371
+ if (asJson && isFresh) {
372
+ log(JSON.stringify(freshMinuteJson(), null, 2));
373
+ return 2;
374
+ }
375
+ const screen = buildFirstMinute({ root, fresh: isFresh });
376
+ log('');
377
+ log(screen.text);
378
+ return 0;
379
+ }
380
+
381
+ module.exports = {
382
+ buildFirstMinute,
383
+ deskNextCommand,
384
+ folderName,
385
+ freshMinuteJson,
386
+ isBareAtrisFlag,
387
+ isCertifiedReview,
388
+ isFreshWorkspace,
389
+ personName,
390
+ pickNext,
391
+ renderFresh,
392
+ renderWorkspace,
393
+ shouldAutoInitFresh,
394
+ speakFirstMinute,
395
+ spokenLineCount,
396
+ taskCommand,
397
+ taskNextCommand,
398
+ };