atris 3.53.0 → 3.56.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/FOR_AGENTS.md +7 -0
- package/README.md +1 -1
- package/atris/policies/ANTISLOP.md +2 -1
- package/atris/skills/copy-editor/SKILL.md +2 -1
- package/atris/skills/design/SKILL.md +1 -1
- package/atris/skills/x-search/SKILL.md +24 -22
- package/atris/skills/youtube/SKILL.md +82 -33
- package/atris/team/_template/SOUL.md +40 -0
- package/atris/team/validator/MEMBER.md +1 -2
- package/bin/atris +21 -0
- package/bin/atris.js +563 -373
- package/commands/api-key.js +170 -0
- package/commands/auth.js +453 -43
- package/commands/autoland.js +3 -1
- package/commands/autopilot-front.js +58 -11
- package/commands/avail.js +5 -4
- package/commands/balance.js +55 -0
- package/commands/bench.js +29 -1
- package/commands/brain.js +63 -12
- package/commands/brainstorm.js +18 -0
- package/commands/business.js +149 -1
- package/commands/chat-scan.js +17 -4
- package/commands/close.js +5 -1
- package/commands/doctor.js +174 -0
- package/commands/dream.js +7 -0
- package/commands/drill.js +34 -2
- package/commands/engine.js +47 -35
- package/commands/errors.js +58 -8
- package/commands/experiments.js +14 -0
- package/commands/feedback.js +61 -2
- package/commands/fleet-report.js +27 -7
- package/commands/fleet.js +98 -11
- package/commands/founder.js +60 -21
- package/commands/human-missions.js +79 -4
- package/commands/improve.js +54 -4
- package/commands/init.js +68 -92
- package/commands/install.js +40 -0
- package/commands/integrations.js +565 -30
- package/commands/interview.js +8 -0
- package/commands/land.js +48 -17
- package/commands/learn.js +29 -4
- package/commands/log.js +51 -0
- package/commands/member.js +68 -10
- package/commands/mission.js +287 -50
- package/commands/now.js +15 -3
- package/commands/orb.js +97 -2
- package/commands/pack.js +83 -9
- package/commands/playbook.js +381 -0
- package/commands/plugin.js +16 -0
- package/commands/radar.js +31 -7
- package/commands/recap.js +128 -21
- package/commands/release.js +102 -2
- package/commands/review.js +5 -10
- package/commands/revisions.js +55 -0
- package/commands/run-front.js +12 -4
- package/commands/scout.js +12 -1
- package/commands/search.js +123 -28
- package/commands/sign.js +2 -2
- package/commands/signup.js +12 -3
- package/commands/site-deploy.js +17 -4
- package/commands/site.js +2 -2
- package/commands/skill-eval.js +289 -0
- package/commands/skill.js +56 -6
- package/commands/social.js +603 -0
- package/commands/spaceship.js +69 -6
- package/commands/stream.js +8 -1
- package/commands/study.js +32 -19
- package/commands/sync.js +28 -22
- package/commands/task.js +435 -60
- package/commands/teach.js +72 -63
- package/commands/terminal.js +25 -9
- package/commands/topup.js +88 -0
- package/commands/truth.js +52 -6
- package/commands/usage.js +91 -0
- package/commands/verify.js +3 -2
- package/commands/version.js +3 -1
- package/commands/voice.js +5 -1
- package/commands/who.js +69 -11
- package/commands/wiki.js +7 -4
- package/commands/wish.js +76 -5
- package/commands/workflow.js +327 -145
- package/commands/worktree.js +1 -0
- package/commands/write.js +5 -0
- package/commands/x-search.js +340 -0
- package/commands/xp.js +5 -1
- package/commands/youtube.js +551 -25
- package/lib/account-bound.js +47 -0
- package/lib/auto-accept-certified.js +8 -1
- package/lib/auto-lessons.js +186 -0
- package/lib/autoland.js +4 -2
- package/lib/chat-log-scan.js +7 -4
- package/lib/cli-json.js +77 -0
- package/lib/cli-scope.js +26 -0
- package/lib/codex-flight.js +1 -0
- package/lib/conductor-artifacts.js +1 -1
- package/lib/context-gatherer.js +11 -0
- package/lib/developer-api.js +116 -0
- package/lib/feedback-triage.js +274 -0
- package/lib/first-minute.js +398 -0
- package/lib/fleet.js +115 -34
- package/lib/functional-owner.js +22 -0
- package/lib/known-commands.js +31 -5
- package/lib/member-scaffold.js +197 -0
- package/lib/mission-root.js +4 -2
- package/lib/noninteractive.js +88 -0
- package/lib/policy-lessons.js +4 -1
- package/lib/revision-metric.js +279 -0
- package/lib/scratch-root.js +48 -0
- package/lib/skill-eval-gate.js +249 -0
- package/lib/task-db.js +4 -0
- package/lib/task-proof.js +69 -8
- package/lib/verifier-quality.js +69 -0
- package/lib/wish-audit.js +2 -2
- package/lib/wish-delegate.js +10 -1
- package/lib/workspace-scaffold.js +270 -0
- package/package.json +3 -2
- package/scripts/det/checklist-score.js +191 -0
- package/scripts/det/ytsearch +31 -0
- 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.
|
|
3
|
+
"version": "3.56.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
|
|
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}"
|
package/utils/update-check.js
CHANGED
|
@@ -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,
|