atris 2.5.2 → 2.5.4

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 (50) hide show
  1. package/README.md +10 -0
  2. package/atris/experiments/README.md +118 -0
  3. package/atris/experiments/_examples/smoke-keep-revert/README.md +45 -0
  4. package/atris/experiments/_examples/smoke-keep-revert/candidate.py +8 -0
  5. package/atris/experiments/_examples/smoke-keep-revert/loop.py +129 -0
  6. package/atris/experiments/_examples/smoke-keep-revert/measure.py +47 -0
  7. package/atris/experiments/_examples/smoke-keep-revert/program.md +3 -0
  8. package/atris/experiments/_examples/smoke-keep-revert/proposals/bad_patch.py +19 -0
  9. package/atris/experiments/_examples/smoke-keep-revert/proposals/fix_patch.py +22 -0
  10. package/atris/experiments/_examples/smoke-keep-revert/reset.py +21 -0
  11. package/atris/experiments/_examples/smoke-keep-revert/results.tsv +5 -0
  12. package/atris/experiments/_examples/smoke-keep-revert/visual.svg +52 -0
  13. package/atris/experiments/_fixtures/invalid/BadName/loop.py +1 -0
  14. package/atris/experiments/_fixtures/invalid/BadName/program.md +3 -0
  15. package/atris/experiments/_fixtures/invalid/BadName/results.tsv +1 -0
  16. package/atris/experiments/_fixtures/invalid/bloated-context/loop.py +1 -0
  17. package/atris/experiments/_fixtures/invalid/bloated-context/measure.py +1 -0
  18. package/atris/experiments/_fixtures/invalid/bloated-context/program.md +6 -0
  19. package/atris/experiments/_fixtures/invalid/bloated-context/results.tsv +1 -0
  20. package/atris/experiments/_fixtures/valid/good-experiment/loop.py +1 -0
  21. package/atris/experiments/_fixtures/valid/good-experiment/measure.py +1 -0
  22. package/atris/experiments/_fixtures/valid/good-experiment/program.md +3 -0
  23. package/atris/experiments/_fixtures/valid/good-experiment/results.tsv +1 -0
  24. package/atris/experiments/_template/pack/loop.py +3 -0
  25. package/atris/experiments/_template/pack/measure.py +13 -0
  26. package/atris/experiments/_template/pack/program.md +3 -0
  27. package/atris/experiments/_template/pack/reset.py +3 -0
  28. package/atris/experiments/_template/pack/results.tsv +1 -0
  29. package/atris/experiments/benchmark_runtime.py +81 -0
  30. package/atris/experiments/benchmark_validate.py +70 -0
  31. package/atris/experiments/validate.py +92 -0
  32. package/atris/policies/atris-design.md +66 -0
  33. package/atris/skills/README.md +1 -0
  34. package/atris/skills/apps/SKILL.md +243 -0
  35. package/atris/skills/autoresearch/SKILL.md +63 -0
  36. package/atris/skills/create-app/SKILL.md +6 -0
  37. package/atris/skills/design/SKILL.md +15 -1
  38. package/atris/skills/drive/SKILL.md +335 -20
  39. package/atris/skills/ramp/SKILL.md +295 -0
  40. package/bin/atris.js +76 -5
  41. package/commands/business.js +132 -0
  42. package/commands/clean.js +113 -70
  43. package/commands/console.js +397 -0
  44. package/commands/experiments.js +216 -0
  45. package/commands/init.js +4 -0
  46. package/commands/pull.js +311 -0
  47. package/commands/push.js +170 -0
  48. package/commands/run.js +366 -0
  49. package/commands/status.js +21 -1
  50. package/package.json +2 -1
