atris 3.1.0 → 3.5.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 (54) hide show
  1. package/GETTING_STARTED.md +65 -131
  2. package/README.md +29 -4
  3. package/atris/GETTING_STARTED.md +65 -131
  4. package/atris/PERSONA.md +5 -1
  5. package/atris/atris.md +122 -153
  6. package/atris/skills/aeo/SKILL.md +117 -0
  7. package/atris/skills/atris/SKILL.md +49 -25
  8. package/atris/skills/create-member/SKILL.md +29 -9
  9. package/atris/skills/endgame/SKILL.md +9 -0
  10. package/atris/skills/improve/SKILL.md +2 -2
  11. package/atris/skills/research-search/SKILL.md +167 -0
  12. package/atris/skills/research-search/arxiv_search.py +157 -0
  13. package/atris/skills/research-search/program.md +48 -0
  14. package/atris/skills/research-search/results.tsv +6 -0
  15. package/atris/skills/research-search/scholar_search.py +154 -0
  16. package/atris/skills/tidy/SKILL.md +36 -21
  17. package/atris/team/_template/MEMBER.md +2 -0
  18. package/atris/team/validator/MEMBER.md +35 -1
  19. package/atris.md +118 -178
  20. package/bin/atris.js +37 -6
  21. package/cli/__pycache__/atris_code.cpython-314.pyc +0 -0
  22. package/cli/__pycache__/runtime_guard.cpython-312.pyc +0 -0
  23. package/cli/__pycache__/runtime_guard.cpython-314.pyc +0 -0
  24. package/cli/atris_code.py +889 -0
  25. package/cli/runtime_guard.py +693 -0
  26. package/commands/align.js +15 -0
  27. package/commands/app.js +316 -0
  28. package/commands/autopilot.js +948 -42
  29. package/commands/business.js +691 -11
  30. package/commands/computer.js +1979 -43
  31. package/commands/context-sync.js +5 -0
  32. package/commands/experiments.js +1 -1
  33. package/commands/lifecycle.js +12 -0
  34. package/commands/plugin.js +24 -0
  35. package/commands/pull.js +40 -1
  36. package/commands/push.js +44 -0
  37. package/commands/release.js +183 -0
  38. package/commands/research.js +52 -0
  39. package/commands/serve.js +1 -0
  40. package/commands/sync.js +372 -87
  41. package/commands/verify.js +53 -4
  42. package/commands/wiki.js +71 -26
  43. package/lib/file-ops.js +13 -1
  44. package/lib/journal.js +23 -0
  45. package/lib/reward-config.js +24 -0
  46. package/lib/scorecard.js +58 -6
  47. package/lib/sync-telemetry.js +59 -0
  48. package/lib/todo.js +6 -0
  49. package/lib/wiki.js +235 -60
  50. package/package.json +4 -2
  51. package/utils/api.js +19 -0
  52. package/utils/auth.js +25 -1
  53. package/utils/config.js +24 -0
  54. package/utils/update-check.js +16 -0
@@ -3,7 +3,8 @@ const path = require('path');
3
3
  const os = require('os');
4
4
  const { loadCredentials } = require('../utils/auth');
5
5
  const { apiRequestJson } = require('../utils/api');
6
- const { syncBusinessCanonical } = require('./sync');
6
+ const { syncBusinessCanonical, ensureWorkspaceStateFiles } = require('./sync');
7
+ const { ensureContextScaffold, writeWikiStatus, appendWikiLog } = require('../lib/wiki');
7
8
 
