atris 3.53.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 (119) 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/x-search/SKILL.md +24 -22
  7. package/atris/skills/youtube/SKILL.md +82 -33
  8. package/atris/team/_template/SOUL.md +40 -0
  9. package/atris/team/validator/MEMBER.md +1 -2
  10. package/bin/atris +21 -0
  11. package/bin/atris.js +563 -373
  12. package/commands/api-key.js +170 -0
  13. package/commands/auth.js +453 -43
  14. package/commands/autoland.js +3 -1
  15. package/commands/autopilot-front.js +58 -11
  16. package/commands/avail.js +5 -4
  17. package/commands/balance.js +55 -0
  18. package/commands/bench.js +29 -1
  19. package/commands/brain.js +63 -12
  20. package/commands/brainstorm.js +18 -0
  21. package/commands/business.js +149 -1
  22. package/commands/chat-scan.js +17 -4
  23. package/commands/close.js +5 -1
  24. package/commands/doctor.js +174 -0
  25. package/commands/dream.js +7 -0
  26. package/commands/drill.js +34 -2
  27. package/commands/engine.js +47 -35
  28. package/commands/errors.js +58 -8
  29. package/commands/experiments.js +14 -0
  30. package/commands/feedback.js +61 -2
  31. package/commands/fleet-report.js +27 -7
  32. package/commands/fleet.js +98 -11
  33. package/commands/founder.js +60 -21
  34. package/commands/human-missions.js +79 -4
  35. package/commands/improve.js +54 -4
  36. package/commands/init.js +68 -92
  37. package/commands/install.js +40 -0
  38. package/commands/integrations.js +399 -30
  39. package/commands/interview.js +8 -0
  40. package/commands/land.js +48 -17
  41. package/commands/learn.js +29 -4
  42. package/commands/log.js +51 -0
  43. package/commands/member.js +68 -10
  44. package/commands/mission.js +287 -50
  45. package/commands/now.js +15 -3
  46. package/commands/orb.js +97 -2
  47. package/commands/pack.js +83 -9
  48. package/commands/playbook.js +381 -0
  49. package/commands/plugin.js +16 -0
  50. package/commands/radar.js +31 -7
  51. package/commands/recap.js +128 -21
  52. package/commands/release.js +102 -2
  53. package/commands/review.js +5 -10
  54. package/commands/revisions.js +55 -0
  55. package/commands/run-front.js +12 -4
  56. package/commands/scout.js +12 -1
  57. package/commands/search.js +123 -28
  58. package/commands/sign.js +2 -2
  59. package/commands/signup.js +12 -3
  60. package/commands/site-deploy.js +17 -4
  61. package/commands/site.js +2 -2
  62. package/commands/skill-eval.js +289 -0
  63. package/commands/skill.js +56 -6
  64. package/commands/social.js +603 -0
  65. package/commands/spaceship.js +69 -6
  66. package/commands/stream.js +8 -1
  67. package/commands/study.js +32 -19
  68. package/commands/sync.js +28 -22
  69. package/commands/task.js +418 -60
  70. package/commands/teach.js +72 -63
  71. package/commands/terminal.js +25 -9
  72. package/commands/topup.js +88 -0
  73. package/commands/truth.js +52 -6
  74. package/commands/usage.js +91 -0
  75. package/commands/verify.js +3 -2
  76. package/commands/version.js +3 -1
  77. package/commands/voice.js +5 -1
  78. package/commands/who.js +69 -11
  79. package/commands/wiki.js +7 -4
  80. package/commands/wish.js +76 -5
  81. package/commands/workflow.js +327 -145
  82. package/commands/worktree.js +1 -0
  83. package/commands/write.js +5 -0
  84. package/commands/x-search.js +340 -0
  85. package/commands/xp.js +5 -1
  86. package/commands/youtube.js +551 -25
  87. package/lib/account-bound.js +47 -0
  88. package/lib/auto-accept-certified.js +8 -1
  89. package/lib/auto-lessons.js +186 -0
  90. package/lib/autoland.js +4 -2
  91. package/lib/chat-log-scan.js +7 -4
  92. package/lib/cli-json.js +77 -0
  93. package/lib/cli-scope.js +26 -0
  94. package/lib/codex-flight.js +1 -0
  95. package/lib/conductor-artifacts.js +1 -1
  96. package/lib/context-gatherer.js +11 -0
  97. package/lib/developer-api.js +116 -0
  98. package/lib/feedback-triage.js +274 -0
  99. package/lib/first-minute.js +398 -0
  100. package/lib/fleet.js +115 -34
  101. package/lib/functional-owner.js +22 -0
  102. package/lib/known-commands.js +31 -5
  103. package/lib/member-scaffold.js +197 -0
  104. package/lib/mission-root.js +4 -2
  105. package/lib/noninteractive.js +88 -0
  106. package/lib/policy-lessons.js +4 -1
  107. package/lib/revision-metric.js +279 -0
  108. package/lib/scratch-root.js +48 -0
  109. package/lib/skill-eval-gate.js +249 -0
  110. package/lib/task-db.js +4 -0
  111. package/lib/task-proof.js +69 -8
  112. package/lib/verifier-quality.js +69 -0
  113. package/lib/wish-audit.js +2 -2
  114. package/lib/wish-delegate.js +10 -1
  115. package/lib/workspace-scaffold.js +270 -0
  116. package/package.json +3 -2
  117. package/scripts/det/checklist-score.js +191 -0
  118. package/scripts/det/ytsearch +31 -0
  119. package/utils/update-check.js +15 -0
