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.
package/commands/sync.js CHANGED
@@ -3,7 +3,17 @@ const path = require('path');
3
3
  const os = require('os');
4
4
  const { ensureWikiScaffold } = require('../lib/wiki');
5
5
 
6
- const BUSINESS_TEMPLATE_DIR = path.join(__dirname, '..', 'templates', 'business-canonical');
6
+ const TEMPLATE_ROOT_DIR = path.join(__dirname, '..', 'templates');
7
+ const WORKSPACE_TEMPLATES = {
8
+ business: {
9
+ dir: path.join(TEMPLATE_ROOT_DIR, 'business-starter'),
10
+ label: 'business environment',
11
+ },
12
+ research: {
13
+ dir: path.join(TEMPLATE_ROOT_DIR, 'research-canonical'),
14
+ label: 'research lab environment',
15
+ },
16
+ };
7
17
 
8
18
  /**
9
19
  * Walk a directory and return relative file paths.
@@ -25,13 +35,64 @@ function _walkTemplateDir(dir, base = dir) {
25
35
  }
26
36
 
27
37
  /**
28
- * Substitute {{name}}, {{slug}}, {{owner_email}} in template content.
38
+ * Substitute workspace metadata in template content.
29
39
  */
30
40
  function _substituteParams(content, params) {
31
41
  return content
32
42
  .replace(/\{\{name\}\}/g, params.name || params.slug || 'this business')
33
43
  .replace(/\{\{slug\}\}/g, params.slug || 'business')
34
- .replace(/\{\{owner_email\}\}/g, params.owner_email || '');
44
+ .replace(/\{\{owner_email\}\}/g, params.owner_email || '')
45
+ .replace(/\{\{business_id\}\}/g, params.business_id || '')
46
+ .replace(/\{\{workspace_id\}\}/g, params.workspace_id || '')
47
+ .replace(/\{\{today\}\}/g, params.today || new Date().toISOString().slice(0, 10))
48
+ .replace(/\{\{workspace_template\}\}/g, params.workspace_template || 'business');
49
+ }
50
+
51
+ function resolveWorkspaceTemplate(templateName = 'business') {
52
+ const normalized = String(templateName || 'business').toLowerCase();
53
+ if (normalized === 'research-lab' || normalized === 'researchlab' || normalized === 'lab') {
54
+ return { key: 'research', ...WORKSPACE_TEMPLATES.research };
55
+ }
56
+ const template = WORKSPACE_TEMPLATES[normalized];
57
+ if (!template) return null;
58
+ return { key: normalized, ...template };
59
+ }
60
+
61
+ function _ensureWorkspaceState(targetRoot, params, options = {}) {
62
+ const dryRun = options.dryRun === true;
63
+ const metaDir = path.join(targetRoot, '.atris');
64
+ const stateDir = path.join(metaDir, 'state');
65
+ const created = [];
66
+
67
+ const files = [
68
+ {
69
+ relPath: '_sync.json',
70
+ content: `${JSON.stringify({
71
+ workspace_slug: params.slug || 'business',
72
+ business_id: params.business_id || '',
73
+ workspace_id: params.workspace_id || '',
74
+ workspace_template: params.workspace_template || 'business',
75
+ status: 'initialized-local',
76
+ updated_at: new Date().toISOString(),
77
+ source: 'workspace template bootstrap',
78
+ }, null, 2)}\n`,
79
+ },
80
+ { relPath: 'events.jsonl', content: '' },
81
+ { relPath: 'episodes.jsonl', content: '' },
82
+ { relPath: 'scorecards.jsonl', content: '' },
83
+ ];
84
+
85
+ for (const file of files) {
86
+ const fullPath = path.join(stateDir, file.relPath);
87
+ if (fs.existsSync(fullPath)) continue;
88
+ created.push(path.join('.atris', 'state', file.relPath));
89
+ if (!dryRun) {
90
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
91
+ fs.writeFileSync(fullPath, file.content);
92
+ }
93
+ }
94
+
95
+ return created;
35
96
  }
36
97
 
37
98
  /**
@@ -41,34 +102,44 @@ function _substituteParams(content, params) {
41
102
  * Default: NEVER overwrites existing files (preserves customizations).
42
103
  * --force: overwrites existing canonical files (bumps to latest).
43
104
  */