8
9
  function getBusinessConfigPath() {
9
10
  const home = require('os').homedir();
@@ -88,6 +89,7 @@ function createCanonicalBusinessWorkspace(targetRoot, bizMeta, options = {}) {
88
89
  throw new Error(`Target already contains .atris/business.json: ${targetRoot}`);
89
90
  }
90
91
 
92
+ const workspaceTemplate = options.templateName || bizMeta.workspace_template || 'business';
91
93
  fs.mkdirSync(atrisMetaDir, { recursive: true });
92
94
  fs.writeFileSync(businessJsonPath, JSON.stringify({
93
95
  business_id: bizMeta.business_id,
@@ -95,11 +97,669 @@ function createCanonicalBusinessWorkspace(targetRoot, bizMeta, options = {}) {
95
97
  name: bizMeta.name,
96
98
  slug: bizMeta.slug,
97
99
  owner_email: bizMeta.owner_email || '',
100
+ workspace_template: workspaceTemplate,
98
101
  created_at: new Date().toISOString(),
99
102
  }, null, 2));
100
103
 
101
- syncBusinessCanonical(targetRoot, bizMeta, { force: false, dryRun: false });
102
- return { targetRoot, businessJsonPath };
104
+ syncBusinessCanonical(targetRoot, bizMeta, { force: false, dryRun: false, templateName: workspaceTemplate });
105
+ return { targetRoot, businessJsonPath, workspaceTemplate };
106
+ }
107
+
108
+ function parseRecordFlags(args, cwd = process.cwd()) {
109
+ const options = {
110
+ cwd,
111
+ reportPath: null,
112
+ summary: '',
113
+ metric: '',
114
+ outcome: 'recorded',
115
+ reward: null,
116
+ loop: 'manual',
117
+ actor: 'operator',
118
+ };
119
+
120
+ for (let i = 0; i < args.length; i++) {
121
+ const arg = args[i];
122
+ const next = args[i + 1];
123
+
124
+ if ((arg === '--summary' || arg === '-s') && next) {
125
+ options.summary = next;
126
+ i++;
127
+ } else if ((arg === '--metric' || arg === '-m') && next) {
128
+ options.metric = next;
129
+ i++;
130
+ } else if ((arg === '--outcome' || arg === '-o') && next) {
131
+ options.outcome = next;
132
+ i++;
133
+ } else if ((arg === '--reward' || arg === '-r') && next) {
134
+ options.reward = next;
135
+ i++;
136
+ } else if (arg === '--loop' && next) {
137
+ options.loop = next;
138
+ i++;
139
+ } else if (arg === '--actor' && next) {
140
+ options.actor = next;
141
+ i++;
142
+ } else if (!arg.startsWith('-') && !options.reportPath) {
143
+ options.reportPath = arg;
144
+ }
145
+ }
146
+
147
+ return options;
148
+ }
149
+
150
+ function parseOnboardFlags(args, cwd = process.cwd()) {
151
+ const options = {
152
+ cwd,
153
+ name: '',
154
+ website: '',
155
+ links: [],
156
+ notes: [],
157
+ sources: [],
158
+ contactName: '',
159
+ contactEmail: '',
160
+ contactRole: '',
161
+ };
162
+ const freeform = [];
163
+
164
+ for (let i = 0; i < args.length; i++) {
165
+ const arg = args[i];
166
+ const next = args[i + 1];
167
+
168
+ if ((arg === '--name' || arg === '--business') && next) {
169
+ options.name = next;
170
+ i++;
171
+ } else if ((arg === '--website' || arg === '--site') && next) {
172
+ options.website = next;
173
+ i++;
174
+ } else if ((arg === '--link' || arg === '--url') && next) {
175
+ options.links.push(next);
176
+ i++;
177
+ } else if ((arg === '--from' || arg === '--source') && next) {
178
+ options.sources.push(next);
179
+ i++;
180
+ } else if ((arg === '--note' || arg === '--notes') && next) {
181
+ options.notes.push(next);
182
+ i++;
183
+ } else if ((arg === '--contact' || arg === '--person') && next) {
184
+ options.contactName = next;
185
+ i++;
186
+ } else if (arg === '--email' && next) {
187
+ options.contactEmail = next;
188
+ i++;
189
+ } else if (arg === '--role' && next) {
190
+ options.contactRole = next;
191
+ i++;
192
+ } else if (!arg.startsWith('-')) {
193
+ const resolved = path.resolve(cwd, arg);
194
+ if (/^https?:\/\//i.test(arg)) {
195
+ if (!options.website) options.website = arg;
196
+ else options.links.push(arg);
197
+ } else if (fs.existsSync(resolved)) {
198
+ options.sources.push(arg);
199
+ } else {
200
+ freeform.push(arg);
201
+ }
202
+ }
203
+ }
204
+
205
+ if (freeform.length > 0) {
206
+ options.notes.push(freeform.join(' '));
207
+ }
208
+
209
+ return options;
210
+ }
211
+
212
+ function readWorkspaceBusinessMeta(cwd = process.cwd()) {
213
+ const bizFile = path.join(cwd, '.atris', 'business.json');
214
+ if (!fs.existsSync(bizFile)) {
215
+ throw new Error('Run this command inside a business environment with .atris/business.json.');
216
+ }
217
+ try {
218
+ return JSON.parse(fs.readFileSync(bizFile, 'utf8'));
219
+ } catch (error) {
220
+ throw new Error(`Failed to read .atris/business.json: ${error.message}`);
221
+ }
222
+ }
223
+
224
+ function resolveWorkspaceReport(cwd, reportPath) {
225
+ if (!reportPath) {
226
+ throw new Error('Usage: atris business record <report-path> [--summary "text"] [--metric name] [--outcome positive|mixed|negative] [--reward N]');
227
+ }
228
+ const absPath = path.resolve(cwd, reportPath);
229
+ if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
230
+ throw new Error(`Report not found: ${reportPath}`);
231
+ }
232
+ const relPath = path.relative(cwd, absPath).replace(/\\/g, '/');
233
+ return { absPath, relPath };
234
+ }
235
+
236
+ function extractReportTitle(content, absPath) {
237
+ const heading = String(content || '').match(/^#\s+(.+)$/m);
238
+ if (heading) return heading[1].trim();
239
+ return path.basename(absPath, path.extname(absPath));
240
+ }
241
+
242
+ function normalizeOutcome(value) {
243
+ const normalized = String(value || 'recorded').trim().toLowerCase();
244
+ if (['positive', 'win', 'success', 'improved'].includes(normalized)) return 'positive';
245
+ if (['negative', 'loss', 'failed', 'regressed'].includes(normalized)) return 'negative';
246
+ if (['mixed', 'partial', 'unclear'].includes(normalized)) return 'mixed';
247
+ return 'recorded';
248
+ }
249
+
250
+ function defaultRewardForOutcome(outcome) {
251
+ if (outcome === 'positive') return 5;
252
+ if (outcome === 'negative') return -3;
253
+ if (outcome === 'mixed') return 1;
254
+ return 0;
255
+ }
256
+
257
+ function appendJsonl(filePath, record) {
258
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
259
+ fs.appendFileSync(filePath, `${JSON.stringify(record)}\n`, 'utf8');
260
+ }
261
+
262
+ function slugifyName(value) {
263
+ return String(value || '')
264
+ .toLowerCase()
265
+ .replace(/[^a-z0-9]+/g, '-')
266
+ .replace(/^-+|-+$/g, '') || 'item';
267
+ }
268
+
269
+ function upsertIndexEntry(indexPath, sectionName, relativePath, description) {
270
+ const normalizedPath = relativePath.replace(/\\/g, '/');
271
+ const entryLine = `- [[${normalizedPath}]] - ${description}`;
272
+ let lines = fs.readFileSync(indexPath, 'utf8').split('\n');
273
+ const existingIndex = lines.findIndex((line) => line.includes(`[[${normalizedPath}]]`));
274
+ if (existingIndex >= 0) {
275
+ lines[existingIndex] = entryLine;
276
+ fs.writeFileSync(indexPath, `${lines.join('\n').replace(/\n*$/, '\n')}`, 'utf8');
277
+ return;
278
+ }
279
+
280
+ const header = `## ${sectionName}`;
281
+ const sectionIndex = lines.findIndex((line) => line.trim() === header);
282
+ if (sectionIndex === -1) return;
283
+
284
+ let insertAt = sectionIndex + 1;
285
+ while (insertAt < lines.length && !/^##\s+/.test(lines[insertAt])) {
286
+ insertAt++;
287
+ }
288
+
289
+ lines.splice(insertAt, 0, entryLine);
290
+ fs.writeFileSync(indexPath, `${lines.join('\n').replace(/\n*$/, '\n')}`, 'utf8');
291
+ }
292
+
293
+ function writeMarkdownWithFrontmatter(filePath, frontmatter, body) {
294
+ const yaml = Object.entries(frontmatter).map(([key, value]) => {
295
+ if (Array.isArray(value)) {
296
+ return `${key}:\n${value.map((item) => ` - ${item}`).join('\n')}`;
297
+ }
298
+ return `${key}: ${value}`;
299
+ }).join('\n');
300
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
301
+ fs.writeFileSync(filePath, `---\n${yaml}\n---\n\n${body.trim()}\n`, 'utf8');
302
+ }
303
+
304
+ function walkOnboardingFiles(dir, options = {}) {
305
+ const skipDirs = new Set(['.git', '.atris', 'atris', '_ingest', 'node_modules', 'dist', 'build', 'coverage', '.next']);
306
+ const allowedExt = new Set(['.md', '.txt', '.pdf', '.csv', '.json', '.html', '.htm', '.docx', '.xlsx', '.png', '.jpg', '.jpeg']);
307
+ const maxFiles = options.maxFiles || 25;
308
+ const output = [];
309
+
310
+ function walk(currentDir) {
311
+ if (!fs.existsSync(currentDir) || output.length >= maxFiles) return;
312
+ for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
313
+ if (output.length >= maxFiles) break;
314
+ const fullPath = path.join(currentDir, entry.name);
315
+ if (entry.isDirectory()) {
316
+ if (skipDirs.has(entry.name)) continue;
317
+ walk(fullPath);
318
+ continue;
319
+ }
320
+ if (!entry.isFile()) continue;
321
+ if (entry.name.startsWith('.')) continue;
322
+ if (!allowedExt.has(path.extname(entry.name).toLowerCase())) continue;
323
+ output.push(fullPath);
324
+ }
325
+ }
326
+
327
+ walk(dir);
328
+ return output;
329
+ }
330
+
331
+ function extractUrlsFromText(text) {
332
+ return Array.from(new Set((String(text || '').match(/https?:\/\/[^\s)<>"']+/g) || []).map((item) => item.replace(/[.,]$/, ''))));
333
+ }
334
+
335
+ function isTextLike(filePath) {
336
+ return new Set(['.md', '.txt', '.json', '.csv', '.html', '.htm']).has(path.extname(filePath).toLowerCase());
337
+ }
338
+
339
+ function readSmallText(filePath, maxBytes = 200000) {
340
+ try {
341
+ const stat = fs.statSync(filePath);
342
+ if (stat.size > maxBytes || !isTextLike(filePath)) return null;
343
+ return fs.readFileSync(filePath, 'utf8');
344
+ } catch {
345
+ return null;
346
+ }
347
+ }
348
+
349
+ function discoverOnboardingSignals(cwd, options = {}) {
350
+ const explicitSourcePaths = (options.sources || [])
351
+ .map((value) => path.resolve(cwd, value))
352
+ .filter((fullPath) => fs.existsSync(fullPath));
353
+
354
+ const rootCandidates = walkOnboardingFiles(cwd, { maxFiles: 20 })
355
+ .filter((fullPath) => {
356
+ const relative = path.relative(cwd, fullPath).replace(/\\/g, '/');
357
+ return !relative.startsWith('atris/') && !relative.startsWith('.atris/');
358
+ });
359
+
360
+ const contextDir = path.join(cwd, 'atris', 'context');
361
+ const contextCandidates = walkOnboardingFiles(contextDir, { maxFiles: 20 })
362
+ .filter((fullPath) => {
363
+ const relative = path.relative(contextDir, fullPath).replace(/\\/g, '/');
364
+ return !relative.startsWith('_ingest/') && relative !== 'README.md' && relative !== 'live-workspace.md';
365
+ });
366
+
367
+ const sourcePaths = Array.from(new Set([...explicitSourcePaths, ...rootCandidates, ...contextCandidates]));
368
+ const urls = new Set([options.website, ...(options.links || [])].filter(Boolean));
369
+
370
+ for (const note of options.notes || []) {
371
+ for (const url of extractUrlsFromText(note)) urls.add(url);
372
+ }
373
+
374
+ for (const sourcePath of sourcePaths) {
375
+ const text = readSmallText(sourcePath);
376
+ if (!text) continue;
377
+ for (const url of extractUrlsFromText(text)) urls.add(url);
378
+ }
379
+
380
+ return {
381
+ website: options.website || Array.from(urls)[0] || '',
382
+ urls: Array.from(urls),
383
+ sourcePaths,
384
+ };
385
+ }
386
+
387
+ function stageOnboardingSources(cwd, packDir, sourcePaths = []) {
388
+ const stagedDir = path.join(packDir, 'sources');
389
+ fs.mkdirSync(stagedDir, { recursive: true });
390
+ const stagedEntries = [];
391
+ let counter = 0;
392
+
393
+ for (const sourcePath of sourcePaths) {
394
+ if (!fs.existsSync(sourcePath)) continue;
395
+ counter += 1;
396
+ const baseName = path.basename(sourcePath);
397
+ const targetPath = path.join(stagedDir, `${String(counter).padStart(2, '0')}-${baseName}`);
398
+ const stat = fs.statSync(sourcePath);
399
+ if (stat.isDirectory()) {
400
+ fs.cpSync(sourcePath, targetPath, { recursive: true });
401
+ } else {
402
+ fs.copyFileSync(sourcePath, targetPath);
403
+ }
404
+ stagedEntries.push({
405
+ original: path.relative(cwd, sourcePath).replace(/\\/g, '/'),
406
+ staged: path.relative(cwd, targetPath).replace(/\\/g, '/'),
407
+ kind: stat.isDirectory() ? 'directory' : 'file',
408
+ });
409
+ }
410
+
411
+ return stagedEntries;
412
+ }
413
+
414
+ function suggestStarterAction(signals) {
415
+ if (signals.contactEmail && signals.website) {
416
+ return {
417
+ title: 'Draft a founder-context note',
418
+ action: `Write a short note to ${signals.contactName || 'the contact'} that reflects the website, asks for the current priority, and proposes one concrete first loop.`,
419
+ why: 'This is the shortest safe path to real feedback from a named human.',
420
+ };
421
+ }
422
+ if (signals.website) {
423
+ return {
424
+ title: 'Map the offer into one loop',
425
+ action: 'Read the website and turn it into one measurable workflow with a clear reward signal.',
426
+ why: 'A website is enough to define a first useful business loop without waiting for perfect intake.',
427
+ };
428
+ }
429
+ if ((signals.sourceEntries || []).length > 0) {
430
+ return {
431
+ title: 'Extract the first workflow from local evidence',
432
+ action: 'Read the strongest local source, summarize what the company does, and choose one workflow worth operationalizing first.',
433
+ why: 'Local evidence is already better than a blank template and can anchor the first action.',
434
+ };
435
+ }
436
+ return {
437
+ title: 'Collect one anchor signal',
438
+ action: 'Get one website, one named human, or one source doc so the environment can stop guessing.',
439
+ why: 'The system can work from partial input, but it still needs one concrete anchor.',
440
+ };
441
+ }
442
+
443
+ async function onboardBusiness(...flags) {
444
+ const options = parseOnboardFlags(flags, process.cwd());
445
+ const cwd = options.cwd || process.cwd();
446
+
447
+ const bizFile = path.join(cwd, '.atris', 'business.json');
448
+ if (!fs.existsSync(bizFile) && options.name) {
449
+ const slug = slugifyName(options.name);
450
+ createCanonicalBusinessWorkspace(cwd, {
451
+ business_id: '',
452
+ workspace_id: '',
453
+ name: options.name,
454
+ slug,
455
+ owner_email: '',
456
+ workspace_template: 'business',
457
+ }, { here: true });
458
+ }
459
+
460
+ const bizMeta = readWorkspaceBusinessMeta(cwd);
461
+
462
+ ensureWorkspaceStateFiles(cwd, {
463
+ slug: bizMeta.slug || 'business',
464
+ business_id: bizMeta.business_id || '',
465
+ workspace_id: bizMeta.workspace_id || '',
466
+ workspace_template: bizMeta.workspace_template || 'business',
467
+ }, { dryRun: false });
468
+
469
+ const contextDir = ensureContextScaffold(cwd, 'public');
470
+ const stamp = new Date().toISOString().replace(/[:]/g, '-').slice(0, 16);
471
+ const packDir = path.join(contextDir, '_ingest', `${stamp}-onboarding`);
472
+ fs.mkdirSync(packDir, { recursive: true });
473
+
474
+ const discovered = discoverOnboardingSignals(cwd, options);
475
+ const stagedSources = stageOnboardingSources(cwd, packDir, discovered.sourcePaths);
476
+ const links = Array.from(new Set([discovered.website, ...discovered.urls].filter(Boolean)));
477
+ const starterAction = suggestStarterAction({
478
+ website: discovered.website,
479
+ contactName: options.contactName,
480
+ contactEmail: options.contactEmail,
481
+ sourceEntries: stagedSources,
482
+ });
483
+ const intakeLines = [
484
+ `# ${bizMeta.name} Onboarding Intake`,
485
+ '',
486
+ `- Business: ${bizMeta.name}`,
487
+ `- Slug: ${bizMeta.slug}`,
488
+ discovered.website ? `- Website: ${discovered.website}` : null,
489
+ options.contactName ? `- Contact: ${options.contactName}` : null,
490
+ options.contactRole ? `- Contact role: ${options.contactRole}` : null,
491
+ options.contactEmail ? `- Contact email: ${options.contactEmail}` : null,
492
+ '',
493
+ '## Notes',
494
+ ...(options.notes.length > 0 ? options.notes.map((note) => `- ${note}`) : ['- No notes captured yet.']),
495
+ '',
496
+ '## Discovered Sources',
497
+ ...(stagedSources.length > 0 ? stagedSources.map((entry) => `- ${entry.original} -> ${entry.staged}`) : ['- No local files discovered yet.']),
498
+ '',
499
+ '## Links',
500
+ ...(links.length > 0 ? links.map((link) => `- ${link}`) : ['- No links captured yet.']),
501
+ ].filter(Boolean);
502
+ const intakePath = path.join(packDir, 'intake.md');
503
+ fs.writeFileSync(intakePath, `${intakeLines.join('\n')}\n`, 'utf8');
504
+
505
+ const linksPath = path.join(packDir, 'links.txt');
506
+ fs.writeFileSync(linksPath, `${links.join('\n')}${links.length > 0 ? '\n' : ''}`, 'utf8');
507
+ const sourcesPath = path.join(packDir, 'sources.txt');
508
+ fs.writeFileSync(
509
+ sourcesPath,
510
+ `${stagedSources.map((entry) => `${entry.original} -> ${entry.staged}`).join('\n')}${stagedSources.length > 0 ? '\n' : ''}`,
511
+ 'utf8'
512
+ );
513
+
514
+ const intakeRel = path.relative(cwd, intakePath).replace(/\\/g, '/');
515
+ const linksRel = path.relative(cwd, linksPath).replace(/\\/g, '/');
516
+ const sourcesRel = path.relative(cwd, sourcesPath).replace(/\\/g, '/');
517
+ const today = new Date().toISOString().slice(0, 10);
518
+
519
+ const briefSlug = `${bizMeta.slug}-starter-brief`;
520
+ const briefPath = path.join(cwd, 'atris', 'wiki', 'briefs', `${briefSlug}.md`);
521
+ writeMarkdownWithFrontmatter(briefPath, {
522
+ type: 'brief',
523
+ slug: briefSlug,
524
+ title: `${bizMeta.name} Starter Brief`,
525
+ sources: [intakeRel, linksRel, sourcesRel],
526
+ last_compiled: today,
527
+ created: today,
528
+ updated: today,
529
+ tags: ['business', 'onboarding', 'starter'],
530
+ }, `
531
+ # ${bizMeta.name} Starter Brief
532
+
533
+ ## What We Know
534
+
535
+ - Website: ${discovered.website || 'unknown'}
536
+ - Contact: ${options.contactName || 'unknown'}
537
+ - Contact role: ${options.contactRole || 'unknown'}
538
+ - Contact email: ${options.contactEmail || 'unknown'}
539
+ ${options.notes.map((note) => `- Note: ${note}`).join('\n') || '- Notes: none captured yet'}
540
+ ${stagedSources.length > 0 ? `- Local sources discovered: ${stagedSources.length}` : '- Local sources discovered: 0'}
541
+
542
+ ## Unknowns
543
+
544
+ - Primary customer or audience
545
+ - Revenue model and buying motion
546
+ - Main operator inside the business
547
+ - Tool stack and source systems
548
+ - First measurable operating loop
549
+
550
+ ## Next Moves
551
+
552
+ - Read the staged intake in \`${intakeRel}\`
553
+ - ${starterAction.action}
554
+ - Turn the first real interaction into a recap, then run \`atris business record ...\`
555
+ `);
556
+ upsertIndexEntry(path.join(cwd, 'atris', 'wiki', 'index.md'), 'Briefs', path.relative(cwd, briefPath), 'Starter business brief from onboarding intake');
557
+
558
+ let personRelativePath = null;
559
+ if (options.contactName) {
560
+ const personSlug = slugifyName(options.contactName);
561
+ const personPath = path.join(cwd, 'atris', 'wiki', 'people', `${personSlug}.md`);
562
+ writeMarkdownWithFrontmatter(personPath, {
563
+ type: 'person',
564
+ slug: personSlug,
565
+ title: options.contactName,
566
+ sources: [intakeRel, sourcesRel],
567
+ last_compiled: today,
568
+ created: today,
569
+ updated: today,
570
+ tags: ['person', 'contact', 'onboarding'],
571
+ }, `
572
+ # ${options.contactName}
573
+
574
+ ## Known
575
+
576
+ - Business: ${bizMeta.name}
577
+ - Role: ${options.contactRole || 'unknown'}
578
+ - Email: ${options.contactEmail || 'unknown'}
579
+
580
+ ## Unknown
581
+
582
+ - Decision authority
583
+ - Preferred communication rhythm
584
+ - Main business pain
585
+
586
+ ## Cross-References
587
+
588
+ - [[atris/wiki/briefs/${path.basename(briefPath)}]] - starter brief
589
+ `);
590
+ personRelativePath = path.relative(cwd, personPath).replace(/\\/g, '/');
591
+ upsertIndexEntry(path.join(cwd, 'atris', 'wiki', 'index.md'), 'People', personRelativePath, `Seed contact for ${bizMeta.name}`);
592
+ }
593
+
594
+ const conceptSlug = `${bizMeta.slug}-first-loop`;
595
+ const conceptPath = path.join(cwd, 'atris', 'wiki', 'concepts', `${conceptSlug}.md`);
596
+ writeMarkdownWithFrontmatter(conceptPath, {
597
+ type: 'concept',
598
+ slug: conceptSlug,
599
+ title: `${bizMeta.name} First Loop`,
600
+ sources: [intakeRel, linksRel, sourcesRel],
601
+ last_compiled: today,
602
+ created: today,
603
+ updated: today,
604
+ tags: ['concept', 'loop', 'onboarding'],
605
+ }, `
606
+ # ${bizMeta.name} First Loop
607
+
608
+ ## Candidate Loop
609
+
610
+ - Trigger: a new lead, meeting, client request, or operator handoff
611
+ - Action: summarize context, propose the next move, and draft one concrete output
612
+ - Reward: operator approval, reply, booked meeting, or visible pipeline progress
613
+
614
+ ## Known Signals
615
+
616
+ ${links.map((link) => `- ${link}`).join('\n') || '- No external links captured yet'}
617
+ ${stagedSources.length > 0 ? `- Local evidence files: ${stagedSources.length}` : ''}
618
+
619
+ ## Unknowns
620
+
621
+ - Best first workflow to automate
622
+ - Exact reward signal
623
+ - Required integrations
624
+ `);
625
+ upsertIndexEntry(path.join(cwd, 'atris', 'wiki', 'index.md'), 'Concepts', path.relative(cwd, conceptPath), 'Seed first-loop hypothesis from onboarding intake');
626
+
627
+ const cheatSheetPath = path.join(cwd, 'atris', 'reports', `${today}-${bizMeta.slug}-onboarding-cheat-sheet.md`);
628
+ const onePagerPath = path.join(cwd, 'atris', 'reports', `${today}-${bizMeta.slug}-operator-one-pager.md`);
629
+ const operatorSummary = [
630
+ `# ${bizMeta.name} Onboarding Cheat Sheet`,
631
+ '',
632
+ '## What Exists',
633
+ `- Starter brief: ${path.relative(cwd, briefPath).replace(/\\/g, '/')}`,
634
+ personRelativePath ? `- Contact page: ${personRelativePath}` : null,
635
+ `- First loop page: ${path.relative(cwd, conceptPath).replace(/\\/g, '/')}`,
636
+ `- Raw intake: ${intakeRel}`,
637
+ `- Source list: ${sourcesRel}`,
638
+ stagedSources.length > 0 ? `- Staged sources: ${stagedSources.length}` : '- Staged sources: 0',
639
+ '',
640
+ '## Best Next Action',
641
+ `- ${starterAction.title}`,
642
+ `- Action: ${starterAction.action}`,
643
+ `- Why: ${starterAction.why}`,
644
+ '- Swarlo join: placeholder preserved for the next live join step.',
645
+ '',
646
+ '## Next 3 Moves',
647
+ '- Open the starter brief and correct anything false.',
648
+ `- ${starterAction.action}`,
649
+ '- After the first real run, write a recap and record it with `atris business record ...`.',
650
+ ].filter(Boolean).join('\n') + '\n';
651
+ fs.writeFileSync(cheatSheetPath, operatorSummary, 'utf8');
652
+ fs.writeFileSync(onePagerPath, operatorSummary.replace('# ', '# One Pager — '), 'utf8');
653
+
654
+ const todoPath = path.join(cwd, 'atris', 'TODO.md');
655
+ if (fs.existsSync(todoPath)) {
656
+ let todoContent = fs.readFileSync(todoPath, 'utf8');
657
+ const taskLine = `- **Onboard:** ${starterAction.title} — ${starterAction.action} [execute]\n`;
658
+ const backlogMatch = todoContent.match(/^## Backlog\s*$/m);
659
+ if (backlogMatch) {
660
+ const insertAt = backlogMatch.index + backlogMatch[0].length;
661
+ todoContent = todoContent.slice(0, insertAt) + '\n' + taskLine + todoContent.slice(insertAt);
662
+ } else {
663
+ todoContent += '\n## Backlog\n\n' + taskLine;
664
+ }
665
+ fs.writeFileSync(todoPath, todoContent, 'utf8');
666
+ }
667
+
668
+ writeWikiStatus(cwd, {
669
+ health: `starter onboarding compiled from ${intakeRel}`,
670
+ nextMove: `review ${path.relative(cwd, briefPath).replace(/\\/g, '/')} and tighten the first loop`,
671
+ }, 'public', { lastIngest: `${today} ${new Date().toTimeString().slice(0, 5)}` });
672
+ appendWikiLog(cwd, `starter onboarding compiled for ${bizMeta.slug}`, [
673
+ `intake ${intakeRel}`,
674
+ `sources ${sourcesRel}`,
675
+ `brief ${path.relative(cwd, briefPath).replace(/\\/g, '/')}`,
676
+ personRelativePath ? `person ${personRelativePath}` : null,
677
+ `concept ${path.relative(cwd, conceptPath).replace(/\\/g, '/')}`,
678
+ `cheat sheet ${path.relative(cwd, cheatSheetPath).replace(/\\/g, '/')}`,
679
+ `one pager ${path.relative(cwd, onePagerPath).replace(/\\/g, '/')}`,
680
+ ].filter(Boolean), 'public', 'ONBOARD');
681
+
682
+ console.log('');
683
+ console.log(`Onboarded ${bizMeta.name}.`);
684
+ console.log(` Intake: ${intakeRel}`);
685
+ console.log(` Sources: ${sourcesRel}`);
686
+ console.log(` Brief: ${path.relative(cwd, briefPath).replace(/\\/g, '/')}`);
687
+ if (personRelativePath) console.log(` Contact: ${personRelativePath}`);
688
+ console.log(` First loop: ${path.relative(cwd, conceptPath).replace(/\\/g, '/')}`);
689
+ console.log(` Cheat sheet: ${path.relative(cwd, cheatSheetPath).replace(/\\/g, '/')}`);
690
+ console.log(` One pager: ${path.relative(cwd, onePagerPath).replace(/\\/g, '/')}`);
691
+ console.log(` Next action: ${starterAction.title}`);
692
+ console.log('');
693
+ }
694
+
695
+ async function recordBusinessRun(reportArg, ...flags) {
696
+ const options = parseRecordFlags([reportArg, ...flags], process.cwd());
697
+ const cwd = options.cwd || process.cwd();
698
+ const bizMeta = readWorkspaceBusinessMeta(cwd);
699
+ const { absPath, relPath } = resolveWorkspaceReport(cwd, options.reportPath);
700
+
701
+ ensureWorkspaceStateFiles(cwd, {
702
+ slug: bizMeta.slug || 'business',
703
+ business_id: bizMeta.business_id || '',
704
+ workspace_id: bizMeta.workspace_id || '',
705
+ workspace_template: bizMeta.workspace_template || 'business',
706
+ }, { dryRun: false });
707
+
708
+ const reportContent = fs.readFileSync(absPath, 'utf8');
709
+ const title = extractReportTitle(reportContent, absPath);
710
+ const outcome = normalizeOutcome(options.outcome);
711
+ const reward = options.reward != null ? Number(options.reward) : defaultRewardForOutcome(outcome);
712
+ if (!Number.isFinite(reward)) {
713
+ throw new Error(`Invalid reward: ${options.reward}`);
714
+ }
715
+
716
+ const recordedAt = new Date().toISOString();
717
+ const summary = options.summary || title;
718
+ const metric = options.metric || null;
719
+ const loop = options.loop || 'manual';
720
+ const actor = options.actor || 'operator';
721
+ const stateDir = path.join(cwd, '.atris', 'state');
722
+
723
+ const shared = {
724
+ recorded_at: recordedAt,
725
+ business_slug: bizMeta.slug || null,
726
+ business_name: bizMeta.name || null,
727
+ business_id: bizMeta.business_id || null,
728
+ workspace_id: bizMeta.workspace_id || null,
729
+ workspace_template: bizMeta.workspace_template || 'business',
730
+ report_path: relPath,
731
+ report_title: title,
732
+ summary,
733
+ metric,
734
+ outcome,
735
+ reward,
736
+ loop,
737
+ actor,
738
+ };
739
+
740
+ appendJsonl(path.join(stateDir, 'events.jsonl'), {
741
+ ...shared,
742
+ type: 'report_recorded',
743
+ });
744
+
745
+ appendJsonl(path.join(stateDir, 'episodes.jsonl'), {
746
+ ...shared,
747
+ type: 'episode',
748
+ });
749
+
750
+ appendJsonl(path.join(stateDir, 'scorecards.jsonl'), {
751
+ ...shared,
752
+ type: 'scorecard',
753
+ });
754
+
755
+ console.log('');
756
+ console.log(`Recorded recap for ${bizMeta.name || bizMeta.slug || 'workspace'}.`);
757
+ console.log(` Report: ${relPath}`);
758
+ console.log(` Outcome: ${outcome}`);
759
+ console.log(` Reward: ${reward}`);
760
+ if (metric) console.log(` Metric: ${metric}`);
761
+ console.log(' State: .atris/state/events.jsonl, episodes.jsonl, scorecards.jsonl');
762
+ console.log('');
103
763
  }
104
764
 
105
765
  function detectBusinessSlug(explicitSlug) {
@@ -198,7 +858,7 @@ async function listBusinesses(opts = {}) {
198
858
  * Walk ~/arena/atris-business/ and print a fleet status table for every
199
859
  * customer workspace. Pure local — no API calls, no rate-limit risk.
200
860
  *
201
- * Classifies each dir as: canonical, flat, unbound, nested, bare, or superseded.
861
+ * Classifies each dir as: ready, flat, unbound, nested, bare, or superseded.
202
862
  *
203
863
  * Discovered the need for this during overnight loop tick #3 when we hand-wrote
204
864
  * /tmp/customer_fleet.md. Now any team member can run `atris business list --local`
@@ -253,7 +913,7 @@ function listBusinessesLocal(opts = {}) {
253
913
 
254
914
  let state, action, icon;
255
915
  if (hasBizJson && hasAtris) {
256
- state = 'canonical'; action = 'none'; icon = '🟢';
916
+ state = 'ready'; action = 'none'; icon = '🟢';
257
917
  } else if (hasBizJson && !hasAtris) {
258
918
  state = 'flat'; action = 'migrate to atris/ wrapper'; icon = '🟡';
259
919
  } else if (!hasBizJson && hasAtris) {
@@ -263,7 +923,7 @@ function listBusinessesLocal(opts = {}) {
263
923
  } else if (total < 5) {
264
924
  state = 'bare'; action = 'not yet onboarded'; icon = '⚪';
265
925
  } else {
266
- state = 'flat-unbound'; action = 'needs canonical init'; icon = '🟡';
926
+ state = 'flat-unbound'; action = 'needs business init'; icon = '🟡';
267
927
  }
268
928
 
269
929
  let bizName = name;
@@ -309,7 +969,7 @@ function listBusinessesLocal(opts = {}) {
309
969
  console.log(' CUSTOMER STATE FILES BIZ.JSON ATRIS/ ACTION');
310
970
  console.log(' ' + '─'.repeat(83));
311
971
 
312
- const order = ['canonical', 'flat', 'unbound', 'flat-unbound', 'bare', 'nested', 'superseded'];
972
+ const order = ['ready', 'flat', 'unbound', 'flat-unbound', 'bare', 'nested', 'superseded'];
313
973
  const grouped = {};
314
974
  for (const c of customers) {
315
975
  if (!grouped[c.state]) grouped[c.state] = [];
@@ -678,7 +1338,7 @@ async function createBusinessInternal(name, flags = [], mode = 'auto') {
678
1338
  }, { here: options.here });
679
1339
  console.log(` Local workspace: ${scaffold.targetRoot}/`);
680
1340
  } else if (!options.noLocal) {
681
- console.log(' Tip: run `atris business init "<name>"` or add `--workspace` for a local canonical workspace.');
1341
+ console.log(' Tip: run `atris business init "<name>"` or add `--workspace` for a local business environment.');
682
1342
  }
683
1343
 
684
1344
  const template = options.template;
@@ -1152,9 +1812,18 @@ async function quickstart() {
1152
1812
  2. Open the local workspace:
1153
1813
  cd ~/arena/atris-business/my-company
1154
1814
 
1155
- 3. Push local state to cloud:
1815
+ 3. Seed onboarding context:
1816
+ atris business onboard --website https://example.com --contact "Founder Name" --note "what they do"
1817
+
1818
+ 4. Push local state to cloud:
1156
1819
  atris align --fix
1157
1820
 
1821
+ Then open atris/TODO.md and work the starter queue:
1822
+ define the first loop -> add named humans -> write the first recap
1823
+
1824
+ After the first recap lands:
1825
+ atris business record atris/reports/YYYY-MM-DD-your-recap.md --outcome mixed --metric "operator speed"
1826
+
1158
1827
  Optional:
1159
1828
  atris business connect slack --business my-company
1160
1829
  atris business connect github --business my-company
@@ -1225,6 +1894,13 @@ async function businessCommand(subcommand, ...args) {
1225
1894
  case 'push':
1226
1895
  await deployBusiness(args[0]);
1227
1896
  break;
1897
+ case 'record':
1898
+ case 'record-recap':
1899
+ await recordBusinessRun(args[0], ...args.slice(1));
1900
+ break;
1901
+ case 'onboard':
1902
+ await onboardBusiness(...args);
1903
+ break;
1228
1904
  case 'quickstart':
1229
1905
  case 'start':
1230
1906
  case 'guide':
@@ -1235,9 +1911,9 @@ async function businessCommand(subcommand, ...args) {
1235
1911
  console.log('');
1236
1912
  console.log(' quickstart ← Start here! 3-command guide');
1237
1913
  console.log('');
1238
- console.log(' init <name> Create a canonical business workspace (cloud + local)');
1914
+ console.log(' init <name> Create a business environment (cloud + local)');
1239
1915
  console.log(' workspace <name> Alias for init');
1240
- console.log(' create <name> Create the cloud business; add --workspace for local canonical scaffold');
1916
+ console.log(' create <name> Create the cloud business; add --workspace for a local business environment');
1241
1917
  console.log(' add <slug> Register an existing cloud business');
1242
1918
  console.log(' list Show registered businesses');
1243
1919
  console.log(' team [slug] Show members, roles, and admin access');
@@ -1247,6 +1923,8 @@ async function businessCommand(subcommand, ...args) {
1247
1923
  console.log(' connect <service> Connect a skill/integration');
1248
1924
  console.log(' notify <mode> Set notification mode (digest/silent/push)');
1249
1925
  console.log(' deploy <slug> Push local business to cloud');
1926
+ console.log(' onboard Seed brief, person, first loop, safe next action, and one-pager from sparse input');
1927
+ console.log(' record <report> Append recap state into events, episodes, and scorecards');
1250
1928
  console.log(' remove <slug> Unregister locally');
1251
1929
  }
1252
1930
  }
@@ -1261,4 +1939,6 @@ module.exports = {
1261
1939
  getBusinessConfigPath,
1262
1940
  createCanonicalBusinessWorkspace,
1263
1941
  initBusinessWorkspace,
1942
+ onboardBusiness,
1943
+ recordBusinessRun,
1264
1944
  };