@@ -0,0 +1,397 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { spawn, spawnSync } = require('child_process');
5
+ const readline = require('readline');
6
+
7
+ // ── Context Gathering ──────────────────────────────────────────────
8
+
9
+ function gatherAtrisContext(workspaceDir) {
10
+ const atrisDir = path.join(workspaceDir, 'atris');
11
+ const ctx = {
12
+ hasAtris: fs.existsSync(atrisDir),
13
+ skills: [],
14
+ teamMembers: [],
15
+ backlogCount: 0,
16
+ todayJournal: null,
17
+ persona: null,
18
+ };
19
+
20
+ if (!ctx.hasAtris) return ctx;
21
+
22
+ // Skills — scan atris/skills/ and .claude/skills/
23
+ for (const skillsRoot of [
24
+ path.join(atrisDir, 'skills'),
25
+ path.join(workspaceDir, '.claude', 'skills'),
26
+ ]) {
27
+ if (!fs.existsSync(skillsRoot)) continue;
28
+ for (const name of fs.readdirSync(skillsRoot)) {
29
+ const skillMd = path.join(skillsRoot, name, 'SKILL.md');
30
+ // Resolve symlinks
31
+ let resolvedPath = skillMd;
32
+ try {
33
+ const fullDir = path.join(skillsRoot, name);
34
+ const stat = fs.lstatSync(fullDir);
35
+ if (stat.isSymbolicLink()) {
36
+ resolvedPath = path.join(fs.realpathSync(fullDir), 'SKILL.md');
37
+ }
38
+ } catch {}
39
+ if (!fs.existsSync(resolvedPath)) continue;
40
+
41
+ // Extract name + description from frontmatter
42
+ try {
43
+ const raw = fs.readFileSync(resolvedPath, 'utf8');
44
+ const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
45
+ let desc = name;
46
+ if (fmMatch) {
47
+ const descLine = fmMatch[1].match(/description:\s*(.+)/);
48
+ if (descLine) desc = descLine[1].trim();
49
+ }
50
+ ctx.skills.push({ name, description: desc, path: resolvedPath });
51
+ } catch {
52
+ ctx.skills.push({ name, description: name, path: resolvedPath });
53
+ }
54
+ }
55
+ }
56
+ // Dedupe by name
57
+ const seen = new Set();
58
+ ctx.skills = ctx.skills.filter(s => {
59
+ if (seen.has(s.name)) return false;
60
+ seen.add(s.name);
61
+ return true;
62
+ });
63
+
64
+ // Team members — scan atris/team/
65
+ const teamDir = path.join(atrisDir, 'team');
66
+ if (fs.existsSync(teamDir)) {
67
+ for (const name of fs.readdirSync(teamDir)) {
68
+ if (name.startsWith('_')) continue;
69
+ const memberMd = path.join(teamDir, name, 'MEMBER.md');
70
+ if (fs.existsSync(memberMd)) {
71
+ ctx.teamMembers.push(name);
72
+ }
73
+ }
74
+ }
75
+
76
+ // Backlog count from TODO.md
77
+ const todoFile = path.join(atrisDir, 'TODO.md');
78
+ if (fs.existsSync(todoFile)) {
79
+ const content = fs.readFileSync(todoFile, 'utf8');
80
+ const backlogMatch = content.match(/## Backlog\n([\s\S]*?)(?=\n## |$)/);
81
+ if (backlogMatch) {
82
+ ctx.backlogCount = (backlogMatch[1].match(/^- /gm) || []).length;
83
+ }
84
+ }
85
+
86
+ // Today's journal
87
+ const now = new Date();
88
+ const y = now.getFullYear();
89
+ const m = String(now.getMonth() + 1).padStart(2, '0');
90
+ const d = String(now.getDate()).padStart(2, '0');
91
+ const journalPath = path.join(atrisDir, 'logs', String(y), `${y}-${m}-${d}.md`);
92
+ if (fs.existsSync(journalPath)) {
93
+ ctx.todayJournal = journalPath;
94
+ }
95
+
96
+ // Persona
97
+ const personaPath = path.join(atrisDir, 'PERSONA.md');
98
+ if (fs.existsSync(personaPath)) {
99
+ ctx.persona = fs.readFileSync(personaPath, 'utf8').slice(0, 2000);
100
+ }
101
+
102
+ return ctx;
103
+ }
104
+
105
+ function buildSystemPrompt(ctx) {
106
+ const lines = [];
107
+
108
+ lines.push('# Atris Console');
109
+ lines.push('You are running inside the Atris Console — an AI workspace operating system.');
110
+ lines.push('');
111
+
112
+ if (ctx.persona) {
113
+ lines.push('## Persona');
114
+ lines.push(ctx.persona);
115
+ lines.push('');
116
+ }
117
+
118
+ if (ctx.teamMembers.length > 0) {
119
+ lines.push('## Team Members');
120
+ lines.push(`Available: ${ctx.teamMembers.join(', ')}`);
121
+ lines.push('Each has a MEMBER.md in atris/team/<name>/ defining their role.');
122
+ lines.push('');
123
+ }
124
+
125
+ if (ctx.skills.length > 0) {
126
+ lines.push('## Skills');
127
+ lines.push('Before replying, scan these skills. If one applies, read its SKILL.md then follow it.');
128
+ lines.push('');
129
+ for (const s of ctx.skills) {
130
+ lines.push(`- **${s.name}**: ${s.description} (${s.path})`);
131
+ }
132
+ lines.push('');
133
+ }
134
+
135
+ if (ctx.backlogCount > 0) {
136
+ lines.push(`## Current Work`);
137
+ lines.push(`${ctx.backlogCount} task${ctx.backlogCount > 1 ? 's' : ''} in backlog. Check atris/TODO.md for details.`);
138
+ lines.push('');
139
+ }
140
+
141
+ if (ctx.todayJournal) {
142
+ lines.push(`## Journal`);
143
+ lines.push(`Today's journal: ${ctx.todayJournal}`);
144
+ lines.push('');
145
+ }
146
+
147
+ return lines.join('\n');
148
+ }
149
+
150
+ // ── TUI Rendering ──────────────────────────────────────────────────
151
+
152
+ const COLORS = {
153
+ reset: '\x1b[0m',
154
+ bold: '\x1b[1m',
155
+ dim: '\x1b[2m',
156
+ cyan: '\x1b[36m',
157
+ green: '\x1b[32m',
158
+ yellow: '\x1b[33m',
159
+ magenta: '\x1b[35m',
160
+ white: '\x1b[37m',
161
+ bgBlack: '\x1b[40m',
162
+ gray: '\x1b[90m',
163
+ };
164
+
165
+ function renderHeader(ctx, backend) {
166
+ const c = COLORS;
167
+ const width = Math.min(process.stdout.columns || 60, 70);
168
+ const line = '─'.repeat(width);
169
+
170
+ console.log('');
171
+ console.log(`${c.cyan}${c.bold} ┌${line}┐${c.reset}`);
172
+ console.log(`${c.cyan}${c.bold} │${''.padEnd(width)}│${c.reset}`);
173
+
174
+ // Title
175
+ const title = 'A T R I S C O N S O L E';
176
+ const pad = Math.max(0, Math.floor((width - title.length) / 2));
177
+ console.log(`${c.cyan}${c.bold} │${' '.repeat(pad)}${c.white}${title}${' '.repeat(width - pad - title.length)}${c.cyan}│${c.reset}`);
178
+
179
+ console.log(`${c.cyan}${c.bold} │${''.padEnd(width)}│${c.reset}`);
180
+ console.log(`${c.cyan} ├${line}┤${c.reset}`);
181
+
182
+ // Status row
183
+ const engine = `Engine: ${backend}`;
184
+ const skills = `Skills: ${ctx.skills.length}`;
185
+ const team = `Team: ${ctx.teamMembers.length}`;
186
+ const tasks = `Tasks: ${ctx.backlogCount}`;
187
+ const statusItems = [engine, skills, team, tasks].join(' │ ');
188
+ const statusPad = Math.max(0, Math.floor((width - statusItems.length) / 2));
189
+ console.log(`${c.cyan} │${c.reset}${' '.repeat(statusPad)}${c.dim}${statusItems}${' '.repeat(Math.max(0, width - statusPad - statusItems.length))}${c.cyan}│${c.reset}`);
190
+
191
+ console.log(`${c.cyan} └${line}┘${c.reset}`);
192
+ console.log('');
193
+ }
194
+
195
+ function renderSkillsBar(ctx) {
196
+ if (ctx.skills.length === 0) return;
197
+ const c = COLORS;
198
+ const names = ctx.skills.map(s => s.name).slice(0, 8);
199
+ console.log(`${c.dim} Skills: ${names.map(n => `${c.magenta}/${n}${c.dim}`).join(' ')}${c.reset}`);
200
+ if (ctx.skills.length > 8) {
201
+ console.log(`${c.dim} ... and ${ctx.skills.length - 8} more${c.reset}`);
202
+ }
203
+ console.log('');
204
+ }
205
+
206
+ // ── Backend Detection & Auth ───────────────────────────────────────
207
+
208
+ function detectBackend(requested) {
209
+ const hasClaude = spawnSync('which', ['claude'], { stdio: 'pipe' }).status === 0;
210
+ const hasCodex = spawnSync('which', ['codex'], { stdio: 'pipe' }).status === 0;
211
+
212
+ if (requested) {
213
+ const installed = requested === 'claude' ? hasClaude : hasCodex;
214
+ if (!installed) return { backend: requested, installed: false, hasClaude, hasCodex };
215
+ return { backend: requested, installed: true, hasClaude, hasCodex };
216
+ }
217
+
218
+ if (hasClaude) return { backend: 'claude', installed: true, hasClaude, hasCodex };
219
+ if (hasCodex) return { backend: 'codex', installed: true, hasClaude, hasCodex };
220
+ return { backend: null, installed: false, hasClaude, hasCodex };
221
+ }
222
+
223
+ function offerInstall(target, callback) {
224
+ const pkg = target === 'claude'
225
+ ? '@anthropic-ai/claude-code'
226
+ : '@openai/codex';
227
+
228
+ console.log(` ${target} is not installed.\n`);
229
+ if (target === 'claude') {
230
+ console.log(' Install: npm install -g @anthropic-ai/claude-code');
231
+ console.log(' Auth: claude (follow login prompts)\n');
232
+ } else {
233
+ console.log(' Install: npm install -g @openai/codex');
234
+ console.log(' Auth: export OPENAI_API_KEY=sk-...\n');
235
+ }
236
+
237
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
238
+ rl.question(` Install ${target} now? (y/N) `, (answer) => {
239
+ rl.close();
240
+ if (answer.trim().toLowerCase() === 'y') {
241
+ console.log(`\n Installing ${pkg}...\n`);
242
+ const install = spawnSync('npm', ['install', '-g', pkg], {
243
+ stdio: 'inherit',
244
+ env: process.env,
245
+ });
246
+ if (install.status !== 0) {
247
+ console.error(`\n✗ Install failed. Try: npm install -g ${pkg}`);
248
+ process.exit(1);
249
+ }
250
+ console.log(`\n✓ ${target} installed.\n`);
251
+ callback();
252
+ } else {
253
+ process.exit(0);
254
+ }
255
+ });
256
+ }
257
+
258
+ function checkAuth(backend) {
259
+ if (backend === 'claude') {
260
+ // Claude handles its own auth interactively — always allow
261
+ return true;
262
+ }
263
+ if (backend === 'codex') {
264
+ if (!process.env.OPENAI_API_KEY) {
265
+ console.error('\n ✗ OPENAI_API_KEY not set.\n');
266
+ console.error(' Codex requires an OpenAI API key:');
267
+ console.error(' export OPENAI_API_KEY=sk-...\n');
268
+ console.error(' Get one at: https://platform.openai.com/api-keys\n');
269
+ process.exit(1);
270
+ return false;
271
+ }
272
+ return true;
273
+ }
274
+ return true;
275
+ }
276
+
277
+ // ── Launch ──────────────────────────────────────────────────────────
278
+
279
+ function launchClaude(systemPrompt, extraArgs) {
280
+ const args = [
281
+ '--dangerously-skip-permissions',
282
+ '--append-system-prompt', systemPrompt,
283
+ ...extraArgs,
284
+ ];
285
+
286
+ const child = spawnSync('claude', args, {
287
+ cwd: process.cwd(),
288
+ stdio: 'inherit',
289
+ env: { ...process.env, CLAUDECODE: undefined },
290
+ });
291
+
292
+ if (child.error) {
293
+ console.error(`✗ Failed to start claude: ${child.error.message}`);
294
+ process.exit(1);
295
+ }
296
+ process.exit(child.status ?? 0);
297
+ }
298
+
299
+ function launchCodex(systemPrompt, extraArgs) {
300
+ // Codex uses ~/.codex/instructions.md for system instructions
301
+ // Write a temporary instructions file and point to it
302
+ const codexDir = path.join(os.homedir(), '.codex');
303
+ const instructionsPath = path.join(codexDir, 'instructions.md');
304
+
305
+ // Preserve existing instructions
306
+ let existingInstructions = '';
307
+ if (fs.existsSync(instructionsPath)) {
308
+ existingInstructions = fs.readFileSync(instructionsPath, 'utf8');
309
+ }
310
+
311
+ // Prepend Atris context
312
+ const combined = systemPrompt + '\n\n---\n\n' + existingInstructions;
313
+
314
+ // Write combined, launch, restore
315
+ if (!fs.existsSync(codexDir)) fs.mkdirSync(codexDir, { recursive: true });
316
+ fs.writeFileSync(instructionsPath, combined, 'utf8');
317
+
318
+ const child = spawnSync('codex', ['--full-auto', ...extraArgs], {
319
+ cwd: process.cwd(),
320
+ stdio: 'inherit',
321
+ env: process.env,
322
+ });
323
+
324
+ // Restore original
325
+ if (existingInstructions) {
326
+ fs.writeFileSync(instructionsPath, existingInstructions, 'utf8');
327
+ } else {
328
+ try { fs.unlinkSync(instructionsPath); } catch {}
329
+ }
330
+
331
+ if (child.error) {
332
+ console.error(`✗ Failed to start codex: ${child.error.message}`);
333
+ process.exit(1);
334
+ }
335
+ process.exit(child.status ?? 0);
336
+ }
337
+
338
+ // ── Main ────────────────────────────────────────────────────────────
339
+
340
+ function consoleCommand() {
341
+ const args = process.argv.slice(3);
342
+ let requested = args[0];
343
+
344
+ if (requested === '--help' || requested === 'help') {
345
+ console.log('Usage: atris console [claude|codex] [...args]\n');
346
+ console.log('Launch a coding agent wrapped in Atris context.\n');
347
+ console.log('The console injects your team, skills, tasks, and persona');
348
+ console.log('into the agent session. Everything Atris knows, it knows.\n');
349
+ console.log('Examples:');
350
+ console.log(' atris console Auto-detect and launch');
351
+ console.log(' atris console claude Launch with Claude Code');
352
+ console.log(' atris console codex Launch with Codex');
353
+ process.exit(0);
354
+ }
355
+
356
+ if (requested && !['claude', 'codex'].includes(requested)) {
357
+ console.error(`✗ Unknown backend: ${requested}`);
358
+ console.error(' Usage: atris console [claude|codex]');
359
+ process.exit(1);
360
+ }
361
+
362
+ const detection = detectBackend(requested);
363
+
364
+ function boot() {
365
+ const backend = detection.backend || 'claude';
366
+ checkAuth(backend);
367
+
368
+ // Gather Atris context
369
+ const ctx = gatherAtrisContext(process.cwd());
370
+
371
+ // Render TUI header
372
+ renderHeader(ctx, backend);
373
+ renderSkillsBar(ctx);
374
+
375
+ // Build system prompt
376
+ const systemPrompt = buildSystemPrompt(ctx);
377
+
378
+ // Extra args (skip backend name if it was first arg)
379
+ const extraArgs = requested === args[0] ? args.slice(1) : args;
380
+
381
+ // Launch
382
+ if (backend === 'claude') {
383
+ launchClaude(systemPrompt, extraArgs);
384
+ } else {
385
+ launchCodex(systemPrompt, extraArgs);
386
+ }
387
+ }
388
+
389
+ if (!detection.installed) {
390
+ const target = detection.backend || 'claude';
391
+ offerInstall(target, boot);
392
+ } else {
393
+ boot();
394
+ }
395
+ }
396
+
397
+ module.exports = { consoleCommand, gatherAtrisContext, buildSystemPrompt };
@@ -0,0 +1,216 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawnSync } = require('child_process');
4
+
5
+ const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
6
+ const ROOT_FILES = ['README.md', 'validate.py', 'benchmark_validate.py', 'benchmark_runtime.py'];
7
+ const SUPPORT_DIRS = ['_fixtures', '_template', '_examples'];
8
+
9
+ function ensureAtrisWorkspace(workspaceDir = process.cwd()) {
10
+ const atrisDir = path.join(workspaceDir, 'atris');
11
+ if (!fs.existsSync(atrisDir)) {
12
+ console.error('✗ Error: atris/ folder not found. Run "atris init" first.');
13
+ process.exit(1);
14
+ }
15
+ return atrisDir;
16
+ }
17
+
18
+ function copyRecursive(src, dest) {
19
+ fs.mkdirSync(dest, { recursive: true });
20
+ const entries = fs.readdirSync(src, { withFileTypes: true });
21
+
22
+ for (const entry of entries) {
23
+ if (entry.name === '.DS_Store' || entry.name === '__pycache__') continue;
24
+
25
+ const srcPath = path.join(src, entry.name);
26
+ const destPath = path.join(dest, entry.name);
27
+
28
+ if (entry.isDirectory()) {
29
+ copyRecursive(srcPath, destPath);
30
+ continue;
31
+ }
32
+
33
+ if (!fs.existsSync(destPath)) {
34
+ fs.copyFileSync(srcPath, destPath);
35
+ }
36
+ }
37
+ }
38
+
39
+ function ensureExperimentsFramework(workspaceDir = process.cwd(), { silent = false } = {}) {
40
+ const atrisDir = ensureAtrisWorkspace(workspaceDir);
41
+ const packageExperimentsDir = path.join(__dirname, '..', 'atris', 'experiments');
42
+ const experimentsDir = path.join(atrisDir, 'experiments');
43
+ const created = [];
44
+
45
+ if (!fs.existsSync(experimentsDir)) {
46
+ fs.mkdirSync(experimentsDir, { recursive: true });
47
+ created.push('atris/experiments/');
48
+ }
49
+
50
+ for (const file of ROOT_FILES) {
51
+ const src = path.join(packageExperimentsDir, file);
52
+ const dest = path.join(experimentsDir, file);
53
+ if (!fs.existsSync(dest) && fs.existsSync(src)) {
54
+ fs.copyFileSync(src, dest);
55
+ created.push(`atris/experiments/${file}`);
56
+ }
57
+ }
58
+
59
+ for (const dirName of SUPPORT_DIRS) {
60
+ const src = path.join(packageExperimentsDir, dirName);
61
+ const dest = path.join(experimentsDir, dirName);
62
+ if (fs.existsSync(src)) {
63
+ const hadDest = fs.existsSync(dest);
64
+ copyRecursive(src, dest);
65
+ if (!hadDest) {
66
+ created.push(`atris/experiments/${dirName}/`);
67
+ }
68
+ }
69
+ }
70
+
71
+ if (!silent) {
72
+ if (created.length > 0) {
73
+ console.log(`✓ Prepared atris/experiments/ (${created.length} item${created.length === 1 ? '' : 's'})`);
74
+ } else {
75
+ console.log('✓ atris/experiments/ already ready');
76
+ }
77
+ }
78
+
79
+ return { atrisDir, experimentsDir, created };
80
+ }
81
+
82
+ function resolvePython() {
83
+ const candidates = [
84
+ process.env.ATRIS_EXPERIMENTS_PYTHON,
85
+ 'python3',
86
+ 'python',
87
+ ].filter(Boolean);
88
+
89
+ for (const candidate of candidates) {
90
+ const probe = spawnSync(candidate, ['--version'], { encoding: 'utf8' });
91
+ if (!probe.error && probe.status === 0) {
92
+ return candidate;
93
+ }
94
+ }
95
+
96
+ return null;
97
+ }
98
+
99
+ function runPython(scriptPath, args = [], cwd = process.cwd()) {
100
+ const python = resolvePython();
101
+ if (!python) {
102
+ console.error('✗ Error: Python not found. Set ATRIS_EXPERIMENTS_PYTHON or install python3.');
103
+ process.exit(1);
104
+ }
105
+
106
+ const result = spawnSync(python, [scriptPath, ...args], {
107
+ cwd,
108
+ stdio: 'inherit',
109
+ env: {
110
+ ...process.env,
111
+ PYTHONDONTWRITEBYTECODE: '1',
112
+ },
113
+ });
114
+
115
+ if (result.error) {
116
+ throw result.error;
117
+ }
118
+
119
+ if (typeof result.status === 'number' && result.status !== 0) {
120
+ process.exit(result.status);
121
+ }
122
+ }
123
+
124
+ function experimentsInit(name) {
125
+ const { experimentsDir } = ensureExperimentsFramework();
126
+
127
+ if (!name) {
128
+ console.log('');
129
+ console.log('Experiments framework ready.');
130
+ console.log('Next: atris experiments init <slug>');
131
+ console.log('');
132
+ return;
133
+ }
134
+
135
+ if (!SLUG_RE.test(name)) {
136
+ console.error('✗ Invalid experiment name. Use lowercase-hyphen slug, for example: self-heal');
137
+ process.exit(1);
138
+ }
139
+
140
+ const targetDir = path.join(experimentsDir, name);
141
+ if (fs.existsSync(targetDir)) {
142
+ console.error(`✗ Experiment "${name}" already exists at atris/experiments/${name}/`);
143
+ process.exit(1);
144
+ }
145
+
146
+ const templateDir = path.join(experimentsDir, '_template', 'pack');
147
+ copyRecursive(templateDir, targetDir);
148
+
149
+ console.log(`✓ Created atris/experiments/${name}/`);
150
+ console.log(' Files: program.md, measure.py, loop.py, results.tsv, reset.py');
151
+ }
152
+
153
+ function experimentsValidate(rootArg) {
154
+ const { experimentsDir } = ensureExperimentsFramework();
155
+ const args = [];
156
+
157
+ if (rootArg) {
158
+ args.push(rootArg);
159
+ }
160
+
161
+ runPython(path.join(experimentsDir, 'validate.py'), args, experimentsDir);
162
+ }
163
+
164
+ function experimentsBenchmark(kind = 'all') {
165
+ const { experimentsDir } = ensureExperimentsFramework();
166
+ const modes = kind === 'all' ? ['validate', 'runtime'] : [kind];
167
+
168
+ for (const mode of modes) {
169
+ if (mode === 'validate') {
170
+ console.log('Running experiment validator benchmark...');
171
+ runPython(path.join(experimentsDir, 'benchmark_validate.py'), [], experimentsDir);
172
+ continue;
173
+ }
174
+
175
+ if (mode === 'runtime') {
176
+ console.log('Running experiment runtime benchmark...');
177
+ runPython(path.join(experimentsDir, 'benchmark_runtime.py'), [], experimentsDir);
178
+ continue;
179
+ }
180
+
181
+ console.error('Usage: atris experiments benchmark [validate|runtime|all]');
182
+ process.exit(1);
183
+ }
184
+ }
185
+
186
+ function experimentsCommand(subcommand, ...args) {
187
+ switch (subcommand) {
188
+ case 'init':
189
+ case 'new':
190
+ return experimentsInit(args[0]);
191
+ case 'validate':
192
+ return experimentsValidate(args[0]);
193
+ case 'benchmark':
194
+ return experimentsBenchmark(args[0] || 'all');
195
+ default:
196
+ console.log('');
197
+ console.log('Usage: atris experiments <subcommand> [name]');
198
+ console.log('');
199
+ console.log('Subcommands:');
200
+ console.log(' init [slug] Prepare atris/experiments/ or scaffold a new pack');
201
+ console.log(' validate [path|slug] Run structural validation on packs or a single pack');
202
+ console.log(' benchmark [mode] Run validate/runtime/all benchmark harness');
203
+ console.log('');
204
+ console.log('Examples:');
205
+ console.log(' atris experiments init');
206
+ console.log(' atris experiments init self-heal');
207
+ console.log(' atris experiments validate');
208
+ console.log(' atris experiments benchmark runtime');
209
+ console.log('');
210
+ }
211
+ }
212
+
213
+ module.exports = {
214
+ experimentsCommand,
215
+ ensureExperimentsFramework,
216
+ };
package/commands/init.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const { ensureExperimentsFramework } = require('./experiments');
3
4
 
4
5
  /**
5
6
  * Detect project context by scanning project structure
@@ -474,6 +475,9 @@ function initAtris() {
474
475
  console.log(`✓ Created features/_templates/${name} (fallback)`);
475
476
  });
476
477
 
478
+ // Create experiments directory and packaged validation harness
479
+ ensureExperimentsFramework(process.cwd(), { silent: false });
480
+
477
481
 
478
482
  // Copy team members (MEMBER.md format — directory per member with skills/tools/context)
479
483
  const members = ['navigator', 'executor', 'validator', 'launcher', 'brainstormer', 'researcher'];