atris 3.79.0 → 3.80.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.
@@ -0,0 +1,2022 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { stateHome } = require('./state-home');
7
+ const { syncBusinessCanonical, ensureWorkspaceStateFiles, ensureBusinessRootAgentAdapters } = require('../commands/sync');
8
+ const { ensureContextScaffold, writeWikiStatus, appendWikiLog } = require('./wiki');
9
+ const { writeRuntimeReceipt } = require('./runtime-bootstrap');
10
+
11
+ function businessWorkspaceBase() {
12
+ return process.env.ATRIS_BUSINESS_ROOT || path.join(os.homedir(), 'arena', 'atris-business');
13
+ }
14
+
15
+ function getBusinessConfigPath() {
16
+ const dir = path.join(stateHome(), '.atris');
17
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
18
+ return path.join(dir, 'businesses.json');
19
+ }
20
+
21
+ function slugify(name) {
22
+ return String(name || '')
23
+ .toLowerCase()
24
+ .trim()
25
+ .replace(/[^a-z0-9\s-]/g, '')
26
+ .replace(/\s+/g, '-')
27
+ .replace(/-+/g, '-')
28
+ .replace(/^-+|-+$/g, '');
29
+ }
30
+
31
+ function isHelpToken(arg) {
32
+ return arg === '--help' || arg === '-h' || arg === 'help' || arg === '-?';
33
+ }
34
+
35
+ const BUSINESS_CREATE_USAGE = 'Usage: atris business create <name> [--description "..."] [--workspace] [--here|--root <dir>]';
36
+ const BUSINESS_SHIP_USAGE = 'Usage: atris business ship "<paragraph>" [--name <name>] [--email <email>] --yes';
37
+
38
+ function isAccidentalHelpBusiness(value) {
39
+ const raw = String(value || '').trim();
40
+ if (!raw) return false;
41
+ if (isHelpToken(raw)) return true;
42
+ const normalized = slugify(raw);
43
+ if (normalized === 'help') return true;
44
+ if (/^help-\d+$/.test(normalized)) return true;
45
+ return false;
46
+ }
47
+
48
+ function validateBusinessCreateName(name) {
49
+ if (!name || !String(name).trim()) {
50
+ return { ok: false, usage: BUSINESS_CREATE_USAGE };
51
+ }
52
+ if (isHelpToken(name)) {
53
+ return { ok: false, usage: BUSINESS_CREATE_USAGE };
54
+ }
55
+ if (String(name).startsWith('-')) {
56
+ return {
57
+ ok: false,
58
+ usage: BUSINESS_CREATE_USAGE,
59
+ detail: `Refusing to create a business named "${name}", looks like a flag, not a name.`,
60
+ };
61
+ }
62
+ if (isAccidentalHelpBusiness(name)) {
63
+ return {
64
+ ok: false,
65
+ usage: BUSINESS_CREATE_USAGE,
66
+ detail: `Refusing to create a business named "${name}", looks like accidental help output, not a real business.`,
67
+ };
68
+ }
69
+ return { ok: true };
70
+ }
71
+
72
+ function compactShipText(value) {
73
+ return String(value || '').replace(/\s+/g, ' ').trim();
74
+ }
75
+
76
+ function parseBusinessShipArgs(args = []) {
77
+ const positional = [];
78
+ let name = '';
79
+ let email = '';
80
+
81
+ for (let i = 0; i < args.length; i++) {
82
+ const arg = args[i];
83
+ const next = args[i + 1];
84
+
85
+ if (arg === '--name') {
86
+ if (!next || String(next).startsWith('-')) {
87
+ return { ok: false, usage: BUSINESS_SHIP_USAGE, detail: 'Missing value for --name.' };
88
+ }
89
+ name = next;
90
+ i++;
91
+ } else if (typeof arg === 'string' && arg.startsWith('--name=')) {
92
+ name = arg.slice('--name='.length);
93
+ } else if (arg === '--email') {
94
+ if (!next || String(next).startsWith('-')) {
95
+ return { ok: false, usage: BUSINESS_SHIP_USAGE, detail: 'Missing value for --email.' };
96
+ }
97
+ email = next;
98
+ i++;
99
+ } else if (typeof arg === 'string' && arg.startsWith('--email=')) {
100
+ email = arg.slice('--email='.length);
101
+ } else if (arg === '--yes' || arg === '-y') {
102
+ // proceed flag; checked by shipBusiness
103
+ } else if (typeof arg === 'string' && arg.startsWith('-')) {
104
+ return { ok: false, usage: BUSINESS_SHIP_USAGE, detail: `Unknown option: ${arg}` };
105
+ } else {
106
+ positional.push(arg);
107
+ }
108
+ }
109
+
110
+ const description = compactShipText(positional.join(' '));
111
+ if (!description) {
112
+ return { ok: false, usage: BUSINESS_SHIP_USAGE };
113
+ }
114
+
115
+ return {
116
+ ok: true,
117
+ description,
118
+ name: compactShipText(name),
119
+ email: compactShipText(email),
120
+ };
121
+ }
122
+
123
+ function shipTeamNames(team) {
124
+ const rows = Array.isArray(team)
125
+ ? team
126
+ : (Array.isArray(team?.members) ? team.members : []);
127
+ return rows.map((member) => {
128
+ if (typeof member === 'string') return compactShipText(member);
129
+ if (!member || typeof member !== 'object') return '';
130
+ return compactShipText(member.display_name || member.name || member.slug || '');
131
+ }).filter(Boolean);
132
+ }
133
+
134
+ function shipTaskTitle(task) {
135
+ if (!task) return '';
136
+ if (typeof task === 'string') return compactShipText(task);
137
+ if (typeof task !== 'object') return '';
138
+ return compactShipText(task.title || task.name || '');
139
+ }
140
+
141
+ function loadBusinesses() {
142
+ const p = getBusinessConfigPath();
143
+ if (!fs.existsSync(p)) return {};
144
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return {}; }
145
+ }
146
+
147
+ function businessAliases(business) {
148
+ const aliases = business?.aliases || business?.config?.aliases || [];
149
+ return Array.isArray(aliases) ? aliases : [];
150
+ }
151
+
152
+ function businessMatchesSlug(business, slug, { includeName = false } = {}) {
153
+ if (!business || !slug) return false;
154
+ const wanted = String(slug).toLowerCase();
155
+ const canonical = String(business.slug || '').toLowerCase();
156
+ const aliases = businessAliases(business).map((alias) => String(alias).toLowerCase());
157
+ if (canonical === wanted || aliases.includes(wanted)) return true;
158
+ return includeName && String(business.name || '').toLowerCase() === wanted;
159
+ }
160
+
161
+ function saveBusinesses(data) {
162
+ fs.writeFileSync(getBusinessConfigPath(), JSON.stringify(data, null, 2));
163
+ }
164
+
165
+ function buildBusinessCacheEntry(business, localSlug, existing = {}) {
166
+ const aliases = businessAliases(business);
167
+ const entry = {
168
+ business_id: business.id || business.business_id,
169
+ workspace_id: business.workspace_id,
170
+ name: business.name || localSlug,
171
+ slug: localSlug || business.slug,
172
+ added_at: existing.added_at || new Date().toISOString(),
173
+ };
174
+ if (business.slug && business.slug !== entry.slug) entry.canonical_slug = business.slug;
175
+ else if (business.slug) entry.canonical_slug = business.slug;
176
+ if (aliases.length > 0) entry.aliases = aliases;
177
+ return entry;
178
+ }
179
+
180
+ function readBusinessFolderBindings(rootDir = businessWorkspaceBase()) {
181
+ const skip = new Set([
182
+ '.git', '.DS_Store', 'archive', 'archives', '_archive', 'bench', 'deals',
183
+ 'node_modules', 'shelf', '_shelf', 'templates',
184
+ ]);
185
+ if (!fs.existsSync(rootDir)) return [];
186
+
187
+ return fs.readdirSync(rootDir, { withFileTypes: true })
188
+ .filter((entry) => !entry.name.startsWith('.') && !skip.has(entry.name))
189
+ .map((entry) => {
190
+ const fullPath = path.join(rootDir, entry.name);
191
+ let isDirectory = entry.isDirectory();
192
+ const isSymlink = entry.isSymbolicLink();
193
+ let symlinkTarget = null;
194
+ if (isSymlink) {
195
+ try {
196
+ symlinkTarget = fs.readlinkSync(fullPath);
197
+ isDirectory = fs.statSync(fullPath).isDirectory();
198
+ } catch {
199
+ isDirectory = false;
200
+ }
201
+ }
202
+ if (!isDirectory) return null;
203
+
204
+ const businessJsonPath = path.join(fullPath, '.atris', 'business.json');
205
+ const atrisDir = path.join(fullPath, 'atris');
206
+ const binding = {
207
+ name: entry.name,
208
+ path: fullPath,
209
+ isSymlink,
210
+ symlinkTarget,
211
+ hasAtris: fs.existsSync(atrisDir) && fs.statSync(atrisDir).isDirectory(),
212
+ hasBusinessJson: fs.existsSync(businessJsonPath),
213
+ businessJsonPath,
214
+ meta: null,
215
+ error: null,
216
+ };
217
+
218
+ if (binding.hasBusinessJson) {
219
+ try {
220
+ binding.meta = JSON.parse(fs.readFileSync(businessJsonPath, 'utf8'));
221
+ } catch (err) {
222
+ binding.error = err.message || 'invalid JSON';
223
+ }
224
+ }
225
+ return binding;
226
+ })
227
+ .filter(Boolean);
228
+ }
229
+
230
+ function analyzeBusinessDoctor({ cache = {}, cloudBusinesses = [], folderBindings = [] } = {}) {
231
+ const issues = [];
232
+ const cacheUpdates = {};
233
+ const activeById = new Map();
234
+ const activeBySlug = new Map();
235
+ const realFolderIds = new Map();
236
+
237
+ for (const business of cloudBusinesses || []) {
238
+ const id = business.id || business.business_id;
239
+ if (!id) continue;
240
+ activeById.set(id, business);
241
+ if (business.slug) {
242
+ const lower = String(business.slug).toLowerCase();
243
+ if (activeBySlug.has(lower)) {
244
+ issues.push({
245
+ level: 'fail',
246
+ code: 'duplicate-active-slug',
247
+ subject: business.slug,
248
+ message: `multiple active cloud businesses use slug ${business.slug}`,
249
+ });
250
+ }
251
+ activeBySlug.set(lower, business);
252
+ }
253
+ }
254
+
255
+ const findActiveMatch = (key, entry = {}) => {
256
+ if (entry.business_id && activeById.has(entry.business_id)) return activeById.get(entry.business_id);
257
+ const candidates = [key, entry.slug, entry.canonical_slug, entry.name].filter(Boolean);
258
+ return cloudBusinesses.find((business) =>
259
+ candidates.some((candidate) => businessMatchesSlug(business, candidate, { includeName: true }))
260
+ ) || null;
261
+ };
262
+
263
+ for (const [key, entry] of Object.entries(cache || {})) {
264
+ const active = findActiveMatch(key, entry);
265
+ if (!active) {
266
+ issues.push({
267
+ level: 'fail',
268
+ code: 'stale-cache',
269
+ subject: key,
270
+ message: `${key} points at a deleted/inaccessible business (${entry.business_id || 'missing id'})`,
271
+ });
272
+ continue;
273
+ }
274
+
275
+ if (entry.business_id && entry.business_id !== active.id) {
276
+ issues.push({
277
+ level: 'fail',
278
+ code: 'stale-cache-repoint',
279
+ subject: key,
280
+ message: `${key} points at ${entry.business_id}; active cloud row is ${active.id}`,
281
+ fixable: true,
282
+ });
283
+ cacheUpdates[key] = buildBusinessCacheEntry(active, key, entry);
284
+ }
285
+
286
+ if (!businessMatchesSlug({ ...active, aliases: businessAliases(active) }, key, { includeName: true }) && key !== active.slug) {
287
+ issues.push({
288
+ level: 'warn',
289
+ code: 'cache-key-not-alias',
290
+ subject: key,
291
+ message: `${key} is cached but is not the canonical slug, alias, or name for ${active.slug}`,
292
+ });
293
+ }
294
+ }
295
+
296
+ for (const business of cloudBusinesses || []) {
297
+ const canonicalKey = business.slug;
298
+ if (canonicalKey && (!cache[canonicalKey] || cache[canonicalKey].business_id !== business.id)) {
299
+ issues.push({
300
+ level: 'warn',
301
+ code: 'missing-canonical-cache',
302
+ subject: canonicalKey,
303
+ message: `${canonicalKey} is active in cloud but missing/stale in local cache`,
304
+ fixable: true,
305
+ });
306
+ cacheUpdates[canonicalKey] = buildBusinessCacheEntry(business, canonicalKey, cache[canonicalKey]);
307
+ }
308
+
309
+ for (const alias of businessAliases(business)) {
310
+ if (!cache[alias] || cache[alias].business_id !== business.id) {
311
+ issues.push({
312
+ level: 'warn',
313
+ code: 'missing-alias-cache',
314
+ subject: alias,
315
+ message: `${alias} is an active alias for ${business.slug} but missing/stale in local cache`,
316
+ fixable: true,
317
+ });
318
+ cacheUpdates[alias] = buildBusinessCacheEntry(business, alias, cache[alias]);
319
+ }
320
+ }
321
+ }
322
+
323
+ for (const binding of folderBindings || []) {
324
+ if (binding.error) {
325
+ issues.push({
326
+ level: 'fail',
327
+ code: 'invalid-business-json',
328
+ subject: binding.name,
329
+ message: `${binding.name}/.atris/business.json is invalid: ${binding.error}`,
330
+ });
331
+ continue;
332
+ }
333
+
334
+ if (!binding.hasBusinessJson) {
335
+ if (binding.hasAtris && !binding.isSymlink) {
336
+ issues.push({
337
+ level: 'warn',
338
+ code: 'folder-unbound',
339
+ subject: binding.name,
340
+ message: `${binding.name} has atris/ but no .atris/business.json`,
341
+ });
342
+ }
343
+ continue;
344
+ }
345
+
346
+ const meta = binding.meta || {};
347
+ const active = findActiveMatch(binding.name, {
348
+ business_id: meta.business_id,
349
+ slug: meta.slug,
350
+ canonical_slug: meta.canonical_slug,
351
+ name: meta.name,
352
+ });
353
+
354
+ if (!active) {
355
+ issues.push({
356
+ level: 'fail',
357
+ code: 'stale-folder-binding',
358
+ subject: binding.name,
359
+ message: `${binding.name} is bound to deleted/inaccessible business ${meta.business_id || meta.slug || 'unknown'}`,
360
+ });
361
+ continue;
362
+ }
363
+
364
+ if (meta.business_id && meta.business_id !== active.id) {
365
+ issues.push({
366
+ level: 'fail',
367
+ code: 'folder-id-mismatch',
368
+ subject: binding.name,
369
+ message: `${binding.name} points at ${meta.business_id}; active cloud row is ${active.id}`,
370
+ });
371
+ }
372
+
373
+ if (meta.slug && !businessMatchesSlug(active, meta.slug, { includeName: true })) {
374
+ issues.push({
375
+ level: 'fail',
376
+ code: 'folder-slug-mismatch',
377
+ subject: binding.name,
378
+ message: `${binding.name} uses slug ${meta.slug}, which is not ${active.slug} or an alias`,
379
+ });
380
+ }
381
+
382
+ if (!businessMatchesSlug(active, binding.name, { includeName: false }) && !binding.isSymlink) {
383
+ issues.push({
384
+ level: 'warn',
385
+ code: 'folder-name-not-slug-or-alias',
386
+ subject: binding.name,
387
+ message: `${binding.name} is not a canonical slug or alias for ${active.slug}`,
388
+ });
389
+ }
390
+
391
+ if (!binding.isSymlink) {
392
+ const existing = realFolderIds.get(active.id) || [];
393
+ existing.push(binding.name);
394
+ realFolderIds.set(active.id, existing);
395
+ }
396
+
397
+ const cacheKey = meta.slug || binding.name;
398
+ if (!cache[cacheKey] || cache[cacheKey].business_id !== active.id) {
399
+ issues.push({
400
+ level: 'warn',
401
+ code: 'folder-cache-missing',
402
+ subject: cacheKey,
403
+ message: `${binding.name} is bound locally but ${cacheKey} is missing/stale in local cache`,
404
+ fixable: true,
405
+ });
406
+ cacheUpdates[cacheKey] = buildBusinessCacheEntry(active, cacheKey, cache[cacheKey]);
407
+ }
408
+ }
409
+
410
+ for (const [businessId, names] of realFolderIds.entries()) {
411
+ if (names.length > 1) {
412
+ issues.push({
413
+ level: 'fail',
414
+ code: 'duplicate-real-folders',
415
+ subject: businessId,
416
+ message: `business ${businessId} has multiple real folders: ${names.join(', ')}`,
417
+ });
418
+ }
419
+ }
420
+
421
+ const cacheRemovals = [];
422
+ for (const business of cloudBusinesses || []) {
423
+ if (!isAccidentalHelpBusiness(business?.name) && !isAccidentalHelpBusiness(business?.slug)) continue;
424
+ const label = business.slug || business.name || business.id;
425
+ issues.push({
426
+ level: 'warn',
427
+ code: 'accidental-help-business',
428
+ subject: label,
429
+ message: `${label} looks like an accidental help business (April 2026 ghost rows)`,
430
+ fixable: true,
431
+ });
432
+ }
433
+
434
+ for (const [key, entry] of Object.entries(cache || {})) {
435
+ const active = findActiveMatch(key, entry);
436
+ const accidental = isAccidentalHelpBusiness(key)
437
+ || isAccidentalHelpBusiness(entry?.name)
438
+ || isAccidentalHelpBusiness(entry?.slug)
439
+ || (active && (isAccidentalHelpBusiness(active.name) || isAccidentalHelpBusiness(active.slug)));
440
+ if (!accidental) continue;
441
+ issues.push({
442
+ level: 'warn',
443
+ code: 'accidental-help-cache',
444
+ subject: key,
445
+ message: `${key} is cached for an accidental help business`,
446
+ fixable: true,
447
+ });
448
+ if (!cacheRemovals.includes(key)) cacheRemovals.push(key);
449
+ }
450
+
451
+ return {
452
+ issues,
453
+ cacheUpdates,
454
+ cacheRemovals,
455
+ stats: {
456
+ cache_entries: Object.keys(cache || {}).length,
457
+ cloud_active: cloudBusinesses.length,
458
+ folders: folderBindings.length,
459
+ fixable_cache_entries: Object.keys(cacheUpdates).length,
460
+ accidental_help_cache_removals: cacheRemovals.length,
461
+ },
462
+ };
463
+ }
464
+
465
+ function parseCreateBusinessFlags(flags, cwd = process.cwd()) {
466
+ const options = {
467
+ description: '',
468
+ template: null,
469
+ ownerEmail: '',
470
+ noLocal: false,
471
+ workspace: false,
472
+ here: false,
473
+ root: null,
474
+ cwd,
475
+ };
476
+
477
+ for (let i = 0; i < flags.length; i++) {
478
+ const flag = flags[i];
479
+ const next = flags[i + 1];
480
+
481
+ if ((flag === '--description' || flag === '-d') && next) {
482
+ options.description = next;
483
+ i++;
484
+ } else if ((flag === '--template' || flag === '-t') && next) {
485
+ options.template = next;
486
+ i++;
487
+ } else if (flag === '--owner-email' && next) {
488
+ options.ownerEmail = next;
489
+ i++;
490
+ } else if ((flag === '--root' || flag === '--workspace-root') && next) {
491
+ options.root = path.resolve(cwd, next);
492
+ options.workspace = true;
493
+ i++;
494
+ } else if (flag === '--here') {
495
+ options.here = true;
496
+ options.workspace = true;
497
+ } else if (flag === '--workspace' || flag === '--local-workspace') {
498
+ options.workspace = true;
499
+ } else if (flag === '--no-local') {
500
+ options.noLocal = true;
501
+ }
502
+ }
503
+
504
+ return options;
505
+ }
506
+
507
+ function resolveWorkspaceRoot(slug, options = {}) {
508
+ if (options.noLocal) return null;
509
+ if (options.here) return options.cwd || process.cwd();
510
+ if (options.root) return path.join(options.root, slug);
511
+ return path.join(businessWorkspaceBase(), slug);
512
+ }
513
+
514
+ function createCanonicalBusinessWorkspace(targetRoot, bizMeta, options = {}) {
515
+ if (!targetRoot) {
516
+ throw new Error('No target directory provided for business workspace.');
517
+ }
518
+
519
+ if (options.here !== true && fs.existsSync(targetRoot) && !fs.statSync(targetRoot).isDirectory()) {
520
+ throw new Error(`Target path is not a directory: ${targetRoot}`);
521
+ }
522
+
523
+ fs.mkdirSync(targetRoot, { recursive: true });
524
+
525
+ const atrisMetaDir = path.join(targetRoot, '.atris');
526
+ const businessJsonPath = path.join(atrisMetaDir, 'business.json');
527
+ if (fs.existsSync(businessJsonPath)) {
528
+ throw new Error(`Target already contains .atris/business.json: ${targetRoot}`);
529
+ }
530
+
531
+ const workspaceTemplate = options.templateName || bizMeta.workspace_template || 'business';
532
+ fs.mkdirSync(atrisMetaDir, { recursive: true });
533
+ fs.writeFileSync(businessJsonPath, JSON.stringify({
534
+ business_id: bizMeta.business_id,
535
+ workspace_id: bizMeta.workspace_id,
536
+ name: bizMeta.name,
537
+ slug: bizMeta.slug,
538
+ owner_email: bizMeta.owner_email || '',
539
+ workspace_template: workspaceTemplate,
540
+ created_at: new Date().toISOString(),
541
+ }, null, 2));
542
+
543
+ const syncResult = syncBusinessCanonical(targetRoot, bizMeta, { force: false, dryRun: false, templateName: workspaceTemplate });
544
+ const agentAdapters = syncResult?.agentAdapterList || ensureBusinessRootAgentAdapters(targetRoot, bizMeta);
545
+ writeRuntimeReceipt(targetRoot, {
546
+ scope: 'local-business-computer',
547
+ boundary: 'business-workspace-scaffold',
548
+ business_id: bizMeta.business_id,
549
+ workspace_id: bizMeta.workspace_id,
550
+ business_slug: bizMeta.slug,
551
+ business_name: bizMeta.name,
552
+ workspace_template: workspaceTemplate,
553
+ install_status: 'local_cli_present',
554
+ sync_status: 'templates_seeded',
555
+ agent_adapters: agentAdapters,
556
+ });
557
+ return { targetRoot, businessJsonPath, workspaceTemplate, agentAdapters };
558
+ }
559
+
560
+ function parseRecordFlags(args, cwd = process.cwd()) {
561
+ const options = {
562
+ cwd,
563
+ reportPath: null,
564
+ summary: '',
565
+ metric: '',
566
+ outcome: 'recorded',
567
+ reward: null,
568
+ loop: 'manual',
569
+ actor: 'operator',
570
+ };
571
+
572
+ for (let i = 0; i < args.length; i++) {
573
+ const arg = args[i];
574
+ const next = args[i + 1];
575
+
576
+ if ((arg === '--summary' || arg === '-s') && next) {
577
+ options.summary = next;
578
+ i++;
579
+ } else if ((arg === '--metric' || arg === '-m') && next) {
580
+ options.metric = next;
581
+ i++;
582
+ } else if ((arg === '--outcome' || arg === '-o') && next) {
583
+ options.outcome = next;
584
+ i++;
585
+ } else if ((arg === '--reward' || arg === '-r') && next) {
586
+ options.reward = next;
587
+ i++;
588
+ } else if (arg === '--loop' && next) {
589
+ options.loop = next;
590
+ i++;
591
+ } else if (arg === '--actor' && next) {
592
+ options.actor = next;
593
+ i++;
594
+ } else if (!arg.startsWith('-') && !options.reportPath) {
595
+ options.reportPath = arg;
596
+ }
597
+ }
598
+
599
+ return options;
600
+ }
601
+
602
+ function parseShareFlags(args, cwd = process.cwd()) {
603
+ const options = {
604
+ cwd,
605
+ role: 'collaborator',
606
+ name: '',
607
+ email: '',
608
+ write: false,
609
+ out: null,
610
+ };
611
+
612
+ for (let i = 0; i < args.length; i++) {
613
+ const arg = args[i];
614
+ const next = args[i + 1];
615
+ if ((arg === '--role' || arg === '-r') && next) {
616
+ options.role = next;
617
+ i++;
618
+ } else if ((arg === '--name' || arg === '--person') && next) {
619
+ options.name = next;
620
+ i++;
621
+ } else if (arg === '--email' && next) {
622
+ options.email = next;
623
+ i++;
624
+ } else if (arg === '--write') {
625
+ options.write = true;
626
+ } else if (arg === '--out' && next) {
627
+ options.out = next;
628
+ options.write = true;
629
+ i++;
630
+ } else if (arg.startsWith('--out=')) {
631
+ options.out = arg.slice('--out='.length);
632
+ options.write = true;
633
+ } else if ((arg === '--cwd' || arg === '--workspace') && next) {
634
+ options.cwd = path.resolve(cwd, next);
635
+ i++;
636
+ } else if (arg.startsWith('--cwd=')) {
637
+ options.cwd = path.resolve(cwd, arg.slice('--cwd='.length));
638
+ } else if (arg.startsWith('--workspace=')) {
639
+ options.cwd = path.resolve(cwd, arg.slice('--workspace='.length));
640
+ }
641
+ }
642
+
643
+ return options;
644
+ }
645
+
646
+ function parseOnboardFlags(args, cwd = process.cwd()) {
647
+ const options = {
648
+ cwd,
649
+ name: '',
650
+ website: '',
651
+ links: [],
652
+ notes: [],
653
+ sources: [],
654
+ contactName: '',
655
+ contactEmail: '',
656
+ contactRole: '',
657
+ };
658
+ const freeform = [];
659
+
660
+ for (let i = 0; i < args.length; i++) {
661
+ const arg = args[i];
662
+ const next = args[i + 1];
663
+
664
+ if ((arg === '--name' || arg === '--business') && next) {
665
+ options.name = next;
666
+ i++;
667
+ } else if ((arg === '--website' || arg === '--site') && next) {
668
+ options.website = next;
669
+ i++;
670
+ } else if ((arg === '--link' || arg === '--url') && next) {
671
+ options.links.push(next);
672
+ i++;
673
+ } else if ((arg === '--from' || arg === '--source') && next) {
674
+ options.sources.push(next);
675
+ i++;
676
+ } else if ((arg === '--note' || arg === '--notes') && next) {
677
+ options.notes.push(next);
678
+ i++;
679
+ } else if ((arg === '--contact' || arg === '--person') && next) {
680
+ options.contactName = next;
681
+ i++;
682
+ } else if (arg === '--email' && next) {
683
+ options.contactEmail = next;
684
+ i++;
685
+ } else if (arg === '--role' && next) {
686
+ options.contactRole = next;
687
+ i++;
688
+ } else if (!arg.startsWith('-')) {
689
+ const resolved = path.resolve(cwd, arg);
690
+ if (/^https?:\/\//i.test(arg)) {
691
+ if (!options.website) options.website = arg;
692
+ else options.links.push(arg);
693
+ } else if (fs.existsSync(resolved)) {
694
+ options.sources.push(arg);
695
+ } else {
696
+ freeform.push(arg);
697
+ }
698
+ }
699
+ }
700
+
701
+ if (freeform.length > 0) {
702
+ options.notes.push(freeform.join(' '));
703
+ }
704
+
705
+ return options;
706
+ }
707
+
708
+ function readWorkspaceBusinessMeta(cwd = process.cwd()) {
709
+ const bizFile = path.join(cwd, '.atris', 'business.json');
710
+ if (!fs.existsSync(bizFile)) {
711
+ throw new Error('Run this command inside a business environment with .atris/business.json.');
712
+ }
713
+ try {
714
+ return JSON.parse(fs.readFileSync(bizFile, 'utf8'));
715
+ } catch (error) {
716
+ throw new Error(`Failed to read .atris/business.json: ${error.message}`);
717
+ }
718
+ }
719
+
720
+ function resolveWorkspaceReport(cwd, reportPath) {
721
+ if (!reportPath) {
722
+ throw new Error('Usage: atris business record <report-path> [--summary "text"] [--metric name] [--outcome positive|mixed|negative] [--reward N]');
723
+ }
724
+ const absPath = path.resolve(cwd, reportPath);
725
+ if (!fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
726
+ throw new Error(`Report not found: ${reportPath}`);
727
+ }
728
+ const relPath = path.relative(cwd, absPath).replace(/\\/g, '/');
729
+ return { absPath, relPath };
730
+ }
731
+
732
+ function extractReportTitle(content, absPath) {
733
+ const heading = String(content || '').match(/^#\s+(.+)$/m);
734
+ if (heading) return heading[1].trim();
735
+ return path.basename(absPath, path.extname(absPath));
736
+ }
737
+
738
+ function normalizeOutcome(value) {
739
+ const normalized = String(value || 'recorded').trim().toLowerCase();
740
+ if (['positive', 'win', 'success', 'improved'].includes(normalized)) return 'positive';
741
+ if (['negative', 'loss', 'failed', 'regressed'].includes(normalized)) return 'negative';
742
+ if (['mixed', 'partial', 'unclear'].includes(normalized)) return 'mixed';
743
+ return 'recorded';
744
+ }
745
+
746
+ function defaultRewardForOutcome(outcome) {
747
+ if (outcome === 'positive') return 5;
748
+ if (outcome === 'negative') return -3;
749
+ if (outcome === 'mixed') return 1;
750
+ return 0;
751
+ }
752
+
753
+ function appendJsonl(filePath, record) {
754
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
755
+ fs.appendFileSync(filePath, `${JSON.stringify(record)}\n`, 'utf8');
756
+ }
757
+
758
+ function countJsonlRows(filePath) {
759
+ if (!fs.existsSync(filePath)) return 0;
760
+ return fs.readFileSync(filePath, 'utf8').split(/\r?\n/).filter(line => line.trim()).length;
761
+ }
762
+
763
+ function readJsonFile(filePath, fallback = null) {
764
+ if (!fs.existsSync(filePath)) return fallback;
765
+ try {
766
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
767
+ } catch {
768
+ return fallback;
769
+ }
770
+ }
771
+
772
+ function readJsonlRows(filePath) {
773
+ if (!fs.existsSync(filePath)) return [];
774
+ return fs.readFileSync(filePath, 'utf8')
775
+ .split(/\r?\n/)
776
+ .map(line => line.trim())
777
+ .filter(Boolean)
778
+ .map((line) => {
779
+ try { return JSON.parse(line); } catch { return null; }
780
+ })
781
+ .filter(Boolean);
782
+ }
783
+
784
+ function countFiles(dir, predicate = () => true) {
785
+ if (!fs.existsSync(dir)) return 0;
786
+ try {
787
+ return fs.readdirSync(dir).filter(predicate).length;
788
+ } catch {
789
+ return 0;
790
+ }
791
+ }
792
+
793
+ function latestMatchingFile(dir, predicate = () => true) {
794
+ if (!fs.existsSync(dir)) return null;
795
+ const rows = fs.readdirSync(dir)
796
+ .filter(predicate)
797
+ .map((name) => {
798
+ const full = path.join(dir, name);
799
+ let mtime = 0;
800
+ try { mtime = fs.statSync(full).mtimeMs; } catch {}
801
+ return { name, full, mtime };
802
+ })
803
+ .sort((a, b) => b.mtime - a.mtime);
804
+ return rows[0]?.full || null;
805
+ }
806
+
807
+ function rel(cwd, filePath) {
808
+ return filePath ? path.relative(cwd, filePath).replace(/\\/g, '/') : null;
809
+ }
810
+
811
+ function hasCloudWorkspaceId(value) {
812
+ const normalized = String(value || '').trim().toLowerCase();
813
+ return Boolean(normalized && normalized !== 'local-only');
814
+ }
815
+
816
+ function countBusinessTeamGoals(teamDir) {
817
+ if (!fs.existsSync(teamDir)) return { members: 0, activeGoalMembers: 0 };
818
+ let members = 0;
819
+ let activeGoalMembers = 0;
820
+ for (const name of fs.readdirSync(teamDir).filter(entry => !entry.startsWith('.'))) {
821
+ const memberDir = path.join(teamDir, name);
822
+ if (!fs.existsSync(path.join(memberDir, 'MEMBER.md'))) continue;
823
+ members++;
824
+ const goals = readJsonFile(path.join(memberDir, 'goals.json'), { goals: [] });
825
+ const activeGoals = Array.isArray(goals?.goals) ? goals.goals.filter(goal => goal.status === 'active') : [];
826
+ if (activeGoals.length > 0) activeGoalMembers++;
827
+ }
828
+ return { members, activeGoalMembers };
829
+ }
830
+
831
+ function collectBusinessOperatingState(cwd = process.cwd(), nowMs = Date.now()) {
832
+ const stateDir = path.join(cwd, '.atris', 'state');
833
+ const projection = readJsonFile(path.join(stateDir, 'tasks.projection.json'), { tasks: [] }) || { tasks: [] };
834
+ const tasks = Array.isArray(projection.tasks) ? projection.tasks : [];
835
+ const taskCounts = {
836
+ open: tasks.filter(task => task.status === 'open').length,
837
+ claimed: tasks.filter(task => task.status === 'claimed').length,
838
+ review: tasks.filter(task => task.status === 'review').length,
839
+ certifiedReview: tasks.filter(task => task.status === 'review' && task.metadata?.agent_certified).length,
840
+ blocked: tasks.filter(task => task.status === 'blocked').length,
841
+ };
842
+
843
+ const missionsById = new Map();
844
+ for (const mission of readJsonlRows(path.join(stateDir, 'missions.jsonl'))) {
845
+ if (mission?.id) missionsById.set(mission.id, mission);
846
+ }
847
+ const missions = [...missionsById.values()];
848
+ const terminal = new Set(['complete', 'completed', 'stopped', 'cancelled', 'done']);
849
+ const activeMissions = missions.filter(mission => !terminal.has(String(mission.status || '').toLowerCase()));
850
+ const staleMissions = activeMissions.filter((mission) => {
851
+ const status = String(mission.status || '').toLowerCase();
852
+ const lastTick = mission.last_tick_at ? Date.parse(mission.last_tick_at) : 0;
853
+ return status === 'running' && (!mission.verifier || !lastTick || nowMs - lastTick > 3 * 24 * 60 * 60 * 1000);
854
+ });
855
+ const missionEvents = readJsonlRows(path.join(stateDir, 'mission_events.jsonl'));
856
+ const codexGoal = readJsonFile(path.join(stateDir, 'codex_goal.json'), {}) || {};
857
+ const xp = readJsonFile(path.join(stateDir, 'career_xp.projection.json'), {}) || {};
858
+ const team = countBusinessTeamGoals(path.join(cwd, 'atris', 'team'));
859
+
860
+ return {
861
+ tasks: taskCounts,
862
+ missions: {
863
+ active: activeMissions.length,
864
+ running: activeMissions.filter(mission => mission.status === 'running').length,
865
+ alwaysOn: activeMissions.filter(mission => mission.always_on).length,
866
+ stale: staleMissions.length,
867
+ },
868
+ loop: {
869
+ ticks: missionEvents.filter(event => event.type === 'mission_tick').length,
870
+ codexGoal: codexGoal.goal?.objective || '',
871
+ },
872
+ team,
873
+ xp: {
874
+ metric: xp.metric_label || 'AgentXP',
875
+ total: Number(xp.total_agent_xp ?? xp.agent_xp ?? xp.total_xp ?? 0) || 0,
876
+ today: Number(xp.today_agent_xp ?? xp.today_xp ?? 0) || 0,
877
+ receipts: countJsonlRows(path.join(stateDir, 'career_xp_receipts.jsonl')),
878
+ integrity: xp.integrity_status || 'unknown',
879
+ },
880
+ };
881
+ }
882
+
883
+ function seedBusinessStarterTask(cwd, todoPath, starterAction) {
884
+ const title = `${starterAction.title}: ${starterAction.action}`;
885
+ try {
886
+ const taskDb = require('./task-db');
887
+ const db = taskDb.open();
888
+ const workspaceRoot = taskDb.workspaceRoot(cwd);
889
+ const sourceKey = taskDb.sourceKey(todoPath, title);
890
+ const result = taskDb.addTask(db, {
891
+ title,
892
+ tag: 'execute',
893
+ workspaceRoot,
894
+ sourceKey,
895
+ metadata: {
896
+ source: 'business_onboard',
897
+ todo_id: 'Onboard',
898
+ todo_tags: ['execute'],
899
+ business_slug: readWorkspaceBusinessMeta(cwd).slug || null,
900
+ verify: 'atris business record atris/reports/<recap>.md --outcome mixed --metric "operator speed"',
901
+ },
902
+ });
903
+ const projection = taskDb.taskProjection(db, { workspaceRoot, limit: 500 });
904
+ const projectionPath = path.join(cwd, '.atris', 'state', 'tasks.projection.json');
905
+ fs.mkdirSync(path.dirname(projectionPath), { recursive: true });
906
+ fs.writeFileSync(projectionPath, `${JSON.stringify(projection, null, 2)}\n`, 'utf8');
907
+ return { ok: true, inserted: Boolean(result.inserted), taskId: result.id, projectionPath };
908
+ } catch (error) {
909
+ return { ok: false, error: error && error.message ? error.message : String(error) };
910
+ }
911
+ }
912
+
913
+ function slugifyName(value) {
914
+ return String(value || '')
915
+ .toLowerCase()
916
+ .replace(/[^a-z0-9]+/g, '-')
917
+ .replace(/^-+|-+$/g, '') || 'item';
918
+ }
919
+
920
+ function collectBusinessShareState(cwd = process.cwd()) {
921
+ const bizMeta = readWorkspaceBusinessMeta(cwd);
922
+ const remoteReady = hasCloudWorkspaceId(bizMeta.business_id) && hasCloudWorkspaceId(bizMeta.workspace_id);
923
+ const reportsDir = path.join(cwd, 'atris', 'reports');
924
+ const briefsDir = path.join(cwd, 'atris', 'wiki', 'briefs');
925
+ const conceptsDir = path.join(cwd, 'atris', 'wiki', 'concepts');
926
+ const ingestDir = path.join(cwd, 'atris', 'context', '_ingest');
927
+ const teamDir = path.join(cwd, 'atris', 'team');
928
+ const teamStartPath = path.join(teamDir, 'START_HERE.md');
929
+ const stateDir = path.join(cwd, '.atris', 'state');
930
+ const rootAgentAdapterNames = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'];
931
+ const rootAgentAdapters = rootAgentAdapterNames.filter(name => fs.existsSync(path.join(cwd, name)));
932
+ const scaffold = {
933
+ map: fs.existsSync(path.join(cwd, 'atris', 'MAP.md')),
934
+ todo: fs.existsSync(path.join(cwd, 'atris', 'TODO.md')),
935
+ persona: fs.existsSync(path.join(cwd, 'atris', 'PERSONA.md')),
936
+ runtime: fs.existsSync(path.join(stateDir, 'runtime.json')),
937
+ sync: fs.existsSync(path.join(stateDir, '_sync.json')),
938
+ };
939
+ const starterBrief = latestMatchingFile(briefsDir, name => /starter-brief\.md$/i.test(name));
940
+ const expectedFirstLoop = `${bizMeta.slug || slugifyName(bizMeta.name)}-first-loop.md`;
941
+ const firstLoop = latestMatchingFile(conceptsDir, name => name === expectedFirstLoop);
942
+ const onePager = latestMatchingFile(reportsDir, name => /one-pager|cheat-sheet|onboarding/i.test(name));
943
+ const reports = countFiles(reportsDir, name => /\.(md|json)$/i.test(name));
944
+ const teamMembers = countFiles(teamDir, name => !name.startsWith('.') && fs.existsSync(path.join(teamDir, name, 'MEMBER.md')));
945
+ const events = countJsonlRows(path.join(stateDir, 'events.jsonl'));
946
+ const episodes = countJsonlRows(path.join(stateDir, 'episodes.jsonl'));
947
+ const scorecards = countJsonlRows(path.join(stateDir, 'scorecards.jsonl'));
948
+ const missing = [];
949
+ if (!scaffold.map || !scaffold.todo || !scaffold.persona) missing.push('canonical Atris scaffold');
950
+ if (!scaffold.runtime) missing.push('runtime receipt');
951
+ if (!scaffold.sync) missing.push('sync state');
952
+ if (!starterBrief) missing.push('starter brief');
953
+ if (!firstLoop) missing.push('first loop');
954
+ if (!onePager) missing.push('operator one-pager');
955
+ if (teamMembers < 1) missing.push('team member lanes');
956
+ if (!fs.existsSync(teamStartPath)) missing.push('team start guide');
957
+ if (rootAgentAdapters.length < rootAgentAdapterNames.length) missing.push('root agent adapters');
958
+ if (scorecards < 1 && events < 1 && episodes < 1) missing.push('first proof recap');
959
+
960
+ return {
961
+ bizMeta,
962
+ scaffold,
963
+ starterBrief: rel(cwd, starterBrief),
964
+ firstLoop: rel(cwd, firstLoop),
965
+ onePager: rel(cwd, onePager),
966
+ teamStart: rel(cwd, fs.existsSync(teamStartPath) ? teamStartPath : null),
967
+ rootAgentAdapters,
968
+ missingRootAgentAdapters: rootAgentAdapterNames.filter(name => !rootAgentAdapters.includes(name)),
969
+ remoteReady,
970
+ os: collectBusinessOperatingState(cwd),
971
+ ingestPacks: countFiles(ingestDir, name => !name.startsWith('.')),
972
+ reports,
973
+ teamMembers,
974
+ proof: { events, episodes, scorecards },
975
+ ready: missing.length === 0,
976
+ missing,
977
+ };
978
+ }
979
+
980
+ function renderBusinessOsLines(os = {}, prefix = '- ') {
981
+ const tasks = os.tasks || {};
982
+ const missions = os.missions || {};
983
+ const team = os.team || {};
984
+ const xp = os.xp || {};
985
+ const loop = os.loop || {};
986
+ return [
987
+ `${prefix}Tasks: ${tasks.open || 0} open, ${tasks.claimed || 0} claimed, ${tasks.review || 0} review (${tasks.certifiedReview || 0} certified), ${tasks.blocked || 0} blocked`,
988
+ `${prefix}Missions: ${missions.active || 0} active, ${missions.running || 0} running, ${missions.alwaysOn || 0} always-on, ${missions.stale || 0} stale/no-verifier`,
989
+ `${prefix}Team goals: ${team.members || 0} member lanes, ${team.activeGoalMembers || 0} with active goals`,
990
+ `${prefix}${xp.metric || 'AgentXP'}: ${xp.total || 0} total, ${xp.today || 0} today, ${xp.receipts || 0} receipts, integrity ${xp.integrity || 'unknown'}`,
991
+ `${prefix}Loop: ${loop.ticks || 0} mission ticks; Codex goal ${loop.codexGoal || 'none'}`,
992
+ `${prefix}XP gate: proof can move to Review; XP is awarded only after human approval`,
993
+ ];
994
+ }
995
+
996
+ function shellDoubleQuote(value) {
997
+ return `"${String(value || '').replace(/(["\\$`])/g, '\\$1')}"`;
998
+ }
999
+
1000
+ function renderBusinessMissionBootstrapLines(bizMeta = {}, prefix = '') {
1001
+ const missionTitle = shellDoubleQuote(`Run the first useful loop for ${bizMeta.name || bizMeta.slug || 'this business'}`);
1002
+ return [
1003
+ `${prefix}atris mission status --status active --json`,
1004
+ `${prefix}# If no active mission exists:`,
1005
+ `${prefix}atris mission start ${missionTitle} --owner operator --runner codex_goal --lane business --verify "atris business check" --stop "first proof recap recorded"`,
1006
+ `${prefix}atris member goal-from-mission operator`,
1007
+ ];
1008
+ }
1009
+
1010
+ function renderBusinessMissingAction(missing) {
1011
+ if (!missing) return 'Start from the first loop, ship one small artifact, then record the recap.';
1012
+ if (missing === 'root agent adapters') {
1013
+ return 'Run `atris update` to restore root AGENTS.md, CLAUDE.md, and GEMINI.md adapters.';
1014
+ }
1015
+ return `Add ${missing}.`;
1016
+ }
1017
+
1018
+ function renderBusinessCreatedNextSteps(bizMeta = {}, workspaceRoot = '.') {
1019
+ const lines = [
1020
+ ' Atris: seeded local computer + operator + validator',
1021
+ '',
1022
+ ' Start here:',
1023
+ ` cd ${workspaceRoot}`,
1024
+ ' atris',
1025
+ ' atris business start',
1026
+ ' atris radar',
1027
+ ' atris task next',
1028
+ ' atris member activate operator',
1029
+ ...renderBusinessMissionBootstrapLines(bizMeta, ' '),
1030
+ '',
1031
+ ' First loop:',
1032
+ ' atris business onboard --website <url> --contact "Name" --note "what they do"',
1033
+ ' atris do',
1034
+ ' atris business record atris/reports/<recap>.md --outcome mixed --metric "operator speed"',
1035
+ ' atris business share --write',
1036
+ '',
1037
+ ' Sync when ready:',
1038
+ ' atris sync --dry-run',
1039
+ ' atris sync',
1040
+ ];
1041
+ return lines.join('\n');
1042
+ }
1043
+
1044
+ function renderBusinessShareHandoff(state, options = {}) {
1045
+ const { bizMeta } = state;
1046
+ const role = options.role || 'collaborator';
1047
+ const person = options.name || role;
1048
+ const workspacePath = options.cwd || process.cwd();
1049
+ const firstMissing = state.missing[0] || null;
1050
+ const lines = [
1051
+ `# ${bizMeta.name} Share Handoff`,
1052
+ '',
1053
+ `For: ${person}${options.email ? ` <${options.email}>` : ''}`,
1054
+ `Role: ${role}`,
1055
+ `Business: ${bizMeta.name} (${bizMeta.slug})`,
1056
+ `Business ID: ${bizMeta.business_id || 'local-only'}`,
1057
+ `Workspace ID: ${bizMeta.workspace_id || 'local-only'}`,
1058
+ `Local path: ${workspacePath}`,
1059
+ `Ready to share: ${state.ready ? 'yes' : 'no'}`,
1060
+ `Remote pull: ${state.remoteReady ? 'available' : 'local-only'}`,
1061
+ `Agent setup: ${(state.missingRootAgentAdapters && state.missingRootAgentAdapters.length) ? 'missing root agent adapters' : 'ready'}`,
1062
+ '',
1063
+ '## Get The Workspace',
1064
+ '',
1065
+ 'If this folder is already on your machine:',
1066
+ '',
1067
+ '```bash',
1068
+ `cd ${workspacePath}`,
1069
+ 'atris business start',
1070
+ '```',
1071
+ '',
1072
+ state.remoteReady ? 'If you need to pull it first:' : 'Remote pull is not available yet:',
1073
+ '',
1074
+ ...(state.remoteReady ? [
1075
+ '```bash',
1076
+ `atris pull ${bizMeta.slug}`,
1077
+ `cd ${bizMeta.slug}`,
1078
+ 'atris sync --dry-run',
1079
+ 'atris business start',
1080
+ '```',
1081
+ ] : [
1082
+ '- This workspace is local-only because it is missing a cloud business ID or workspace ID.',
1083
+ '- Share the folder directly, or create/pull the cloud business workspace before sending this handoff.',
1084
+ ]),
1085
+ '',
1086
+ '## Start Here',
1087
+ '',
1088
+ '```bash',
1089
+ `cd ${workspacePath}`,
1090
+ 'atris',
1091
+ 'atris business start',
1092
+ 'atris radar',
1093
+ 'atris task next',
1094
+ 'atris member activate operator',
1095
+ ...renderBusinessMissionBootstrapLines(bizMeta),
1096
+ '```',
1097
+ '',
1098
+ '## What To Read',
1099
+ '',
1100
+ `- Protocol: atris/atris.md`,
1101
+ `- Map: atris/MAP.md`,
1102
+ `- Queue: atris/TODO.md`,
1103
+ `- Team start: ${state.teamStart || 'missing'}`,
1104
+ `- Starter brief: ${state.starterBrief || 'missing'}`,
1105
+ `- First loop: ${state.firstLoop || 'missing'}`,
1106
+ `- Operator one-pager: ${state.onePager || 'missing'}`,
1107
+ '',
1108
+ '## First Useful Loop',
1109
+ '',
1110
+ '```bash',
1111
+ 'atris business onboard --website <url> --contact "Name" --note "what changed"',
1112
+ state.remoteReady ? 'atris sync --dry-run' : '# local-only: no cloud sync is available yet',
1113
+ 'atris task next',
1114
+ ...renderBusinessMissionBootstrapLines(bizMeta),
1115
+ 'atris do',
1116
+ 'atris business record atris/reports/<recap>.md --outcome mixed --metric "operator speed"',
1117
+ 'atris business share --write',
1118
+ state.remoteReady ? 'atris sync' : '# local-only: share the folder directly',
1119
+ '```',
1120
+ '',
1121
+ '## Proof State',
1122
+ '',
1123
+ `- Team lanes: ${state.teamMembers}`,
1124
+ `- Onboarding packs: ${state.ingestPacks}`,
1125
+ `- Reports: ${state.reports}`,
1126
+ `- Events: ${state.proof.events}`,
1127
+ `- Episodes: ${state.proof.episodes}`,
1128
+ `- Scorecards: ${state.proof.scorecards}`,
1129
+ '',
1130
+ '## Atris OS State',
1131
+ '',
1132
+ ...renderBusinessOsLines(state.os),
1133
+ '',
1134
+ 'Useful commands:',
1135
+ '',
1136
+ '```bash',
1137
+ state.remoteReady ? 'atris sync --status' : '# local-only: no cloud sync status yet',
1138
+ state.remoteReady ? 'atris sync --watch' : '# local-only: no cloud watcher yet',
1139
+ 'atris radar',
1140
+ 'atris task next',
1141
+ ...renderBusinessMissionBootstrapLines(bizMeta),
1142
+ 'atris xp status --local --json',
1143
+ '```',
1144
+ '',
1145
+ '## Next Action',
1146
+ '',
1147
+ `- ${renderBusinessMissingAction(firstMissing)}`,
1148
+ '',
1149
+ '## Guardrails',
1150
+ '',
1151
+ '- Do not mix another business into this workspace.',
1152
+ '- No external sends without operator approval.',
1153
+ '- No XP until proof is accepted by a human.',
1154
+ '',
1155
+ ];
1156
+ return lines.join('\n');
1157
+ }
1158
+
1159
+ function renderBusinessStartCard(state, options = {}) {
1160
+ const { bizMeta } = state;
1161
+ const workspacePath = options.cwd || process.cwd();
1162
+ const firstMissing = state.missing[0] || null;
1163
+ const lines = [
1164
+ `${bizMeta.name} collaborator start`,
1165
+ '',
1166
+ `Business: ${bizMeta.name} (${bizMeta.slug})`,
1167
+ `Workspace: ${workspacePath}`,
1168
+ `Ready: ${state.ready ? 'yes' : 'no'}`,
1169
+ `Remote pull: ${state.remoteReady ? 'available' : 'local-only'}`,
1170
+ `Agent setup: ${(state.missingRootAgentAdapters && state.missingRootAgentAdapters.length) ? 'missing root agent adapters' : 'ready'}`,
1171
+ '',
1172
+ 'Read:',
1173
+ `- atris/atris.md`,
1174
+ `- atris/MAP.md`,
1175
+ `- atris/TODO.md`,
1176
+ `- ${state.teamStart || 'missing team start guide'}`,
1177
+ `- ${state.starterBrief || 'missing starter brief'}`,
1178
+ `- ${state.firstLoop || 'missing first loop'}`,
1179
+ `- ${state.onePager || 'missing operator one-pager'}`,
1180
+ '',
1181
+ 'Run:',
1182
+ state.remoteReady ? ' atris sync --dry-run' : ' # local-only: no cloud sync is available yet',
1183
+ ' atris',
1184
+ ' atris business start',
1185
+ ' atris radar',
1186
+ ' atris task next',
1187
+ ' atris member activate operator',
1188
+ ...renderBusinessMissionBootstrapLines(bizMeta, ' '),
1189
+ ' atris do',
1190
+ ' atris business record atris/reports/<recap>.md --outcome mixed --metric "operator speed"',
1191
+ ' atris business share --write',
1192
+ state.remoteReady ? ' atris sync' : ' # local-only: share the folder directly',
1193
+ state.remoteReady ? ' atris sync --watch' : ' # local-only: no cloud watcher yet',
1194
+ '',
1195
+ 'Proof:',
1196
+ `- team lanes: ${state.teamMembers}`,
1197
+ `- onboarding packs: ${state.ingestPacks}`,
1198
+ `- scorecards: ${state.proof.scorecards}`,
1199
+ '',
1200
+ 'OS:',
1201
+ ...renderBusinessOsLines(state.os, ' '),
1202
+ '',
1203
+ 'Next:',
1204
+ `- ${state.ready ? 'Work the first loop, record the recap, then rewrite the share handoff.' : renderBusinessMissingAction(firstMissing)}`,
1205
+ '',
1206
+ ];
1207
+ return lines.join('\n');
1208
+ }
1209
+
1210
+ async function startBusinessWorkspace(...args) {
1211
+ const options = parseShareFlags(args, process.cwd());
1212
+ const state = collectBusinessShareState(options.cwd);
1213
+ const content = renderBusinessStartCard(state, options);
1214
+ console.log(content);
1215
+ return state;
1216
+ }
1217
+
1218
+ // Local calendar day; toISOString is UTC and dates evening work tomorrow
1219
+ // anywhere west of Greenwich.
1220
+ function localDay(now = new Date()) {
1221
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
1222
+ }
1223
+
1224
+ function defaultSharePath(cwd, role) {
1225
+ const stamp = localDay();
1226
+ const roleSlug = slugifyName(role || 'collaborator');
1227
+ return path.join(cwd, 'atris', 'reports', `${stamp}-share-${roleSlug}.md`);
1228
+ }
1229
+
1230
+ async function shareBusinessWorkspace(...args) {
1231
+ const options = parseShareFlags(args, process.cwd());
1232
+ const state = collectBusinessShareState(options.cwd);
1233
+ const content = renderBusinessShareHandoff(state, options);
1234
+ console.log(content);
1235
+
1236
+ if (options.write || options.out) {
1237
+ const outputPath = options.out
1238
+ ? (path.isAbsolute(options.out) ? options.out : path.join(options.cwd, options.out))
1239
+ : defaultSharePath(options.cwd, options.role);
1240
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
1241
+ fs.writeFileSync(outputPath, content, 'utf8');
1242
+ console.log(`Wrote ${path.relative(options.cwd, outputPath).replace(/\\/g, '/')}`);
1243
+ return outputPath;
1244
+ }
1245
+
1246
+ return state;
1247
+ }
1248
+
1249
+ function upsertIndexEntry(indexPath, sectionName, relativePath, description) {
1250
+ const normalizedPath = relativePath.replace(/\\/g, '/');
1251
+ const entryLine = `- [[${normalizedPath}]] - ${description}`;
1252
+ let lines = fs.readFileSync(indexPath, 'utf8').split('\n');
1253
+ const existingIndex = lines.findIndex((line) => line.includes(`[[${normalizedPath}]]`));
1254
+ if (existingIndex >= 0) {
1255
+ lines[existingIndex] = entryLine;
1256
+ fs.writeFileSync(indexPath, `${lines.join('\n').replace(/\n*$/, '\n')}`, 'utf8');
1257
+ return;
1258
+ }
1259
+
1260
+ const header = `## ${sectionName}`;
1261
+ const sectionIndex = lines.findIndex((line) => line.trim() === header);
1262
+ if (sectionIndex === -1) return;
1263
+
1264
+ let insertAt = sectionIndex + 1;
1265
+ while (insertAt < lines.length && !/^##\s+/.test(lines[insertAt])) {
1266
+ insertAt++;
1267
+ }
1268
+
1269
+ lines.splice(insertAt, 0, entryLine);
1270
+ fs.writeFileSync(indexPath, `${lines.join('\n').replace(/\n*$/, '\n')}`, 'utf8');
1271
+ }
1272
+
1273
+ function writeMarkdownWithFrontmatter(filePath, frontmatter, body) {
1274
+ const yaml = Object.entries(frontmatter).map(([key, value]) => {
1275
+ if (Array.isArray(value)) {
1276
+ return `${key}:\n${value.map((item) => ` - ${item}`).join('\n')}`;
1277
+ }
1278
+ return `${key}: ${value}`;
1279
+ }).join('\n');
1280
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1281
+ fs.writeFileSync(filePath, `---\n${yaml}\n---\n\n${body.trim()}\n`, 'utf8');
1282
+ }
1283
+
1284
+ function walkOnboardingFiles(dir, options = {}) {
1285
+ const skipDirs = new Set(['.git', '.atris', 'atris', '_ingest', 'node_modules', 'dist', 'build', 'coverage', '.next']);
1286
+ const allowedExt = new Set(['.md', '.txt', '.pdf', '.csv', '.json', '.html', '.htm', '.docx', '.xlsx', '.png', '.jpg', '.jpeg']);
1287
+ const maxFiles = options.maxFiles || 25;
1288
+ const output = [];
1289
+
1290
+ function walk(currentDir) {
1291
+ if (!fs.existsSync(currentDir) || output.length >= maxFiles) return;
1292
+ for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
1293
+ if (output.length >= maxFiles) break;
1294
+ const fullPath = path.join(currentDir, entry.name);
1295
+ if (entry.isDirectory()) {
1296
+ if (skipDirs.has(entry.name)) continue;
1297
+ walk(fullPath);
1298
+ continue;
1299
+ }
1300
+ if (!entry.isFile()) continue;
1301
+ if (entry.name.startsWith('.')) continue;
1302
+ if (!allowedExt.has(path.extname(entry.name).toLowerCase())) continue;
1303
+ output.push(fullPath);
1304
+ }
1305
+ }
1306
+
1307
+ walk(dir);
1308
+ return output;
1309
+ }
1310
+
1311
+ function extractUrlsFromText(text) {
1312
+ return Array.from(new Set((String(text || '').match(/https?:\/\/[^\s)<>"']+/g) || []).map((item) => item.replace(/[.,]$/, ''))));
1313
+ }
1314
+
1315
+ function isTextLike(filePath) {
1316
+ return new Set(['.md', '.txt', '.json', '.csv', '.html', '.htm']).has(path.extname(filePath).toLowerCase());
1317
+ }
1318
+
1319
+ function readSmallText(filePath, maxBytes = 200000) {
1320
+ try {
1321
+ const stat = fs.statSync(filePath);
1322
+ if (stat.size > maxBytes || !isTextLike(filePath)) return null;
1323
+ return fs.readFileSync(filePath, 'utf8');
1324
+ } catch {
1325
+ return null;
1326
+ }
1327
+ }
1328
+
1329
+ function discoverOnboardingSignals(cwd, options = {}) {
1330
+ const explicitSourcePaths = (options.sources || [])
1331
+ .map((value) => path.resolve(cwd, value))
1332
+ .filter((fullPath) => fs.existsSync(fullPath));
1333
+
1334
+ const rootCandidates = walkOnboardingFiles(cwd, { maxFiles: 20 })
1335
+ .filter((fullPath) => {
1336
+ const relative = path.relative(cwd, fullPath).replace(/\\/g, '/');
1337
+ return !relative.startsWith('atris/') && !relative.startsWith('.atris/');
1338
+ });
1339
+
1340
+ const contextDir = path.join(cwd, 'atris', 'context');
1341
+ const contextCandidates = walkOnboardingFiles(contextDir, { maxFiles: 20 })
1342
+ .filter((fullPath) => {
1343
+ const relative = path.relative(contextDir, fullPath).replace(/\\/g, '/');
1344
+ return !relative.startsWith('_ingest/') && relative !== 'README.md' && relative !== 'live-workspace.md';
1345
+ });
1346
+
1347
+ const sourcePaths = Array.from(new Set([...explicitSourcePaths, ...rootCandidates, ...contextCandidates]));
1348
+ const urls = new Set([options.website, ...(options.links || [])].filter(Boolean));
1349
+
1350
+ for (const note of options.notes || []) {
1351
+ for (const url of extractUrlsFromText(note)) urls.add(url);
1352
+ }
1353
+
1354
+ for (const sourcePath of sourcePaths) {
1355
+ const text = readSmallText(sourcePath);
1356
+ if (!text) continue;
1357
+ for (const url of extractUrlsFromText(text)) urls.add(url);
1358
+ }
1359
+
1360
+ return {
1361
+ website: options.website || Array.from(urls)[0] || '',
1362
+ urls: Array.from(urls),
1363
+ sourcePaths,
1364
+ };
1365
+ }
1366
+
1367
+ function stageOnboardingSources(cwd, packDir, sourcePaths = []) {
1368
+ const stagedDir = path.join(packDir, 'sources');
1369
+ fs.mkdirSync(stagedDir, { recursive: true });
1370
+ const stagedEntries = [];
1371
+ let counter = 0;
1372
+
1373
+ for (const sourcePath of sourcePaths) {
1374
+ if (!fs.existsSync(sourcePath)) continue;
1375
+ counter += 1;
1376
+ const baseName = path.basename(sourcePath);
1377
+ const targetPath = path.join(stagedDir, `${String(counter).padStart(2, '0')}-${baseName}`);
1378
+ const stat = fs.statSync(sourcePath);
1379
+ if (stat.isDirectory()) {
1380
+ fs.cpSync(sourcePath, targetPath, { recursive: true });
1381
+ } else {
1382
+ fs.copyFileSync(sourcePath, targetPath);
1383
+ }
1384
+ stagedEntries.push({
1385
+ original: path.relative(cwd, sourcePath).replace(/\\/g, '/'),
1386
+ staged: path.relative(cwd, targetPath).replace(/\\/g, '/'),
1387
+ kind: stat.isDirectory() ? 'directory' : 'file',
1388
+ });
1389
+ }
1390
+
1391
+ return stagedEntries;
1392
+ }
1393
+
1394
+ function suggestStarterAction(signals) {
1395
+ if (signals.contactEmail && signals.website) {
1396
+ return {
1397
+ title: 'Draft a founder-context note',
1398
+ 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.`,
1399
+ why: 'This is the shortest safe path to real feedback from a named human.',
1400
+ };
1401
+ }
1402
+ if (signals.website) {
1403
+ return {
1404
+ title: 'Map the offer into one loop',
1405
+ action: 'Read the website and turn it into one measurable workflow with a clear reward signal.',
1406
+ why: 'A website is enough to define a first useful business loop without waiting for perfect intake.',
1407
+ };
1408
+ }
1409
+ if ((signals.sourceEntries || []).length > 0) {
1410
+ return {
1411
+ title: 'Extract the first workflow from local evidence',
1412
+ action: 'Read the strongest local source, summarize what the company does, and choose one workflow worth operationalizing first.',
1413
+ why: 'Local evidence is already better than a blank template and can anchor the first action.',
1414
+ };
1415
+ }
1416
+ return {
1417
+ title: 'Collect one anchor signal',
1418
+ action: 'Get one website, one named human, or one source doc so the environment can stop guessing.',
1419
+ why: 'The system can work from partial input, but it still needs one concrete anchor.',
1420
+ };
1421
+ }
1422
+
1423
+ async function onboardBusiness(...flags) {
1424
+ const options = parseOnboardFlags(flags, process.cwd());
1425
+ const cwd = options.cwd || process.cwd();
1426
+
1427
+ const bizFile = path.join(cwd, '.atris', 'business.json');
1428
+ if (!fs.existsSync(bizFile) && options.name) {
1429
+ const slug = slugifyName(options.name);
1430
+ createCanonicalBusinessWorkspace(cwd, {
1431
+ business_id: '',
1432
+ workspace_id: '',
1433
+ name: options.name,
1434
+ slug,
1435
+ owner_email: '',
1436
+ workspace_template: 'business',
1437
+ }, { here: true });
1438
+ }
1439
+
1440
+ const bizMeta = readWorkspaceBusinessMeta(cwd);
1441
+
1442
+ ensureWorkspaceStateFiles(cwd, {
1443
+ slug: bizMeta.slug || 'business',
1444
+ business_id: bizMeta.business_id || '',
1445
+ workspace_id: bizMeta.workspace_id || '',
1446
+ workspace_template: bizMeta.workspace_template || 'business',
1447
+ }, { dryRun: false });
1448
+
1449
+ const contextDir = ensureContextScaffold(cwd, 'public');
1450
+ const stamp = new Date().toISOString().replace(/[:]/g, '-').slice(0, 16);
1451
+ const packDir = path.join(contextDir, '_ingest', `${stamp}-onboarding`);
1452
+ fs.mkdirSync(packDir, { recursive: true });
1453
+
1454
+ const discovered = discoverOnboardingSignals(cwd, options);
1455
+ const stagedSources = stageOnboardingSources(cwd, packDir, discovered.sourcePaths);
1456
+ const links = Array.from(new Set([discovered.website, ...discovered.urls].filter(Boolean)));
1457
+ const starterAction = suggestStarterAction({
1458
+ website: discovered.website,
1459
+ contactName: options.contactName,
1460
+ contactEmail: options.contactEmail,
1461
+ sourceEntries: stagedSources,
1462
+ });
1463
+ const intakeLines = [
1464
+ `# ${bizMeta.name} Onboarding Intake`,
1465
+ '',
1466
+ `- Business: ${bizMeta.name}`,
1467
+ `- Slug: ${bizMeta.slug}`,
1468
+ discovered.website ? `- Website: ${discovered.website}` : null,
1469
+ options.contactName ? `- Contact: ${options.contactName}` : null,
1470
+ options.contactRole ? `- Contact role: ${options.contactRole}` : null,
1471
+ options.contactEmail ? `- Contact email: ${options.contactEmail}` : null,
1472
+ '',
1473
+ '## Notes',
1474
+ ...(options.notes.length > 0 ? options.notes.map((note) => `- ${note}`) : ['- No notes captured yet.']),
1475
+ '',
1476
+ '## Discovered Sources',
1477
+ ...(stagedSources.length > 0 ? stagedSources.map((entry) => `- ${entry.original} -> ${entry.staged}`) : ['- No local files discovered yet.']),
1478
+ '',
1479
+ '## Links',
1480
+ ...(links.length > 0 ? links.map((link) => `- ${link}`) : ['- No links captured yet.']),
1481
+ ].filter(Boolean);
1482
+ const intakePath = path.join(packDir, 'intake.md');
1483
+ fs.writeFileSync(intakePath, `${intakeLines.join('\n')}\n`, 'utf8');
1484
+
1485
+ const linksPath = path.join(packDir, 'links.txt');
1486
+ fs.writeFileSync(linksPath, `${links.join('\n')}${links.length > 0 ? '\n' : ''}`, 'utf8');
1487
+ const sourcesPath = path.join(packDir, 'sources.txt');
1488
+ fs.writeFileSync(
1489
+ sourcesPath,
1490
+ `${stagedSources.map((entry) => `${entry.original} -> ${entry.staged}`).join('\n')}${stagedSources.length > 0 ? '\n' : ''}`,
1491
+ 'utf8'
1492
+ );
1493
+
1494
+ const intakeRel = path.relative(cwd, intakePath).replace(/\\/g, '/');
1495
+ const linksRel = path.relative(cwd, linksPath).replace(/\\/g, '/');
1496
+ const sourcesRel = path.relative(cwd, sourcesPath).replace(/\\/g, '/');
1497
+ const today = localDay();
1498
+
1499
+ const briefSlug = `${bizMeta.slug}-starter-brief`;
1500
+ const briefPath = path.join(cwd, 'atris', 'wiki', 'briefs', `${briefSlug}.md`);
1501
+ writeMarkdownWithFrontmatter(briefPath, {
1502
+ type: 'brief',
1503
+ slug: briefSlug,
1504
+ title: `${bizMeta.name} Starter Brief`,
1505
+ sources: [intakeRel, linksRel, sourcesRel],
1506
+ last_compiled: today,
1507
+ created: today,
1508
+ updated: today,
1509
+ tags: ['business', 'onboarding', 'starter'],
1510
+ }, `
1511
+ # ${bizMeta.name} Starter Brief
1512
+
1513
+ ## What We Know
1514
+
1515
+ - Website: ${discovered.website || 'unknown'}
1516
+ - Contact: ${options.contactName || 'unknown'}
1517
+ - Contact role: ${options.contactRole || 'unknown'}
1518
+ - Contact email: ${options.contactEmail || 'unknown'}
1519
+ ${options.notes.map((note) => `- Note: ${note}`).join('\n') || '- Notes: none captured yet'}
1520
+ ${stagedSources.length > 0 ? `- Local sources discovered: ${stagedSources.length}` : '- Local sources discovered: 0'}
1521
+
1522
+ ## Unknowns
1523
+
1524
+ - Primary customer or audience
1525
+ - Revenue model and buying motion
1526
+ - Main operator inside the business
1527
+ - Tool stack and source systems
1528
+ - First measurable operating loop
1529
+
1530
+ ## Next Moves
1531
+
1532
+ - Read the staged intake in \`${intakeRel}\`
1533
+ - ${starterAction.action}
1534
+ - Turn the first real interaction into a recap, then run \`atris business record ...\`
1535
+ `);
1536
+ upsertIndexEntry(path.join(cwd, 'atris', 'wiki', 'index.md'), 'Briefs', path.relative(cwd, briefPath), 'Starter business brief from onboarding intake');
1537
+
1538
+ let personRelativePath = null;
1539
+ if (options.contactName) {
1540
+ const personSlug = slugifyName(options.contactName);
1541
+ const personPath = path.join(cwd, 'atris', 'wiki', 'people', `${personSlug}.md`);
1542
+ writeMarkdownWithFrontmatter(personPath, {
1543
+ type: 'person',
1544
+ slug: personSlug,
1545
+ title: options.contactName,
1546
+ sources: [intakeRel, sourcesRel],
1547
+ last_compiled: today,
1548
+ created: today,
1549
+ updated: today,
1550
+ tags: ['person', 'contact', 'onboarding'],
1551
+ }, `
1552
+ # ${options.contactName}
1553
+
1554
+ ## Known
1555
+
1556
+ - Business: ${bizMeta.name}
1557
+ - Role: ${options.contactRole || 'unknown'}
1558
+ - Email: ${options.contactEmail || 'unknown'}
1559
+
1560
+ ## Unknown
1561
+
1562
+ - Decision authority
1563
+ - Preferred communication rhythm
1564
+ - Main business pain
1565
+
1566
+ ## Cross-References
1567
+
1568
+ - [[atris/wiki/briefs/${path.basename(briefPath)}]] - starter brief
1569
+ `);
1570
+ personRelativePath = path.relative(cwd, personPath).replace(/\\/g, '/');
1571
+ upsertIndexEntry(path.join(cwd, 'atris', 'wiki', 'index.md'), 'People', personRelativePath, `Seed contact for ${bizMeta.name}`);
1572
+ }
1573
+
1574
+ const conceptSlug = `${bizMeta.slug}-first-loop`;
1575
+ const conceptPath = path.join(cwd, 'atris', 'wiki', 'concepts', `${conceptSlug}.md`);
1576
+ writeMarkdownWithFrontmatter(conceptPath, {
1577
+ type: 'concept',
1578
+ slug: conceptSlug,
1579
+ title: `${bizMeta.name} First Loop`,
1580
+ sources: [intakeRel, linksRel, sourcesRel],
1581
+ last_compiled: today,
1582
+ created: today,
1583
+ updated: today,
1584
+ tags: ['concept', 'loop', 'onboarding'],
1585
+ }, `
1586
+ # ${bizMeta.name} First Loop
1587
+
1588
+ ## Candidate Loop
1589
+
1590
+ - Trigger: a new lead, meeting, client request, or operator handoff
1591
+ - Action: summarize context, propose the next move, and draft one concrete output
1592
+ - Reward: operator approval, reply, booked meeting, or visible pipeline progress
1593
+
1594
+ ## Known Signals
1595
+
1596
+ ${links.map((link) => `- ${link}`).join('\n') || '- No external links captured yet'}
1597
+ ${stagedSources.length > 0 ? `- Local evidence files: ${stagedSources.length}` : ''}
1598
+
1599
+ ## Unknowns
1600
+
1601
+ - Best first workflow to automate
1602
+ - Exact reward signal
1603
+ - Required integrations
1604
+ `);
1605
+ upsertIndexEntry(path.join(cwd, 'atris', 'wiki', 'index.md'), 'Concepts', path.relative(cwd, conceptPath), 'Seed first-loop hypothesis from onboarding intake');
1606
+
1607
+ const cheatSheetPath = path.join(cwd, 'atris', 'reports', `${today}-${bizMeta.slug}-onboarding-cheat-sheet.md`);
1608
+ const onePagerPath = path.join(cwd, 'atris', 'reports', `${today}-${bizMeta.slug}-operator-one-pager.md`);
1609
+ const operatorSummary = [
1610
+ `# ${bizMeta.name} Onboarding Cheat Sheet`,
1611
+ '',
1612
+ '## What Exists',
1613
+ `- Starter brief: ${path.relative(cwd, briefPath).replace(/\\/g, '/')}`,
1614
+ personRelativePath ? `- Contact page: ${personRelativePath}` : null,
1615
+ `- First loop page: ${path.relative(cwd, conceptPath).replace(/\\/g, '/')}`,
1616
+ `- Raw intake: ${intakeRel}`,
1617
+ `- Source list: ${sourcesRel}`,
1618
+ stagedSources.length > 0 ? `- Staged sources: ${stagedSources.length}` : '- Staged sources: 0',
1619
+ '',
1620
+ '## Best Next Action',
1621
+ `- ${starterAction.title}`,
1622
+ `- Action: ${starterAction.action}`,
1623
+ `- Why: ${starterAction.why}`,
1624
+ '- Swarlo join: placeholder preserved for the next live join step.',
1625
+ '',
1626
+ '## Next 3 Moves',
1627
+ '- Open the starter brief and correct anything false.',
1628
+ `- ${starterAction.action}`,
1629
+ '- After the first real run, write a recap and record it with `atris business record ...`.',
1630
+ '- Before sharing the workspace, run `atris business share --write` and send the handoff.',
1631
+ ].filter(Boolean).join('\n') + '\n';
1632
+ fs.writeFileSync(cheatSheetPath, operatorSummary, 'utf8');
1633
+ fs.writeFileSync(onePagerPath, operatorSummary.replace('# ', '# One Pager: '), 'utf8');
1634
+
1635
+ const todoPath = path.join(cwd, 'atris', 'TODO.md');
1636
+ if (fs.existsSync(todoPath)) {
1637
+ let todoContent = fs.readFileSync(todoPath, 'utf8');
1638
+ const taskLine = `- **Onboard:** ${starterAction.title}: ${starterAction.action} [execute]\n`;
1639
+ const backlogMatch = todoContent.match(/^## Backlog\s*$/m);
1640
+ if (backlogMatch) {
1641
+ const insertAt = backlogMatch.index + backlogMatch[0].length;
1642
+ todoContent = todoContent.slice(0, insertAt) + '\n' + taskLine + todoContent.slice(insertAt);
1643
+ } else {
1644
+ todoContent += '\n## Backlog\n\n' + taskLine;
1645
+ }
1646
+ fs.writeFileSync(todoPath, todoContent, 'utf8');
1647
+ }
1648
+ const taskSeed = seedBusinessStarterTask(cwd, todoPath, starterAction);
1649
+
1650
+ writeWikiStatus(cwd, {
1651
+ health: `starter onboarding compiled from ${intakeRel}`,
1652
+ nextMove: `review ${path.relative(cwd, briefPath).replace(/\\/g, '/')} and tighten the first loop`,
1653
+ }, 'public', { lastIngest: `${today} ${new Date().toTimeString().slice(0, 5)}` });
1654
+ appendWikiLog(cwd, `starter onboarding compiled for ${bizMeta.slug}`, [
1655
+ `intake ${intakeRel}`,
1656
+ `sources ${sourcesRel}`,
1657
+ `brief ${path.relative(cwd, briefPath).replace(/\\/g, '/')}`,
1658
+ personRelativePath ? `person ${personRelativePath}` : null,
1659
+ `concept ${path.relative(cwd, conceptPath).replace(/\\/g, '/')}`,
1660
+ `cheat sheet ${path.relative(cwd, cheatSheetPath).replace(/\\/g, '/')}`,
1661
+ `one pager ${path.relative(cwd, onePagerPath).replace(/\\/g, '/')}`,
1662
+ ].filter(Boolean), 'public', 'ONBOARD');
1663
+
1664
+ console.log('');
1665
+ console.log(`Onboarded ${bizMeta.name}.`);
1666
+ console.log(` Intake: ${intakeRel}`);
1667
+ console.log(` Sources: ${sourcesRel}`);
1668
+ console.log(` Brief: ${path.relative(cwd, briefPath).replace(/\\/g, '/')}`);
1669
+ if (personRelativePath) console.log(` Contact: ${personRelativePath}`);
1670
+ console.log(` First loop: ${path.relative(cwd, conceptPath).replace(/\\/g, '/')}`);
1671
+ console.log(` Cheat sheet: ${path.relative(cwd, cheatSheetPath).replace(/\\/g, '/')}`);
1672
+ console.log(` One pager: ${path.relative(cwd, onePagerPath).replace(/\\/g, '/')}`);
1673
+ console.log(` Next action: ${starterAction.title}`);
1674
+ if (taskSeed.ok) {
1675
+ console.log(` Task plane: ${taskSeed.inserted ? 'seeded starter task' : 'starter task already present'}`);
1676
+ } else {
1677
+ console.log(` Task plane: TODO fallback only (${taskSeed.error})`);
1678
+ }
1679
+ console.log(' Share: atris business share --write');
1680
+ console.log('');
1681
+ }
1682
+
1683
+ async function recordBusinessRun(reportArg, ...flags) {
1684
+ const options = parseRecordFlags([reportArg, ...flags], process.cwd());
1685
+ const cwd = options.cwd || process.cwd();
1686
+ const bizMeta = readWorkspaceBusinessMeta(cwd);
1687
+ const { absPath, relPath } = resolveWorkspaceReport(cwd, options.reportPath);
1688
+
1689
+ ensureWorkspaceStateFiles(cwd, {
1690
+ slug: bizMeta.slug || 'business',
1691
+ business_id: bizMeta.business_id || '',
1692
+ workspace_id: bizMeta.workspace_id || '',
1693
+ workspace_template: bizMeta.workspace_template || 'business',
1694
+ }, { dryRun: false });
1695
+
1696
+ const reportContent = fs.readFileSync(absPath, 'utf8');
1697
+ const title = extractReportTitle(reportContent, absPath);
1698
+ const outcome = normalizeOutcome(options.outcome);
1699
+ const reward = options.reward != null ? Number(options.reward) : defaultRewardForOutcome(outcome);
1700
+ if (!Number.isFinite(reward)) {
1701
+ throw new Error(`Invalid reward: ${options.reward}`);
1702
+ }
1703
+
1704
+ const recordedAt = new Date().toISOString();
1705
+ const summary = options.summary || title;
1706
+ const metric = options.metric || null;
1707
+ const loop = options.loop || 'manual';
1708
+ const actor = options.actor || 'operator';
1709
+ const stateDir = path.join(cwd, '.atris', 'state');
1710
+
1711
+ const shared = {
1712
+ recorded_at: recordedAt,
1713
+ business_slug: bizMeta.slug || null,
1714
+ business_name: bizMeta.name || null,
1715
+ business_id: bizMeta.business_id || null,
1716
+ workspace_id: bizMeta.workspace_id || null,
1717
+ workspace_template: bizMeta.workspace_template || 'business',
1718
+ report_path: relPath,
1719
+ report_title: title,
1720
+ summary,
1721
+ metric,
1722
+ outcome,
1723
+ reward,
1724
+ loop,
1725
+ actor,
1726
+ };
1727
+
1728
+ appendJsonl(path.join(stateDir, 'events.jsonl'), {
1729
+ ...shared,
1730
+ type: 'report_recorded',
1731
+ });
1732
+
1733
+ appendJsonl(path.join(stateDir, 'episodes.jsonl'), {
1734
+ ...shared,
1735
+ type: 'episode',
1736
+ });
1737
+
1738
+ appendJsonl(path.join(stateDir, 'scorecards.jsonl'), {
1739
+ ...shared,
1740
+ type: 'scorecard',
1741
+ });
1742
+
1743
+ console.log('');
1744
+ console.log(`Recorded recap for ${bizMeta.name || bizMeta.slug || 'workspace'}.`);
1745
+ console.log(` Report: ${relPath}`);
1746
+ console.log(` Outcome: ${outcome}`);
1747
+ console.log(` Reward: ${reward}`);
1748
+ if (metric) console.log(` Metric: ${metric}`);
1749
+ console.log(' State: .atris/state/events.jsonl, episodes.jsonl, scorecards.jsonl');
1750
+ console.log('');
1751
+ }
1752
+
1753
+ function detectBusinessSlug(explicitSlug) {
1754
+ if (explicitSlug) return explicitSlug;
1755
+ const bizFile = path.join(process.cwd(), '.atris', 'business.json');
1756
+ if (!fs.existsSync(bizFile)) return null;
1757
+ try {
1758
+ const biz = JSON.parse(fs.readFileSync(bizFile, 'utf8'));
1759
+ return biz.slug || biz.name || null;
1760
+ } catch {
1761
+ return null;
1762
+ }
1763
+ }
1764
+
1765
+ function relativeTime(dateStr) {
1766
+ if (!dateStr) return 'unknown';
1767
+ const diff = Date.now() - new Date(dateStr).getTime();
1768
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
1769
+ if (days <= 0) return 'today';
1770
+ if (days === 1) return '1d ago';
1771
+ return `${days}d ago`;
1772
+ }
1773
+
1774
+ function activityBar(daysSinceActive, width = 10) {
1775
+ const filled = Math.max(0, Math.min(width, width - Math.floor(daysSinceActive / 3)));
1776
+ return '\u2501'.repeat(filled) + '\u2591'.repeat(width - filled);
1777
+ }
1778
+
1779
+ function asRecord(value) {
1780
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
1781
+ }
1782
+
1783
+ function firstString(...values) {
1784
+ for (const value of values) {
1785
+ if (typeof value === 'string' && value.trim()) return value.trim();
1786
+ }
1787
+ return '';
1788
+ }
1789
+
1790
+ function firstNumber(...values) {
1791
+ for (const value of values) {
1792
+ const parsed = typeof value === 'number'
1793
+ ? value
1794
+ : (typeof value === 'string' && value.trim() ? Number(value) : Number.NaN);
1795
+ if (Number.isFinite(parsed)) return parsed;
1796
+ }
1797
+ return 0;
1798
+ }
1799
+
1800
+ function firstBoolean(...values) {
1801
+ for (const value of values) {
1802
+ if (typeof value === 'boolean') return value;
1803
+ if (value === 1 || value === '1' || value === 'true') return true;
1804
+ if (value === 0 || value === '0' || value === 'false') return false;
1805
+ }
1806
+ return false;
1807
+ }
1808
+
1809
+ function normalizeOrdersPayload(value) {
1810
+ const root = asRecord(value);
1811
+ const body = asRecord(root.data);
1812
+ const source = Object.keys(body).length > 0 ? body : root;
1813
+ const summary = asRecord(source.summary);
1814
+ const rawOrders = Array.isArray(value)
1815
+ ? value
1816
+ : (Array.isArray(source.orders) ? source.orders : (Array.isArray(root.orders) ? root.orders : []));
1817
+ const orders = rawOrders.map((rawOrder, index) => {
1818
+ const order = asRecord(rawOrder);
1819
+ const product = asRecord(order.product);
1820
+ const item = asRecord(order.item);
1821
+ const buyer = asRecord(order.buyer);
1822
+ const customer = asRecord(order.customer);
1823
+ const metadata = asRecord(order.metadata);
1824
+ const firstItem = Array.isArray(order.items) ? asRecord(order.items[0]) : {};
1825
+ const status = firstString(order.status, order.payment_status, order.order_type).toLowerCase();
1826
+ const createdAt = firstString(
1827
+ order.created_at,
1828
+ order.createdAt,
1829
+ order.order_date,
1830
+ order.date,
1831
+ order.paid_at,
1832
+ ) || null;
1833
+
1834
+ return {
1835
+ id: firstString(order.id, order.order_id, order.reference) || `order-${index + 1}`,
1836
+ reference: firstString(order.order_ref, order.reference, order.order_number, order.id) || `order ${index + 1}`,
1837
+ product: firstString(
1838
+ order.product_name,
1839
+ order.product_title,
1840
+ typeof order.product === 'string' ? order.product : null,
1841
+ product.name,
1842
+ product.title,
1843
+ item.name,
1844
+ firstItem.name,
1845
+ firstItem.product_name,
1846
+ metadata.product_name,
1847
+ ) || 'product',
1848
+ quantity: Math.max(1, Math.round(firstNumber(order.quantity, order.qty, firstItem.quantity, 1))),
1849
+ buyer: firstString(
1850
+ order.buyer_name,
1851
+ order.customer_name,
1852
+ buyer.name,
1853
+ customer.name,
1854
+ order.buyer_email,
1855
+ order.customer_email,
1856
+ buyer.email,
1857
+ customer.email,
1858
+ ) || 'guest',
1859
+ status: status || 'unknown',
1860
+ isPreorder: firstBoolean(order.is_preorder, order.preorder)
1861
+ || status.replace(/[_-]/g, '').includes('preorder')
1862
+ || status === 'pending',
1863
+ amountCents: firstNumber(order.amount_cents, order.price_cents, order.total_cents, firstItem.amount_cents),
1864
+ createdAt,
1865
+ };
1866
+ });
1867
+
1868
+ orders.sort((a, b) => {
1869
+ const aTime = a.createdAt ? Date.parse(a.createdAt) : 0;
1870
+ const bTime = b.createdAt ? Date.parse(b.createdAt) : 0;
1871
+ return (Number.isFinite(bTime) ? bTime : 0) - (Number.isFinite(aTime) ? aTime : 0);
1872
+ });
1873
+
1874
+ return {
1875
+ orders,
1876
+ revenueCents: firstNumber(summary.revenue_cents, source.revenue_cents, root.revenue_cents),
1877
+ paidOrders: Math.max(0, Math.round(firstNumber(summary.paid_orders, source.paid_orders, root.paid_orders))),
1878
+ pendingRevenueCents: firstNumber(
1879
+ summary.pending_revenue_cents,
1880
+ source.pending_revenue_cents,
1881
+ root.pending_revenue_cents,
1882
+ ),
1883
+ };
1884
+ }
1885
+
1886
+ function readStorefront(business) {
1887
+ const config = asRecord(asRecord(business).config);
1888
+ const storefront = asRecord(config.storefront);
1889
+ return {
1890
+ enabled: storefront.enabled === true,
1891
+ products: Array.isArray(storefront.products) ? storefront.products.filter(product => product && typeof product === 'object') : [],
1892
+ };
1893
+ }
1894
+
1895
+ function readWalletNet(business) {
1896
+ const config = asRecord(asRecord(business).config);
1897
+ for (const key of ['wallet_balance', 'credits_balance', 'credits', 'wallet_credits']) {
1898
+ const value = config[key];
1899
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
1900
+ }
1901
+ return null;
1902
+ }
1903
+
1904
+ function formatWalletNet(value) {
1905
+ if (value === null) return 'not available';
1906
+ return `$${value.toLocaleString('en-US', { maximumFractionDigits: 2 })}`;
1907
+ }
1908
+
1909
+ function formatCents(value, currency = 'usd') {
1910
+ const amount = firstNumber(value) / 100;
1911
+ const code = firstString(currency).toUpperCase() || 'USD';
1912
+ const formatted = amount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
1913
+ return code === 'USD' ? `$${formatted}` : `${code} ${formatted}`;
1914
+ }
1915
+
1916
+ function renderOrderLine(order) {
1917
+ const kind = order.isPreorder ? ', preorder' : '';
1918
+ const when = order.createdAt ? relativeTime(order.createdAt) : 'date unavailable';
1919
+ return ` ${order.reference}: ${order.product} x${order.quantity}, ${order.buyer}, ${when}${kind}`;
1920
+ }
1921
+
1922
+ function parseBusinessStoreArgs(args = []) {
1923
+ const actions = new Set(['status', 'on', 'enable', 'toggle', 'off', 'disable']);
1924
+ if (actions.has(args[0])) return { action: args[0], slug: args[1] };
1925
+ if (actions.has(args[1])) return { action: args[1], slug: args[0] };
1926
+ return { action: 'status', slug: args[0] };
1927
+ }
1928
+
1929
+ function parseBusinessDoctorOptions(args = []) {
1930
+ const options = {
1931
+ fix: args.includes('--fix'),
1932
+ json: args.includes('--json'),
1933
+ root: businessWorkspaceBase(),
1934
+ };
1935
+ const rootIdx = args.indexOf('--root');
1936
+ if (rootIdx !== -1 && args[rootIdx + 1]) {
1937
+ options.root = path.resolve(args[rootIdx + 1]);
1938
+ }
1939
+ return options;
1940
+ }
1941
+
1942
+ function describeAccess(member) {
1943
+ const role = (member.role || '').toLowerCase();
1944
+ if (role === 'owner') return 'full control';
1945
+ if (role === 'admin') return 'admin access';
1946
+ if (role === 'member') return 'standard access';
1947
+ if (role === 'agent') return 'agent';
1948
+ return role || 'unknown';
1949
+ }
1950
+
1951
+ function walkDir(dir) {
1952
+ let results = [];
1953
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1954
+ const full = path.join(dir, entry.name);
1955
+ if (entry.isDirectory()) {
1956
+ results = results.concat(walkDir(full));
1957
+ } else {
1958
+ results.push(full);
1959
+ }
1960
+ }
1961
+ return results;
1962
+ }
1963
+
1964
+ function findAtrisDir() {
1965
+ let dir = process.cwd();
1966
+ while (dir !== path.dirname(dir)) {
1967
+ if (fs.existsSync(path.join(dir, 'atris'))) return path.join(dir, 'atris');
1968
+ dir = path.dirname(dir);
1969
+ }
1970
+ return null;
1971
+ }
1972
+
1973
+ module.exports = {
1974
+ BUSINESS_SHIP_USAGE,
1975
+ businessWorkspaceBase,
1976
+ slugify,
1977
+ isHelpToken,
1978
+ isAccidentalHelpBusiness,
1979
+ validateBusinessCreateName,
1980
+ parseBusinessShipArgs,
1981
+ shipTeamNames,
1982
+ shipTaskTitle,
1983
+ loadBusinesses,
1984
+ businessMatchesSlug,
1985
+ saveBusinesses,
1986
+ readBusinessFolderBindings,
1987
+ analyzeBusinessDoctor,
1988
+ parseCreateBusinessFlags,
1989
+ resolveWorkspaceRoot,
1990
+ createCanonicalBusinessWorkspace,
1991
+ normalizeOutcome,
1992
+ defaultRewardForOutcome,
1993
+ appendJsonl,
1994
+ readJsonFile,
1995
+ readJsonlRows,
1996
+ countFiles,
1997
+ rel,
1998
+ collectBusinessShareState,
1999
+ renderBusinessCreatedNextSteps,
2000
+ startBusinessWorkspace,
2001
+ localDay,
2002
+ shareBusinessWorkspace,
2003
+ onboardBusiness,
2004
+ recordBusinessRun,
2005
+ detectBusinessSlug,
2006
+ relativeTime,
2007
+ activityBar,
2008
+ asRecord,
2009
+ firstString,
2010
+ firstNumber,
2011
+ normalizeOrdersPayload,
2012
+ readStorefront,
2013
+ readWalletNet,
2014
+ formatWalletNet,
2015
+ formatCents,
2016
+ renderOrderLine,
2017
+ parseBusinessStoreArgs,
2018
+ parseBusinessDoctorOptions,
2019
+ describeAccess,
2020
+ walkDir,
2021
+ findAtrisDir,
2022
+ };