@@ -0,0 +1,270 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { ensureWikiScaffold } = require('./wiki');
4
+
5
+ const PACKAGE_ATRIS_MD = path.join(__dirname, '..', 'atris.md');
6
+
7
+ function mapPlaceholder() {
8
+ return '# MAP.md\n\n> Generated by your AI agent after reading atris.md\n\nRun your AI agent with atris.md to populate this file.\n';
9
+ }
10
+
11
+ function todoPlaceholder() {
12
+ return `# TODO.md
13
+
14
+ > Working task queue for this project. Target state = 0.
15
+ > Note: Daily tasks also appear in \`atris/logs/YYYY/YYYY-MM-DD.md\`
16
+
17
+ ---
18
+
19
+ ## Backlog
20
+
21
+ - **T1:** Generate MAP.md — scan codebase, create navigation index with file:line refs
22
+
23
+ ---
24
+
25
+ ## In Progress
26
+
27
+ (empty)
28
+
29
+ ---
30
+
31
+ ## Completed
32
+
33
+ (Validator deletes after verification)
34
+
35
+ ---
36
+ `;
37
+ }
38
+
39
+ function firstJournalMarkdown(today) {
40
+ return `# Log — ${today}
41
+
42
+ ## Completed ✅
43
+
44
+ ---
45
+
46
+ ## In Progress 🔄
47
+
48
+ ---
49
+
50
+ ## Backlog
51
+
52
+ - **T1:** Generate MAP.md — scan codebase, create navigation index with file:line refs
53
+
54
+ ---
55
+
56
+ ## Notes
57
+
58
+ **Bootstrap:** Say "atris next" to start. After MAP.md is generated, system will brainstorm ideas for your first build.
59
+
60
+ ---
61
+
62
+ ## Inbox
63
+
64
+ `;
65
+ }
66
+
67
+ function posixRel(root, filePath) {
68
+ return path.relative(root, filePath).split(path.sep).join('/');
69
+ }
70
+
71
+ function writeIfMissing(filePath, content) {
72
+ if (fs.existsSync(filePath)) return false;
73
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
74
+ fs.writeFileSync(filePath, content, { encoding: 'utf8', flag: 'wx' });
75
+ return true;
76
+ }
77
+
78
+ function copyIfMissing(sourcePath, destPath) {
79
+ if (fs.existsSync(destPath) || !fs.existsSync(sourcePath)) return false;
80
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
81
+ fs.copyFileSync(sourcePath, destPath);
82
+ return true;
83
+ }
84
+
85
+ function ensureDir(dirPath) {
86
+ if (fs.existsSync(dirPath)) return false;
87
+ fs.mkdirSync(dirPath, { recursive: true });
88
+ return true;
89
+ }
90
+
91
+ function hasDatedLog(logsDir) {
92
+ if (!fs.existsSync(logsDir)) return false;
93
+ const walk = (dir) => {
94
+ let entries;
95
+ try {
96
+ entries = fs.readdirSync(dir, { withFileTypes: true });
97
+ } catch {
98
+ return false;
99
+ }
100
+ for (const entry of entries) {
101
+ const full = path.join(dir, entry.name);
102
+ if (entry.isDirectory()) {
103
+ if (walk(full)) return true;
104
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
105
+ return true;
106
+ }
107
+ }
108
+ return false;
109
+ };
110
+ return walk(logsDir);
111
+ }
112
+
113
+ function workspaceBrainPresent(projectRoot = process.cwd()) {
114
+ const atrisDir = path.join(projectRoot, 'atris');
115
+ const required = ['MAP.md', 'TODO.md', 'now.md', 'atris.md', path.join('wiki', 'index.md')];
116
+ if (!required.every((rel) => fs.existsSync(path.join(atrisDir, rel)))) return false;
117
+ try {
118
+ if (!fs.statSync(path.join(atrisDir, 'team')).isDirectory()) return false;
119
+ } catch {
120
+ return false;
121
+ }
122
+ return hasDatedLog(path.join(atrisDir, 'logs'));
123
+ }
124
+
125
+ function ensureWorkspaceBrain(projectRoot = process.cwd(), { now = new Date() } = {}) {
126
+ const created = [];
127
+ const atrisDir = path.join(projectRoot, 'atris');
128
+ const note = (filePath) => created.push(posixRel(projectRoot, filePath));
129
+
130
+ ensureDir(atrisDir);
131
+
132
+ const wikiIndex = path.join(atrisDir, 'wiki', 'index.md');
133
+ const wikiMissing = !fs.existsSync(wikiIndex);
134
+ ensureWikiScaffold(projectRoot);
135
+ if (wikiMissing && fs.existsSync(wikiIndex)) note(wikiIndex);
136
+
137
+ const mapFile = path.join(atrisDir, 'MAP.md');
138
+ if (writeIfMissing(mapFile, mapPlaceholder())) note(mapFile);
139
+
140
+ const todoFile = path.join(atrisDir, 'TODO.md');
141
+ if (writeIfMissing(todoFile, todoPlaceholder())) note(todoFile);
142
+
143
+ const nowFile = path.join(atrisDir, 'now.md');
144
+ if (!fs.existsSync(nowFile)) {
145
+ const { renderDefaultNow } = require('../commands/now');
146
+ if (writeIfMissing(nowFile, renderDefaultNow(projectRoot))) note(nowFile);
147
+ }
148
+
149
+ const year = String(now.getFullYear());
150
+ const today = now.toISOString().split('T')[0];
151
+ const journalFile = path.join(atrisDir, 'logs', year, `${today}.md`);
152
+ if (writeIfMissing(journalFile, firstJournalMarkdown(today))) note(journalFile);
153
+
154
+ const teamDir = path.join(atrisDir, 'team');
155
+ if (ensureDir(teamDir)) created.push(posixRel(projectRoot, teamDir) + '/');
156
+
157
+ const atrisMd = path.join(atrisDir, 'atris.md');
158
+ if (copyIfMissing(PACKAGE_ATRIS_MD, atrisMd)) note(atrisMd);
159
+
160
+ const taskSeed = seedInitTaskPlane(projectRoot, { todoFile, journalFile });
161
+
162
+ return { created, atrisDir, taskSeed };
163
+ }
164
+
165
+ /**
166
+ * Seed the SQLite task plane from TODO.md + today's journal so status/task list agree.
167
+ * Idempotent via source_key and normalized-title dedupe.
168
+ */
169
+ function seedInitTaskPlane(projectRoot, { todoFile, journalFile } = {}) {
170
+ try {
171
+ const taskDb = require('./task-db');
172
+ const { parseTodoFile, parseSection } = require('./todo-fallback');
173
+ const { parseInboxItems } = require('./file-ops');
174
+ const db = taskDb.open();
175
+ const workspaceRoot = taskDb.workspaceRoot(projectRoot);
176
+ const existing = taskDb.listTasks(db, { workspaceRoot, limit: 500 });
177
+ const knownTitles = new Set(existing.map((row) => taskDb.normalizeTitle(row.title)));
178
+
179
+ const candidates = [];
180
+ const todoPath = todoFile || path.join(projectRoot, 'atris', 'TODO.md');
181
+ if (fs.existsSync(todoPath)) {
182
+ const parsed = parseTodoFile(todoPath);
183
+ for (const t of parsed.backlog || []) {
184
+ candidates.push({ ...t, importStatus: 'open', sourceFile: todoPath });
185
+ }
186
+ for (const t of parsed.inProgress || []) {
187
+ candidates.push({ ...t, importStatus: 'claimed', sourceFile: todoPath });
188
+ }
189
+ for (const t of parsed.review || []) {
190
+ candidates.push({ ...t, importStatus: 'review', sourceFile: todoPath });
191
+ }
192
+ }
193
+
194
+ const journalPath = journalFile || null;
195
+ if (journalPath && fs.existsSync(journalPath)) {
196
+ const content = fs.readFileSync(journalPath, 'utf8');
197
+ for (const t of parseSection(content, 'Backlog')) {
198
+ candidates.push({ ...t, importStatus: 'open', sourceFile: journalPath });
199
+ }
200
+ for (const t of parseSection(content, 'In Progress')) {
201
+ candidates.push({ ...t, importStatus: 'claimed', sourceFile: journalPath });
202
+ }
203
+ for (const item of parseInboxItems(content)) {
204
+ candidates.push({
205
+ id: `I${item.id}`,
206
+ title: item.text,
207
+ tag: 'inbox',
208
+ importStatus: 'open',
209
+ sourceFile: journalPath,
210
+ });
211
+ }
212
+ }
213
+
214
+ let inserted = 0;
215
+ let skipped = 0;
216
+ for (const t of candidates) {
217
+ if (!t.title) continue;
218
+ const normalized = taskDb.normalizeTitle(t.title);
219
+ if (knownTitles.has(normalized)) {
220
+ skipped += 1;
221
+ continue;
222
+ }
223
+ const sk = taskDb.sourceKey(t.sourceFile, t.title);
224
+ const result = taskDb.addTask(db, {
225
+ title: t.title,
226
+ tag: t.tag || null,
227
+ workspaceRoot,
228
+ sourceKey: sk,
229
+ status: t.importStatus || 'open',
230
+ claimedBy: t.claimed || null,
231
+ metadata: {
232
+ source: 'init_seed',
233
+ todo_id: t.id || null,
234
+ todo_tags: t.tags || [],
235
+ },
236
+ });
237
+ if (result.inserted) {
238
+ inserted += 1;
239
+ knownTitles.add(normalized);
240
+ } else {
241
+ skipped += 1;
242
+ }
243
+ }
244
+
245
+ const rows = taskDb.listTasks(db, { workspaceRoot, limit: 500 });
246
+ const projection = taskDb.taskProjection(db, { workspaceRoot, limit: 500 });
247
+ const projectionPath = path.join(projectRoot, '.atris', 'state', 'tasks.projection.json');
248
+ fs.mkdirSync(path.dirname(projectionPath), { recursive: true });
249
+ fs.writeFileSync(projectionPath, `${JSON.stringify(projection, null, 2)}\n`, 'utf8');
250
+ try {
251
+ fs.writeFileSync(
252
+ path.join(projectRoot, 'atris', 'TODO.md'),
253
+ taskDb.renderTodoMarkdown(rows, { title: 'TODO.md' }),
254
+ 'utf8'
255
+ );
256
+ } catch {}
257
+
258
+ return { ok: true, inserted, skipped, count: rows.length, projectionPath };
259
+ } catch (error) {
260
+ return {
261
+ ok: false,
262
+ error: error && error.message ? error.message : String(error),
263
+ };
264
+ }
265
+ }
266
+
267
+ module.exports = {
268
+ ensureWorkspaceBrain,
269
+ workspaceBrainPresent,
270
+ };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.53.0",
3
+ "version": "3.55.0",
4
4
  "description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {
