atris 3.2.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 (49) hide show
  1. package/GETTING_STARTED.md +65 -131
  2. package/README.md +18 -2
  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/research-search/SKILL.md +167 -0
  11. package/atris/skills/research-search/arxiv_search.py +157 -0
  12. package/atris/skills/research-search/program.md +48 -0
  13. package/atris/skills/research-search/results.tsv +6 -0
  14. package/atris/skills/research-search/scholar_search.py +154 -0
  15. package/atris/skills/tidy/SKILL.md +36 -21
  16. package/atris/team/_template/MEMBER.md +2 -0
  17. package/atris/team/validator/MEMBER.md +35 -1
  18. package/atris.md +118 -178
  19. package/bin/atris.js +30 -5
  20. package/cli/__pycache__/atris_code.cpython-314.pyc +0 -0
  21. package/cli/__pycache__/runtime_guard.cpython-312.pyc +0 -0
  22. package/cli/__pycache__/runtime_guard.cpython-314.pyc +0 -0
  23. package/cli/atris_code.py +889 -0
  24. package/cli/runtime_guard.py +693 -0
  25. package/commands/align.js +15 -0
  26. package/commands/app.js +316 -0
  27. package/commands/autopilot.js +390 -7
  28. package/commands/business.js +677 -2
  29. package/commands/computer.js +1979 -43
  30. package/commands/context-sync.js +5 -0
  31. package/commands/lifecycle.js +12 -0
  32. package/commands/plugin.js +24 -0
  33. package/commands/pull.js +40 -1
  34. package/commands/push.js +44 -0
  35. package/commands/serve.js +1 -0
  36. package/commands/sync.js +272 -76
  37. package/commands/verify.js +50 -1
  38. package/commands/wiki.js +27 -2
  39. package/lib/file-ops.js +13 -1
  40. package/lib/journal.js +23 -0
  41. package/lib/scorecard.js +42 -4
  42. package/lib/sync-telemetry.js +59 -0
  43. package/lib/todo.js +6 -0
  44. package/lib/wiki.js +150 -6
  45. package/package.json +2 -1
  46. package/utils/api.js +19 -0
  47. package/utils/auth.js +25 -1
  48. package/utils/config.js +24 -0
  49. 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();
@@ -104,6 +105,663 @@ function createCanonicalBusinessWorkspace(targetRoot, bizMeta, options = {}) {
104
105
  return { targetRoot, businessJsonPath, workspaceTemplate };
105
106
  }
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('');
763
+ }
764
+
107
765
  function detectBusinessSlug(explicitSlug) {
108
766
  if (explicitSlug) return explicitSlug;
109
767
  const bizFile = path.join(process.cwd(), '.atris', 'business.json');
@@ -1154,12 +1812,18 @@ async function quickstart() {
1154
1812
  2. Open the local workspace:
1155
1813
  cd ~/arena/atris-business/my-company
1156
1814
 
1157
- 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:
1158
1819
  atris align --fix
1159
1820
 
1160
1821
  Then open atris/TODO.md and work the starter queue:
1161
1822
  define the first loop -> add named humans -> write the first recap
1162
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
+
1163
1827
  Optional:
1164
1828
  atris business connect slack --business my-company
1165
1829
  atris business connect github --business my-company
@@ -1230,6 +1894,13 @@ async function businessCommand(subcommand, ...args) {
1230
1894
  case 'push':
1231
1895
  await deployBusiness(args[0]);
1232
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;
1233
1904
  case 'quickstart':
1234
1905
  case 'start':
1235
1906
  case 'guide':
@@ -1252,6 +1923,8 @@ async function businessCommand(subcommand, ...args) {
1252
1923
  console.log(' connect <service> Connect a skill/integration');
1253
1924
  console.log(' notify <mode> Set notification mode (digest/silent/push)');
1254
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');
1255
1928
  console.log(' remove <slug> Unregister locally');
1256
1929
  }
1257
1930
  }
@@ -1266,4 +1939,6 @@ module.exports = {
1266
1939
  getBusinessConfigPath,
1267
1940
  createCanonicalBusinessWorkspace,
1268
1941
  initBusinessWorkspace,
1942
+ onboardBusiness,
1943
+ recordBusinessRun,
1269
1944
  };