atris 3.0.1 → 3.2.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,7 +1,9 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const os = require('os');
3
4
  const { loadCredentials } = require('../utils/auth');
4
5
  const { apiRequestJson } = require('../utils/api');
6
+ const { syncBusinessCanonical } = require('./sync');
5
7
 
6
8
  function getBusinessConfigPath() {
7
9
  const home = require('os').homedir();
@@ -20,6 +22,88 @@ function saveBusinesses(data) {
20
22
  fs.writeFileSync(getBusinessConfigPath(), JSON.stringify(data, null, 2));
21
23
  }
22
24
 
25
+ function parseCreateBusinessFlags(flags, cwd = process.cwd()) {
26
+ const options = {
27
+ description: '',
28
+ template: null,
29
+ ownerEmail: '',
30
+ noLocal: false,
31
+ workspace: false,
32
+ here: false,
33
+ root: null,
34
+ cwd,
35
+ };
36
+
37
+ for (let i = 0; i < flags.length; i++) {
38
+ const flag = flags[i];
39
+ const next = flags[i + 1];
40
+
41
+ if ((flag === '--description' || flag === '-d') && next) {
42
+ options.description = next;
43
+ i++;
44
+ } else if ((flag === '--template' || flag === '-t') && next) {
45
+ options.template = next;
46
+ i++;
47
+ } else if (flag === '--owner-email' && next) {
48
+ options.ownerEmail = next;
49
+ i++;
50
+ } else if ((flag === '--root' || flag === '--workspace-root') && next) {
51
+ options.root = path.resolve(cwd, next);
52
+ options.workspace = true;
53
+ i++;
54
+ } else if (flag === '--here') {
55
+ options.here = true;
56
+ options.workspace = true;
57
+ } else if (flag === '--workspace' || flag === '--local-workspace') {
58
+ options.workspace = true;
59
+ } else if (flag === '--no-local') {
60
+ options.noLocal = true;
61
+ }
62
+ }
63
+
64
+ return options;
65
+ }
66
+
67
+ function resolveWorkspaceRoot(slug, options = {}) {
68
+ if (options.noLocal) return null;
69
+ if (options.here) return options.cwd || process.cwd();
70
+ if (options.root) return path.join(options.root, slug);
71
+ return path.join(os.homedir(), 'arena', 'atris-business', slug);
72
+ }
73
+
74
+ function createCanonicalBusinessWorkspace(targetRoot, bizMeta, options = {}) {
75
+ if (!targetRoot) {
76
+ throw new Error('No target directory provided for business workspace.');
77
+ }
78
+
79
+ if (options.here !== true && fs.existsSync(targetRoot) && !fs.statSync(targetRoot).isDirectory()) {
80
+ throw new Error(`Target path is not a directory: ${targetRoot}`);
81
+ }
82
+
83
+ fs.mkdirSync(targetRoot, { recursive: true });
84
+
85
+ const atrisMetaDir = path.join(targetRoot, '.atris');
86
+ const businessJsonPath = path.join(atrisMetaDir, 'business.json');
87
+ if (fs.existsSync(businessJsonPath)) {
88
+ throw new Error(`Target already contains .atris/business.json: ${targetRoot}`);
89
+ }
90
+
91
+ const workspaceTemplate = options.templateName || bizMeta.workspace_template || 'business';
92
+ fs.mkdirSync(atrisMetaDir, { recursive: true });
93
+ fs.writeFileSync(businessJsonPath, JSON.stringify({
94
+ business_id: bizMeta.business_id,
95
+ workspace_id: bizMeta.workspace_id,
96
+ name: bizMeta.name,
97
+ slug: bizMeta.slug,
98
+ owner_email: bizMeta.owner_email || '',
99
+ workspace_template: workspaceTemplate,
100
+ created_at: new Date().toISOString(),
101
+ }, null, 2));
102
+
103
+ syncBusinessCanonical(targetRoot, bizMeta, { force: false, dryRun: false, templateName: workspaceTemplate });
104
+ return { targetRoot, businessJsonPath, workspaceTemplate };
105
+ }
106
+
23
107
  function detectBusinessSlug(explicitSlug) {
24
108
  if (explicitSlug) return explicitSlug;
25
109
  const bizFile = path.join(process.cwd(), '.atris', 'business.json');
@@ -116,7 +200,7 @@ async function listBusinesses(opts = {}) {
116
200
  * Walk ~/arena/atris-business/ and print a fleet status table for every
117
201
  * customer workspace. Pure local — no API calls, no rate-limit risk.
118
202
  *
119
- * Classifies each dir as: canonical, flat, unbound, nested, bare, or superseded.
203
+ * Classifies each dir as: ready, flat, unbound, nested, bare, or superseded.
120
204
  *
121
205
  * Discovered the need for this during overnight loop tick #3 when we hand-wrote
122
206
  * /tmp/customer_fleet.md. Now any team member can run `atris business list --local`
@@ -171,7 +255,7 @@ function listBusinessesLocal(opts = {}) {
171
255
 
172
256
  let state, action, icon;
173
257
  if (hasBizJson && hasAtris) {
174
- state = 'canonical'; action = 'none'; icon = '🟢';
258
+ state = 'ready'; action = 'none'; icon = '🟢';
175
259
  } else if (hasBizJson && !hasAtris) {
176
260
  state = 'flat'; action = 'migrate to atris/ wrapper'; icon = '🟡';
177
261
  } else if (!hasBizJson && hasAtris) {
@@ -181,7 +265,7 @@ function listBusinessesLocal(opts = {}) {
181
265
  } else if (total < 5) {
182
266
  state = 'bare'; action = 'not yet onboarded'; icon = '⚪';
183
267
  } else {
184
- state = 'flat-unbound'; action = 'needs canonical init'; icon = '🟡';
268
+ state = 'flat-unbound'; action = 'needs business init'; icon = '🟡';
185
269
  }
186
270
 
187
271
  let bizName = name;
@@ -227,7 +311,7 @@ function listBusinessesLocal(opts = {}) {
227
311
  console.log(' CUSTOMER STATE FILES BIZ.JSON ATRIS/ ACTION');
228
312
  console.log(' ' + '─'.repeat(83));
229
313
 
230
- const order = ['canonical', 'flat', 'unbound', 'flat-unbound', 'bare', 'nested', 'superseded'];
314
+ const order = ['ready', 'flat', 'unbound', 'flat-unbound', 'bare', 'nested', 'superseded'];
231
315
  const grouped = {};
232
316
  for (const c of customers) {
233
317
  if (!grouped[c.state]) grouped[c.state] = [];
@@ -519,9 +603,9 @@ async function businessAudit() {
519
603
  console.log('');
520
604
  }
521
605
 
522
- async function createBusiness(name, ...flags) {
606
+ async function createBusinessInternal(name, flags = [], mode = 'auto') {
523
607
  if (!name) {
524
- console.error('Usage: atris business create <name> [--description "..."]');
608
+ console.error('Usage: atris business create <name> [--description "..."] [--workspace] [--here|--root <dir>]');
525
609
  process.exit(1);
526
610
  }
527
611
 
@@ -531,14 +615,8 @@ async function createBusiness(name, ...flags) {
531
615
  process.exit(1);
532
616
  }
533
617
 
534
- // Parse flags
535
- let description = '';
536
- for (let i = 0; i < flags.length; i++) {
537
- if ((flags[i] === '--description' || flags[i] === '-d') && flags[i + 1]) {
538
- description = flags[i + 1];
539
- i++;
540
- }
541
- }
618
+ const options = parseCreateBusinessFlags(flags);
619
+ const description = options.description;
542
620
 
543
621
  console.log(`Creating business: ${name}...`);
544
622
 
@@ -567,8 +645,15 @@ async function createBusiness(name, ...flags) {
567
645
  };
568
646
  saveBusinesses(businesses);
569
647
 
570
- // Scaffold local directory if in an atris project
571
- const atrisDir = findAtrisDir();
648
+ const shouldCreateCanonicalWorkspace = !options.noLocal && (
649
+ mode === 'canonical' ||
650
+ options.workspace ||
651
+ options.here ||
652
+ Boolean(options.root)
653
+ );
654
+
655
+ // Scaffold legacy local directory if in an atris project
656
+ const atrisDir = !shouldCreateCanonicalWorkspace ? findAtrisDir() : null;
572
657
  if (atrisDir) {
573
658
  const bizDir = path.join(atrisDir, 'business', biz.slug);
574
659
  if (!fs.existsSync(bizDir)) {
@@ -584,16 +669,21 @@ async function createBusiness(name, ...flags) {
584
669
  ].join(''));
585
670
  console.log(` Local scaffold: ${bizDir}/`);
586
671
  }
587
- }
588
-
589
- // Apply template if specified
590
- let template = null;
591
- for (let i = 0; i < flags.length; i++) {
592
- if ((flags[i] === '--template' || flags[i] === '-t') && flags[i + 1]) {
593
- template = flags[i + 1];
594
- i++;
595
- }
596
- }
672
+ } else if (shouldCreateCanonicalWorkspace) {
673
+ const workspaceRoot = resolveWorkspaceRoot(biz.slug, options);
674
+ const scaffold = createCanonicalBusinessWorkspace(workspaceRoot, {
675
+ business_id: biz.id,
676
+ workspace_id: biz.workspace_id,
677
+ name: biz.name,
678
+ slug: biz.slug,
679
+ owner_email: options.ownerEmail,
680
+ }, { here: options.here });
681
+ console.log(` Local workspace: ${scaffold.targetRoot}/`);
682
+ } else if (!options.noLocal) {
683
+ console.log(' Tip: run `atris business init "<name>"` or add `--workspace` for a local business environment.');
684
+ }
685
+
686
+ const template = options.template;
597
687
 
598
688
  if (template) {
599
689
  const templates = {
@@ -620,9 +710,22 @@ async function createBusiness(name, ...flags) {
620
710
  console.log(` Slug: ${biz.slug}`);
621
711
  console.log(` Agent: ${biz.agent_id || '(none)'}`);
622
712
  console.log(` Dashboard: https://atris.ai/dashboard/gm/${biz.id}`);
713
+ if (shouldCreateCanonicalWorkspace) {
714
+ const workspaceRoot = resolveWorkspaceRoot(biz.slug, options);
715
+ console.log(` Next: cd ${workspaceRoot}`);
716
+ console.log(' atris align --fix');
717
+ }
623
718
  console.log('');
624
719
  }
625
720
 
721
+ async function createBusiness(name, ...flags) {
722
+ return createBusinessInternal(name, flags, 'auto');
723
+ }
724
+
725
+ async function initBusinessWorkspace(name, ...flags) {
726
+ return createBusinessInternal(name, flags, 'canonical');
727
+ }
728
+
626
729
 
627
730
  async function businessStatus(slug) {
628
731
  const creds = loadCredentials();
@@ -1046,18 +1149,21 @@ async function quickstart() {
1046
1149
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1047
1150
 
1048
1151
  1. Create:
1049
- atris business create "My Company" --template saas
1152
+ atris business init "My Company" --template saas
1050
1153
 
1051
- 2. Connect integrations:
1052
- atris business connect slack --business my-company
1053
- atris business connect github --business my-company
1154
+ 2. Open the local workspace:
1155
+ cd ~/arena/atris-business/my-company
1054
1156
 
1055
- 3. Deploy:
1056
- atris business deploy my-company
1157
+ 3. Push local state to cloud:
1158
+ atris align --fix
1057
1159
 
1058
- That's it. Your agents are live.
1160
+ Then open atris/TODO.md and work the starter queue:
1161
+ define the first loop -> add named humans -> write the first recap
1059
1162
 
1060
1163
  Optional:
1164
+ atris business connect slack --business my-company
1165
+ atris business connect github --business my-company
1166
+
1061
1167
  atris business notify digest --business my-company
1062
1168
  (get 1 email/day instead of every notification)
1063
1169
 
@@ -1076,6 +1182,10 @@ async function businessCommand(subcommand, ...args) {
1076
1182
  case 'new':
1077
1183
  await createBusiness(args[0], ...args.slice(1));
1078
1184
  break;
1185
+ case 'init':
1186
+ case 'workspace':
1187
+ await initBusinessWorkspace(args[0], ...args.slice(1));
1188
+ break;
1079
1189
  case 'list':
1080
1190
  case 'ls': {
1081
1191
  const opts = {};
@@ -1130,7 +1240,9 @@ async function businessCommand(subcommand, ...args) {
1130
1240
  console.log('');
1131
1241
  console.log(' quickstart ← Start here! 3-command guide');
1132
1242
  console.log('');
1133
- console.log(' create <name> Create a new business (cloud + local)');
1243
+ console.log(' init <name> Create a business environment (cloud + local)');
1244
+ console.log(' workspace <name> Alias for init');
1245
+ console.log(' create <name> Create the cloud business; add --workspace for a local business environment');
1134
1246
  console.log(' add <slug> Register an existing cloud business');
1135
1247
  console.log(' list Show registered businesses');
1136
1248
  console.log(' team [slug] Show members, roles, and admin access');
@@ -1144,4 +1256,14 @@ async function businessCommand(subcommand, ...args) {
1144
1256
  }
1145
1257
  }
1146
1258
 
1147
- module.exports = { businessCommand, businessHealth, businessAudit, businessTeam, loadBusinesses, saveBusinesses, getBusinessConfigPath };
1259
+ module.exports = {
1260
+ businessCommand,
1261
+ businessHealth,
1262
+ businessAudit,
1263
+ businessTeam,
1264
+ loadBusinesses,
1265
+ saveBusinesses,
1266
+ getBusinessConfigPath,
1267
+ createCanonicalBusinessWorkspace,
1268
+ initBusinessWorkspace,
1269
+ };
@@ -351,7 +351,7 @@ function buildBenchmarkArtifact(name, packDir, options) {
351
351
  }
352
352
  );
353
353
 
354
- reviewStatus = execution.success ? 'pass' : 'fail';
354
+ reviewStatus = execution.verifyPass ? 'pass' : 'fail';
355
355
  reviewSummary = summarizeReview(execution.reviewOutput);
356
356
  tests = inferTestResults(execution.reviewOutput);
357
357
  } catch (error) {
@@ -0,0 +1,183 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execFileSync } = require('child_process');
4
+
5
+ /**
6
+ * atris release [--dry-run] - Tag a release, create GitHub release, draft /launch post
7
+ *
8
+ * - Reads git log since last tag
9
+ * - Determines bump type (minor if any scorecard has reward>=5, else patch)
10
+ * - Bumps package.json version
11
+ * - Commits, tags, pushes
12
+ * - Creates GitHub release via `gh`
13
+ * - Drafts a /launch post (3 emoji bullets)
14
+ */
15
+ async function releaseAtris({ dryRun = false } = {}) {
16
+ const cwd = process.cwd();
17
+
18
+ // 1. Get last tag
19
+ let lastTag;
20
+ try {
21
+ lastTag = execFileSync('git', ['describe', '--tags', '--abbrev=0'], { cwd, encoding: 'utf8' }).trim();
22
+ } catch {
23
+ lastTag = null;
24
+ }
25
+
26
+ // 2. Get git log since last tag
27
+ let logArgs = ['log', '--oneline'];
28
+ if (lastTag) {
29
+ logArgs = ['log', `${lastTag}..HEAD`, '--oneline'];
30
+ }
31
+ let commits;
32
+ try {
33
+ commits = execFileSync('git', logArgs, { cwd, encoding: 'utf8' }).trim();
34
+ } catch {
35
+ commits = '';
36
+ }
37
+
38
+ if (!commits) {
39
+ console.log('no new commits since last tag' + (lastTag ? ` (${lastTag})` : '') + '. nothing to release.');
40
+ return;
41
+ }
42
+
43
+ const commitLines = commits.split('\n').filter(Boolean);
44
+ console.log('');
45
+ console.log(`commits since ${lastTag || 'beginning'}: ${commitLines.length}`);
46
+
47
+ // 3. Build changelog
48
+ const changelog = commitLines.map(l => `- ${l}`).join('\n');
49
+
50
+ // 4. Determine bump type — minor if any scorecard has reward >= 5, else patch
51
+ const bumpType = determineBumpType(cwd);
52
+ console.log(`bump type: ${bumpType}`);
53
+
54
+ // 5. Read current version and compute next
55
+ const pkgPath = path.join(cwd, 'package.json');
56
+ if (!fs.existsSync(pkgPath)) {
57
+ console.log('no package.json found. cannot bump version.');
58
+ return;
59
+ }
60
+ const pkgRaw = fs.readFileSync(pkgPath, 'utf8');
61
+ const pkg = JSON.parse(pkgRaw);
62
+ const currentVersion = pkg.version;
63
+ const nextVersion = bumpVersion(currentVersion, bumpType);
64
+
65
+ console.log(`version: ${currentVersion} → ${nextVersion}`);
66
+ console.log('');
67
+
68
+ // 6. Show changelog
69
+ console.log('changelog:');
70
+ console.log(changelog);
71
+ console.log('');
72
+
73
+ if (dryRun) {
74
+ console.log('--- dry-run: draft release ---');
75
+ console.log(`tag: v${nextVersion}`);
76
+ console.log(`title: v${nextVersion}`);
77
+ console.log('');
78
+ printLaunchPost(nextVersion, commitLines);
79
+ return;
80
+ }
81
+
82
+ // 7. Bump package.json
83
+ const updatedPkgRaw = pkgRaw.replace(
84
+ `"version": "${currentVersion}"`,
85
+ `"version": "${nextVersion}"`
86
+ );
87
+ fs.writeFileSync(pkgPath, updatedPkgRaw);
88
+ console.log(`bumped package.json to ${nextVersion}`);
89
+
90
+ // 8. Commit
91
+ execFileSync('git', ['add', 'package.json'], { cwd });
92
+ execFileSync('git', ['commit', '-m', `v${nextVersion}`], { cwd });
93
+ console.log('committed');
94
+
95
+ // 9. Tag
96
+ execFileSync('git', ['tag', `v${nextVersion}`], { cwd });
97
+ console.log(`tagged v${nextVersion}`);
98
+
99
+ // 10. Push + push tags
100
+ try {
101
+ execFileSync('git', ['push'], { cwd });
102
+ execFileSync('git', ['push', '--tags'], { cwd });
103
+ console.log('pushed');
104
+ } catch (err) {
105
+ console.log('push failed — you may need to push manually');
106
+ }
107
+
108
+ // 11. Create GitHub release via gh
109
+ try {
110
+ execFileSync('gh', ['release', 'create', `v${nextVersion}`,
111
+ '--title', `v${nextVersion}`,
112
+ '--notes', changelog
113
+ ], { cwd });
114
+ console.log(`github release created: v${nextVersion}`);
115
+ } catch (err) {
116
+ console.log('gh release create failed — install gh or create release manually');
117
+ }
118
+
119
+ // 12. Draft launch post
120
+ console.log('');
121
+ printLaunchPost(nextVersion, commitLines);
122
+ }
123
+
124
+ /**
125
+ * Check scorecards for reward >= 5 → minor bump. Otherwise patch.
126
+ */
127
+ function determineBumpType(cwd) {
128
+ const scorecardsDir = path.join(cwd, 'atris', 'scorecards');
129
+ if (!fs.existsSync(scorecardsDir)) return 'patch';
130
+
131
+ try {
132
+ const files = fs.readdirSync(scorecardsDir).filter(f => f.endsWith('.md'));
133
+ for (const file of files) {
134
+ const content = fs.readFileSync(path.join(scorecardsDir, file), 'utf8');
135
+ const rewardMatch = content.match(/reward[:\s]+(\d+)/i);
136
+ if (rewardMatch && parseInt(rewardMatch[1], 10) >= 5) {
137
+ return 'minor';
138
+ }
139
+ }
140
+ } catch {
141
+ // ignore
142
+ }
143
+ return 'patch';
144
+ }
145
+
146
+ /**
147
+ * Bump a semver string: "1.2.3" + "patch" → "1.2.4", "minor" → "1.3.0"
148
+ */
149
+ function bumpVersion(version, type) {
150
+ const parts = version.split('.').map(Number);
151
+ if (type === 'minor') {
152
+ parts[1]++;
153
+ parts[2] = 0;
154
+ } else {
155
+ parts[2]++;
156
+ }
157
+ return parts.join('.');
158
+ }
159
+
160
+ /**
161
+ * Print a 3-emoji-bullet launch post (Twitter + LinkedIn format)
162
+ */
163
+ function printLaunchPost(version, commitLines) {
164
+ // Pick top 3 changes (dedupe, trim hashes)
165
+ const topChanges = commitLines
166
+ .slice(0, 3)
167
+ .map(l => l.replace(/^[a-f0-9]+\s+/, ''));
168
+
169
+ const emojis = ['🚀', '⚡', '🔧'];
170
+
171
+ console.log('--- launch post draft ---');
172
+ console.log('');
173
+ console.log(`Atris v${version} is out.`);
174
+ console.log('');
175
+ topChanges.forEach((change, i) => {
176
+ console.log(`${emojis[i] || '•'} ${change}`);
177
+ });
178
+ console.log('');
179
+ console.log('npm i -g atris');
180
+ console.log('--- end launch post ---');
181
+ }
182
+
183
+ module.exports = { releaseAtris };
@@ -0,0 +1,52 @@
1
+ const { initResearchWorkspace } = require('./business');
2
+
3
+ async function researchQuickstart() {
4
+ console.log(`
5
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6
+ Start a Research Lab in 3 Commands
7
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8
+
9
+ 1. Create:
10
+ atris research init "Frontier Lab"
11
+
12
+ 2. Open the local workspace:
13
+ cd ~/arena/atris-business/frontier-lab
14
+
15
+ 3. Push local state to cloud:
16
+ atris align --fix
17
+
18
+ The research template starts with:
19
+ hypotheses + experiment lanes
20
+ eval-first reward policy
21
+ literature + findings workflow
22
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
23
+ `);
24
+ }
25
+
26
+ async function researchCommand(subcommand, ...args) {
27
+ switch (subcommand) {
28
+ case 'init':
29
+ case 'workspace':
30
+ case 'create':
31
+ await initResearchWorkspace(args[0], ...args.slice(1));
32
+ break;
33
+ case 'quickstart':
34
+ case 'start':
35
+ case 'guide':
36
+ await researchQuickstart();
37
+ break;
38
+ default:
39
+ console.log('Usage: atris research <command> [args]');
40
+ console.log('');
41
+ console.log(' quickstart ← Start here! 3-command guide');
42
+ console.log('');
43
+ console.log(' init <name> Create a research lab workspace (cloud + local)');
44
+ console.log(' workspace <name> Alias for init');
45
+ console.log(' create <name> Alias for init');
46
+ }
47
+ }
48
+
49
+ module.exports = {
50
+ researchCommand,
51
+ researchQuickstart,
52
+ };