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