7
- "atris": "bin/atris.js",
7
+ "atris": "bin/atris",
8
8
  "ax": "ax"
9
9
  },
10
10
  "files": [
@@ -55,6 +55,7 @@
55
55
  "atris/team/opus-overnight/START_HERE.md",
56
56
  "atris/team/opus-overnight/STEERING.md",
57
57
  "atris/team/_template/MEMBER.md",
58
+ "atris/team/_template/SOUL.md",
58
59
  "atris/features/_templates/",
59
60
  "atris/features/company-brain-sync/",
60
61
  "atris/policies/",
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Pareto-3 checklist quality score.
5
+ //
6
+ // Measures the three front doors a brand-new person actually hits, the way
7
+ // they would hit them: from an empty folder, with no context, reading only
8
+ // what the tool prints. Every point is earned by a live probe, never opinion.
9
+ //
10
+ // 1. onboard - `atris init --yes` in a fresh folder
11
+ // 2. chat - one real answered message through `ax`
12
+ // 3. mission - the end-to-end loop drill in its own sandbox
13
+ //
14
+ // Rubric per feature (100 points):
15
+ // cold start works, zero setup ......... 40
16
+ // speed to first value ................. 20 (full marks at target, 0 at 4x)
17
+ // plain language on the first screen ... 20 (fraction of clean lines)
18
+ // graceful failure on wrong input ...... 20 (honest exit + plain reason,
19
+ // no stack trace)
20
+ //
21
+ // Usage: node scripts/det/checklist-score.js [--json]
22
+
23
+ const { spawnSync } = require('node:child_process');
24
+ const fs = require('node:fs');
25
+ const os = require('node:os');
26
+ const path = require('node:path');
27
+
28
+ const repoRoot = path.resolve(__dirname, '..', '..');
29
+ const cliPath = path.join(repoRoot, 'bin', 'atris.js');
30
+ const axPath = path.join(repoRoot, 'ax');
31
+ const { scanText } = require(path.join(repoRoot, 'lib', 'voice-gate'));
32
+
33
+ // System nouns a person with no idea should never have to decode on a first
34
+ // screen. Backticked command examples are exempt (they are copy targets).
35
+ const SYSTEM_NOUNS = /\b(?:verifier|worktree|subagent|jsonl|projection|runner[- ]profile|heartbeat|orchestrat\w+|frontmatter|monorepo)\b/i;
36
+ const STACK_FRAME = /^\s+at .+:\d+:\d+\)?$/;
37
+
38
+ function run(cmd, args, options = {}) {
39
+ const started = Date.now();
40
+ const result = spawnSync(cmd, args, {
41
+ cwd: options.cwd,
42
+ encoding: 'utf8',
43
+ timeout: options.timeoutMs || 180000,
44
+ env: { ...process.env, ATRIS_SKIP_UPDATE_CHECK: '1', ...(options.env || {}) },
45
+ });
46
+ return {
47
+ status: result.status,
48
+ error: result.error || null,
49
+ stdout: result.stdout || '',
50
+ stderr: result.stderr || '',
51
+ ms: Date.now() - started,
52
+ };
53
+ }
54
+
55
+ function speedPoints(ms, targetMs) {
56
+ if (ms <= targetMs) return 20;
57
+ const over = (ms - targetMs) / (targetMs * 3);
58
+ return Math.max(0, Math.round(20 * (1 - over)));
59
+ }
60
+
61
+ function plainPoints(text) {
62
+ const lines = String(text || '')
63
+ .split(/\r?\n/)
64
+ .map((line) => line.trim())
65
+ .filter(Boolean)
66
+ .slice(0, 30);
67
+ if (!lines.length) return { points: 0, dirty: ['(no output at all)'] };
68
+ const dirty = [];
69
+ for (const line of lines) {
70
+ // Command examples are copy targets, not prose; the reader with no idea
71
+ // reads the description column, so scan only the human words.
72
+ if (/^`.*`$/.test(line) || /^\$\s/.test(line) || /^(?:ax|atris|npx)\s/.test(line)) continue;
73
+ const bare = line
74
+ .replace(/`[^`]*`/g, '')
75
+ .replace(/--[\w-]+/g, '')
76
+ .replace(/\[[^\]]*\]/g, '')
77
+ .replace(/<[^>]*>/g, '');
78
+ if (STACK_FRAME.test(line) || /\u2014/.test(bare) || SYSTEM_NOUNS.test(bare) || scanText(bare).length) {
79
+ dirty.push(line.slice(0, 80));
80
+ }
81
+ }
82
+ const clean = lines.length - dirty.length;
83
+ return { points: Math.round((20 * clean) / lines.length), dirty };
84
+ }
85
+
86
+ function failurePoints(result) {
87
+ const text = `${result.stdout}\n${result.stderr}`;
88
+ const honest = result.status !== 0 && !result.error;
89
+ const noStack = !text.split(/\r?\n/).some((line) => STACK_FRAME.test(line))
90
+ && !/\b(?:TypeError|ReferenceError|SyntaxError)\b/.test(text);
91
+ const saysWhy = /\b(?:unknown|not found|no such|missing|usage|help|try|error)\b/i.test(text);
92
+ let points = 0;
93
+ if (honest) points += 8;
94
+ if (noStack) points += 6;
95
+ if (saysWhy) points += 6;
96
+ return points;
97
+ }
98
+
99
+ function scoreOnboard(base) {
100
+ const dir = fs.mkdtempSync(path.join(base, 'onboard-'));
101
+ const cold = run(process.execPath, [cliPath, 'init', '--yes'], { cwd: dir });
102
+ const artifact = fs.existsSync(path.join(dir, 'atris'));
103
+ const coldPoints = cold.status === 0 && artifact ? 40 : 0;
104
+ const plain = plainPoints(cold.stdout);
105
+ const bad = run(process.execPath, [cliPath, 'definitely-not-a-command'], { cwd: dir });
106
+ return {
107
+ feature: 'onboard: atris init in an empty folder',
108
+ dir,
109
+ cold: coldPoints,
110
+ speed: speedPoints(cold.ms, 10000),
111
+ plain: plain.points,
112
+ failure: failurePoints(bad),
113
+ ms: cold.ms,
114
+ dirty_lines: plain.dirty,
115
+ };
116
+ }
117
+
118
+ function scoreChat(base, workspaceDir) {
119
+ const cold = run(process.execPath, [axPath, '--fast', '--print', 'Reply with exactly: checklist-ok'], {
120
+ cwd: workspaceDir,
121
+ timeoutMs: 120000,
122
+ });
123
+ let answered = false;
124
+ try {
125
+ const payload = JSON.parse(cold.stdout.slice(cold.stdout.indexOf('{')));
126
+ answered = payload.ok === true && /checklist-ok/.test(String(payload.output || ''));
127
+ } catch {}
128
+ const helpScreen = run(process.execPath, [axPath, '--help'], { cwd: workspaceDir, timeoutMs: 30000 });
129
+ const plain = plainPoints(helpScreen.stdout);
130
+ const bad = run(process.execPath, [axPath, '--fast', '--print'], { cwd: workspaceDir, timeoutMs: 30000 });
131
+ return {
132
+ feature: 'chat: one real answered message through ax',
133
+ cold: cold.status !== null && answered ? 40 : 0,
134
+ speed: speedPoints(cold.ms, 15000),
135
+ plain: plain.points,
136
+ failure: failurePoints(bad),
137
+ ms: cold.ms,
138
+ dirty_lines: plain.dirty,
139
+ };
140
+ }
141
+
142
+ function scoreMission(base, workspaceDir) {
143
+ const cold = run(process.execPath, [cliPath, 'drill', '--json'], { cwd: workspaceDir, timeoutMs: 300000 });
144
+ let passed = false;
145
+ let stages = 0;
146
+ try {
147
+ const payload = JSON.parse(cold.stdout.slice(cold.stdout.indexOf('{')));
148
+ passed = payload.pass === true;
149
+ stages = Array.isArray(payload.stages) ? payload.stages.length : 0;
150
+ } catch {}
151
+ const status = run(process.execPath, [cliPath, 'mission'], { cwd: workspaceDir, timeoutMs: 60000 });
152
+ const plain = plainPoints(status.stdout);
153
+ const bad = run(process.execPath, [cliPath, 'mission', 'run', '9999'], { cwd: workspaceDir, timeoutMs: 60000 });
154
+ return {
155
+ feature: `mission: full loop drill (${stages} stages) plus status screen`,
156
+ cold: passed ? 40 : 0,
157
+ speed: speedPoints(cold.ms, 90000),
158
+ plain: plain.points,
159
+ failure: failurePoints(bad),
160
+ ms: cold.ms,
161
+ dirty_lines: plain.dirty,
162
+ };
163
+ }
164
+
165
+ function main() {
166
+ const asJson = process.argv.includes('--json');
167
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-checklist-'));
168
+ const onboard = scoreOnboard(base);
169
+ const chat = scoreChat(base, onboard.dir);
170
+ const mission = scoreMission(base, onboard.dir);
171
+ const features = [onboard, chat, mission].map((f) => ({
172
+ ...f,
173
+ total: f.cold + f.speed + f.plain + f.failure,
174
+ }));
175
+ const overall = Math.round(features.reduce((sum, f) => sum + f.total, 0) / features.length);
176
+ const report = { schema: 'atris.checklist_score.v1', overall, features };
177
+ fs.rmSync(base, { recursive: true, force: true });
178
+ if (asJson) {
179
+ console.log(JSON.stringify(report, null, 2));
180
+ return;
181
+ }
182
+ console.log('Pareto-3 checklist quality score\n');
183
+ for (const f of features) {
184
+ console.log(` ${f.feature}`);
185
+ console.log(` cold start ${f.cold}/40 · speed ${f.speed}/20 (${(f.ms / 1000).toFixed(1)}s) · plain words ${f.plain}/20 · graceful failure ${f.failure}/20 => ${f.total}/100`);
186
+ if (f.dirty_lines.length) console.log(` lines that need plain words: ${f.dirty_lines.length}`);
187
+ }
188
+ console.log(`\n overall: ${overall}/100`);
189
+ }
190
+
191
+ main();
@@ -0,0 +1,31 @@
1
+ #!/bin/bash
2
+ # ytsearch <query> [limit]: free YouTube discovery via yt-dlp ytsearchN.
3
+ # Zero Atris credits. Prints one line per video:
4
+ # title | channel | duration | views | upload_date | https://youtu.be/ID
5
+ # upload_date is YYYYMMDD (or NA) so callers can apply a freshness gate.
6
+ set -euo pipefail
7
+
8
+ USAGE='usage: ytsearch "<query>" [limit]'
9
+
10
+ QUERY="${1:-}"
11
+ LIMIT="${2:-5}"
12
+
13
+ if [ -z "$QUERY" ] || [ "$QUERY" = '-h' ] || [ "$QUERY" = '--help' ] || [ "$QUERY" = 'help' ]; then
14
+ echo "$USAGE" >&2
15
+ exit 2
16
+ fi
17
+
18
+ if ! [[ "$LIMIT" =~ ^[1-9][0-9]*$ ]]; then
19
+ echo "limit must be a positive integer" >&2
20
+ exit 2
21
+ fi
22
+
23
+ if ! command -v yt-dlp >/dev/null 2>&1; then
24
+ echo "yt-dlp not found on PATH" >&2
25
+ exit 2
26
+ fi
27
+
28
+ # flat-playlist keeps this a metadata-only discovery call (no download).
29
+ yt-dlp --no-update --flat-playlist --no-warnings \
30
+ --print '%(title)s | %(channel)s | %(duration_string)s | %(view_count)s | %(upload_date)s | https://youtu.be/%(id)s' \
31
+ "ytsearch${LIMIT}:${QUERY}"
@@ -293,6 +293,20 @@ function formatInstallGitWarning(state) {
293
293
  'npm install -g atris@latest may not change the code currently on PATH; resolve that checkout before trusting upgrade status.';
294
294
  }
295
295
 
296
+ /**
297
+ * Human version line. Published/sealed installs stay plain.
298
+ * Git checkouts append `(git <sha> [dirty] <branch>)`.
299
+ */
300
+ function formatCliVersionLine(version, packageRoot = path.join(__dirname, '..')) {
301
+ const base = String(version || 'unknown').replace(/^v/, '');
302
+ if (!isGitCheckout(packageRoot)) return `atris v${base}`;
303
+ const state = inspectInstallGitState(packageRoot);
304
+ const sha = state.head || 'unknown';
305
+ const branch = state.detached ? 'detached' : (state.branch || 'unknown');
306
+ if (state.dirty) return `atris v${base} (git ${sha} dirty ${branch})`;
307
+ return `atris v${base} (git ${sha} ${branch})`;
308
+ }
309
+
296
310
  function normalizeAutoUpdateMode(env = process.env) {
297
311
  const raw = String(env.ATRIS_AUTO_UPDATE || '').trim().toLowerCase();
298
312
  if (['0', 'false', 'no', 'off', 'notify'].includes(raw)) return 'off';
@@ -349,6 +363,7 @@ module.exports = {
349
363
  shouldAutoUpdate,
350
364
  inspectInstallGitState,
351
365
  formatInstallGitWarning,
366
+ formatCliVersionLine,
352
367
  isGitCheckout,
353
368
  getNpmSelfUpdateCommand,
354
369
  getNpmSelfUpdateSpawnArgs,