44
- function syncBusinessCanonical(targetRoot, bizMeta) {
105
+ function syncWorkspaceTemplate(targetRoot, bizMeta, options = {}) {
106
+ const template = resolveWorkspaceTemplate(options.templateName || bizMeta.workspace_template || 'business');
107
+ if (!template) {
108
+ console.error(`✗ Unknown workspace template: ${options.templateName || bizMeta.workspace_template}`);
109
+ process.exit(1);
110
+ }
111
+
45
112
  const params = {
46
113
  slug: bizMeta.slug || 'business',
47
114
  name: bizMeta.name || bizMeta.slug || 'this business',
48
115
  owner_email: bizMeta.owner_email || '',
116
+ business_id: bizMeta.business_id || '',
117
+ workspace_id: bizMeta.workspace_id || '',
118
+ today: new Date().toISOString().slice(0, 10),
119
+ workspace_template: template.key,
49
120
  };
50
- const force = process.argv.includes('--force');
51
- const dryRun = process.argv.includes('--dry-run');
121
+ const force = options.force != null ? options.force : process.argv.includes('--force');
122
+ const dryRun = options.dryRun != null ? options.dryRun : process.argv.includes('--dry-run');
52
123
  const targetAtrisDir = path.join(targetRoot, 'atris');
53
124
 
54
- if (!fs.existsSync(BUSINESS_TEMPLATE_DIR)) {
55
- console.error(`✗ Canonical template directory not found: ${BUSINESS_TEMPLATE_DIR}`);
125
+ if (!fs.existsSync(template.dir)) {
126
+ console.error(`✗ Workspace template directory not found: ${template.dir}`);
56
127
  console.error(' Your atris-cli installation may be incomplete.');
57
128
  process.exit(1);
58
129
  }
59
130
 
60
131
  console.log('');
61
- console.log(`Updating ${params.name} (${params.slug}) from canonical templates...`);
132
+ console.log(`Updating ${params.name} (${params.slug}) from ${template.label} templates...`);
62
133
  console.log(` Target: ${targetAtrisDir}/`);
63
- console.log(` Source: ${BUSINESS_TEMPLATE_DIR}`);
134
+ console.log(` Source: ${template.dir}`);
64
135
  console.log('');
65
136
 
66
- const templateFiles = _walkTemplateDir(BUSINESS_TEMPLATE_DIR).sort();
137
+ const templateFiles = _walkTemplateDir(template.dir).sort();
67
138
  let added = 0, updated = 0, skipped = 0, preserved = 0;
68
139
  const addedList = [], updatedList = [], preservedList = [];
69
140
 
70
141
  for (const relPath of templateFiles) {
71
- const templatePath = path.join(BUSINESS_TEMPLATE_DIR, relPath);
142
+ const templatePath = path.join(template.dir, relPath);
72
143
  const targetPath = path.join(targetAtrisDir, relPath);
73
144
  let templateContent;
74
145
  try { templateContent = fs.readFileSync(templatePath, 'utf-8'); } catch { continue; }
@@ -93,6 +164,8 @@ function syncBusinessCanonical(targetRoot, bizMeta) {
93
164
  }
94
165
  }
95
166
 
167
+ const stateAddedList = _ensureWorkspaceState(targetRoot, params, { dryRun });
168
+
96
169
  console.log(` Added: ${added}`);
97
170
  console.log(` Updated: ${updated} ${force ? '' : '(--force to enable)'}`);
98
171
  console.log(` Preserved: ${preserved} (existing customizations kept)`);
@@ -111,10 +184,15 @@ function syncBusinessCanonical(targetRoot, bizMeta) {
111
184
  if (updatedList.length > 15) console.log(` ... +${updatedList.length - 15} more`);
112
185
  console.log('');
113
186
  }
187
+ if (stateAddedList.length > 0) {
188
+ console.log(' State files:');
189
+ stateAddedList.forEach(p => console.log(` + ${p}`));
190
+ console.log('');
191
+ }
114
192
 
115
193
  if (dryRun) {
116
194
  console.log(' (--dry-run, no changes made)');
117
- } else if (added === 0 && updated === 0) {
195
+ } else if (added === 0 && updated === 0 && stateAddedList.length === 0) {
118
196
  ensureWikiScaffold(targetRoot);
119
197
  console.log(' ✓ Already up to date');
120
198
  } else {
@@ -123,13 +201,22 @@ function syncBusinessCanonical(targetRoot, bizMeta) {
123
201
  }
124
202
  }
125
203
 
204
+ function syncBusinessCanonical(targetRoot, bizMeta, options = {}) {
205
+ return syncWorkspaceTemplate(targetRoot, bizMeta, {
206
+ ...options,
207
+ templateName: options.templateName || bizMeta.workspace_template || 'business',
208
+ });
209
+ }
210
+
126
211
  function syncAtris() {
127
212
  // Business mode detection: if .atris/business.json exists, use canonical templates
128
213
  const bizFile = path.join(process.cwd(), '.atris', 'business.json');
129
214
  if (fs.existsSync(bizFile)) {
130
215
  try {
131
216
  const bizMeta = JSON.parse(fs.readFileSync(bizFile, 'utf8'));
132
- return syncBusinessCanonical(process.cwd(), bizMeta);
217
+ return syncWorkspaceTemplate(process.cwd(), bizMeta, {
218
+ templateName: bizMeta.workspace_template || 'business',
219
+ });
133
220
  } catch (e) {
134
221
  console.error(`✗ Failed to read .atris/business.json: ${e.message}`);
135
222
  process.exit(1);
@@ -567,4 +654,10 @@ function syncSkills({ silent = false } = {}) {
567
654
  return updated;
568
655
  }
569
656
 
570
- module.exports = { syncAtris, syncSkills };
657
+ module.exports = {
658
+ syncAtris,
659
+ syncSkills,
660
+ syncBusinessCanonical,
661
+ syncWorkspaceTemplate,
662
+ resolveWorkspaceTemplate,
663
+ };
@@ -444,11 +444,11 @@ function verifyChange(cwd, change) {
444
444
  };
445
445
  }
446
446
 
447
- // Generic check - always pass (can't verify without specifics)
447
+ // No specific check possible refuse to auto-pass
448
448
  return {
449
- pass: true,
449
+ pass: false,
450
450
  description: change.description,
451
- details: 'Manual verification recommended'
451
+ details: 'No verifiable check for this change type. Add an explicit verify command.'
452
452
  };
453
453
  }
454
454
 
package/commands/wiki.js CHANGED
@@ -5,6 +5,8 @@ const { apiRequestJson } = require('../utils/api');
5
5
  const { loadBusinesses, saveBusinesses } = require('./business');
6
6
  const {
7
7
  WIKI_ROOT,
8
+ PRIVATE_WIKI_ROOT,
9
+ getWikiRoot,
8
10
  ensureWikiScaffold,
9
11
  findLocalWikiDir,
10
12
  buildIngestPrompt,
@@ -31,9 +33,14 @@ function parseCloudArgs(args) {
31
33
 
32
34
  function parseModeArgs(args) {
33
35
  const cloud = args.includes('--cloud');
36
+ const privateMode = args.includes('--private');
37
+ if (cloud && privateMode) {
38
+ console.error('Use either --cloud or --private, not both.');
39
+ process.exit(1);
40
+ }
34
41
  return {
35
- mode: cloud ? 'cloud' : 'local',
36
- args: args.filter((arg) => arg !== '--cloud' && arg !== '--local'),
42
+ mode: cloud ? 'cloud' : (privateMode ? 'private' : 'local'),
43
+ args: args.filter((arg) => arg !== '--cloud' && arg !== '--local' && arg !== '--private'),
37
44
  };
38
45
  }
39
46
 
@@ -142,10 +149,10 @@ async function runChat(business, prompt, token) {
142
149
  }
143
150
  }
144
151
 
145
- function printLocalPrompt(title, prompt, details = []) {
152
+ function printLocalPrompt(title, prompt, wikiRoot, details = []) {
146
153
  console.log('');
147
154
  console.log(title);
148
- console.log(`Target: ${WIKI_ROOT}`);
155
+ console.log(`Target: ${wikiRoot}`);
149
156
  details.forEach((detail) => console.log(detail));
150
157
  console.log('');
151
158
  console.log('Prompt for the current coding agent:');
@@ -160,9 +167,10 @@ async function wikiIngest(mode, slug, sourceValue) {
160
167
  process.exit(1);
161
168
  }
162
169
 
163
- if (mode === 'local') {
164
- const wikiDir = ensureWikiScaffold();
165
- printLocalPrompt('Local wiki ingest', buildIngestPrompt(sourceValue), [
170
+ if (mode === 'local' || mode === 'private') {
171
+ const wikiMode = mode === 'private' ? 'private' : 'public';
172
+ const wikiDir = ensureWikiScaffold(process.cwd(), wikiMode);
173
+ printLocalPrompt(mode === 'private' ? 'Private wiki ingest' : 'Local wiki ingest', buildIngestPrompt(sourceValue, wikiMode), getWikiRoot(wikiMode), [
166
174
  `Wiki dir: ${wikiDir}`,
167
175
  `Sources: ${sourceValue}`,
168
176
  ]);
@@ -182,13 +190,14 @@ async function wikiQuery(mode, slug, question) {
182
190
  process.exit(1);
183
191
  }
184
192
 
185
- if (mode === 'local') {
186
- const wikiDir = findLocalWikiDir(process.cwd(), slug);
193
+ if (mode !== 'cloud') {
194
+ const wikiMode = mode === 'private' ? 'private' : 'public';
195
+ const wikiDir = findLocalWikiDir(process.cwd(), slug, wikiMode);
187
196
  if (!wikiDir) {
188
- console.error('No local atris/wiki found. Run: atris ingest <path>');
197
+ console.error(`No local wiki found at ${getWikiRoot(wikiMode)}. Run: atris wiki ingest${wikiMode === 'private' ? ' --private' : ''} <path>`);
189
198
  process.exit(1);
190
199
  }
191
- printLocalPrompt('Local wiki query', buildQueryPrompt(question), [
200
+ printLocalPrompt(mode === 'private' ? 'Private wiki query' : 'Local wiki query', buildQueryPrompt(question, wikiMode), getWikiRoot(wikiMode), [
192
201
  `Wiki dir: ${wikiDir}`,
193
202
  `Question: ${question}`,
194
203
  ]);
@@ -201,13 +210,14 @@ async function wikiQuery(mode, slug, question) {
201
210
  }
202
211
 
203
212
  async function wikiLint(mode, slug) {
204
- if (mode === 'local') {
205
- const wikiDir = findLocalWikiDir(process.cwd(), slug);
213
+ if (mode !== 'cloud') {
214
+ const wikiMode = mode === 'private' ? 'private' : 'public';
215
+ const wikiDir = findLocalWikiDir(process.cwd(), slug, wikiMode);
206
216
  if (!wikiDir) {
207
- console.error('No local atris/wiki found. Run: atris ingest <path>');
217
+ console.error(`No local wiki found at ${getWikiRoot(wikiMode)}. Run: atris wiki ingest${wikiMode === 'private' ? ' --private' : ''} <path>`);
208
218
  process.exit(1);
209
219
  }
210
- printLocalPrompt('Local wiki lint', buildLintPrompt(), [`Wiki dir: ${wikiDir}`]);
220
+ printLocalPrompt(mode === 'private' ? 'Private wiki lint' : 'Local wiki lint', buildLintPrompt(wikiMode), getWikiRoot(wikiMode), [`Wiki dir: ${wikiDir}`]);
211
221
  return;
212
222
  }
213
223
 
@@ -217,15 +227,16 @@ async function wikiLint(mode, slug) {
217
227
  await runChat(business, buildLintPrompt(), creds.token);
218
228
  }
219
229
 
220
- function wikiSearch(slug, query) {
230
+ function wikiSearch(mode, slug, query) {
221
231
  if (!query) {
222
232
  console.error('Usage: atris wiki search [business] <term>');
223
233
  process.exit(1);
224
234
  }
225
235
 
226
- const wikiDir = findLocalWikiDir(process.cwd(), slug);
236
+ const wikiMode = mode === 'private' ? 'private' : 'public';
237
+ const wikiDir = findLocalWikiDir(process.cwd(), slug, wikiMode);
227
238
  if (!wikiDir) {
228
- console.error(`No local wiki found. Run: atris pull ${slug || ''} --only wiki`);
239
+ console.error(`No local wiki found at ${getWikiRoot(wikiMode)}.`);
229
240
  process.exit(1);
230
241
  }
231
242
 
@@ -250,10 +261,11 @@ function wikiSearch(slug, query) {
250
261
  console.log('');
251
262
  }
252
263
 
253
- function wikiLog(slug, limit) {
254
- const wikiDir = findLocalWikiDir(process.cwd(), slug);
264
+ function wikiLog(mode, slug, limit) {
265
+ const wikiMode = mode === 'private' ? 'private' : 'public';
266
+ const wikiDir = findLocalWikiDir(process.cwd(), slug, wikiMode);
255
267
  if (!wikiDir) {
256
- console.error(`No local wiki found. Run: atris pull ${slug || ''} --only wiki`);
268
+ console.error(`No local wiki found at ${getWikiRoot(wikiMode)}.`);
257
269
  process.exit(1);
258
270
  }
259
271
 
@@ -314,14 +326,21 @@ async function wikiCommand(subcommand, ...args) {
314
326
  break;
315
327
  }
316
328
  case 'search': {
317
- const [slug, query] = parseCloudArgs(cleanArgs);
318
- wikiSearch(slug, query);
329
+ if (mode === 'private') {
330
+ wikiSearch(mode, null, cleanArgs.join(' '));
331
+ } else {
332
+ const [slug, query] = parseCloudArgs(cleanArgs);
333
+ wikiSearch(mode, slug, query);
334
+ }
319
335
  break;
320
336
  }
321
337
  case 'log': {
322
338
  let slug;
323
339
  let limit;
324
- if (cleanArgs.length === 0) {
340
+ if (mode === 'private') {
341
+ slug = null;
342
+ limit = parseInt(cleanArgs[0], 10) || 20;
343
+ } else if (cleanArgs.length === 0) {
325
344
  slug = autoDetectSlug();
326
345
  limit = 20;
327
346
  } else if (cleanArgs.length === 1) {
@@ -336,7 +355,7 @@ async function wikiCommand(subcommand, ...args) {
336
355
  slug = cleanArgs[0];
337
356
  limit = parseInt(cleanArgs[1], 10) || 20;
338
357
  }
339
- wikiLog(slug, limit);
358
+ wikiLog(mode, slug, limit);
340
359
  break;
341
360
  }
342
361
  case 'loop': {
@@ -361,6 +380,7 @@ async function wikiCommand(subcommand, ...args) {
361
380
  console.log('Flags:');
362
381
  console.log(' --cloud Route ingest/query/lint to the cloud workspace');
363
382
  console.log(' --local Be explicit about local mode');
383
+ console.log(` --private Use local private wiki at ${PRIVATE_WIKI_ROOT}/`);
364
384
  console.log('');
365
385
  console.log('Business is auto-detected from .atris/business.json for cloud mode if omitted.');
366
386
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Frozen reward constants for the RL loop.
3
+ *
4
+ * These live outside mutable repo state so the loop cannot edit its own
5
+ * judge. REWARD_CHECKSUM is the SHA-256 of JSON.stringify(REWARD_CONFIG)
6
+ * + computeTickReward.toString() at ship time — if the config values or
7
+ * function body change, verifyJudgeIntegrity() halts the next tick.
8
+ */
9
+
10
+ const crypto = require('crypto');
11
+
12
+ const REWARD_CONFIG = Object.freeze({
13
+ REVIEW_CLEAN: 1,
14
+ VERIFY_PASS: 3,
15
+ NPM_TEST_BONUS: 2,
16
+ COMMIT_LANDED: 1,
17
+ HALT_PENALTY: -3,
18
+ });
19
+
20
+ // SHA-256 of JSON.stringify(REWARD_CONFIG) + computeTickReward.toString() at ship time.
21
+ // Regenerate: node -e "const c=require('./lib/reward-config');const h=require('crypto').createHash('sha256');h.update(JSON.stringify(c.REWARD_CONFIG));h.update(require('./commands/autopilot').computeTickReward.toString());console.log(h.digest('hex'))"
22
+ const REWARD_CHECKSUM = '5a84be0f7f392d6ef05337be0776f864852e94d6391da0b41486298555595a40';
23
+
24
+ module.exports = { REWARD_CONFIG, REWARD_CHECKSUM };