atris 3.15.23 → 3.15.30

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/aeo.js CHANGED
@@ -1,24 +1,98 @@
1
1
  /**
2
2
  * atris aeo — AI Engine Optimization commands
3
3
  *
4
- * atris aeo init # create entity-graph skeleton in workspace
5
- * atris aeo draft "<topic>" [opts] # generate citation-optimized article (credit-metered)
4
+ * Backend-routed (require EC2):
5
+ * atris aeo init # create entity-graph skeleton
6
+ * atris aeo draft "<topic>" [opts] # generate citation-optimized article
6
7
  *
7
- * Hits the backend endpoints registered under:
8
- * POST /api/business/{id}/workspaces/{ws}/aeo/init
9
- * POST /api/business/{id}/workspaces/{ws}/aeo/draft
8
+ * Local-read against ~/arena/atrisos-backend/atris/features/aeo/proof/:
9
+ * atris aeo log [--engine X] [--limit N] # citation attempt log
10
+ * atris aeo status # engine + proof + buyer summary
11
+ * atris aeo packet <slug> # buyer packet for a surface
12
+ * atris aeo proofs [--filter X] # list proof receipt categories
10
13
  *
11
- * Business resolution mirrors `atris terminal`: explicit --workspace slug,
12
- * else cwd .atris/business.json. The endpoint itself takes care of running
13
- * Claude Sonnet 4.6 with the 10 AEO rules and writing to /workspace/atris/aeo/drafts/.
14
+ * Shell out to atrisos-backend/scripts/aeo_*.py:
15
+ * atris aeo discover <source> [...] # discovery audit
16
+ * atris aeo audit <source> [...] # agent-usability audit
17
+ *
18
+ * Backend root resolution: $ATRIS_BACKEND_ROOT or ~/arena/atrisos-backend.
14
19
  */
15
20
 
16
21
  const fs = require('fs');
22
+ const os = require('os');
17
23
  const path = require('path');
24
+ const { spawnSync } = require('child_process');
18
25
  const { loadCredentials } = require('../utils/auth');
19
26
  const { apiRequestJson } = require('../utils/api');
20
27
  const { loadBusinesses, saveBusinesses } = require('./business');
21
28
 
29
+ function resolveBackendRoot() {
30
+ const candidates = [
31
+ process.env.ATRIS_BACKEND_ROOT,
32
+ path.join(os.homedir(), 'arena', 'atrisos-backend'),
33
+ ].filter(Boolean);
34
+ for (const root of candidates) {
35
+ if (fs.existsSync(path.join(root, 'atris', 'features', 'aeo'))) return root;
36
+ }
37
+ return null;
38
+ }
39
+
40
+ function requireBackendRoot() {
41
+ const root = resolveBackendRoot();
42
+ if (!root) {
43
+ console.error('Cannot find atrisos-backend. Set $ATRIS_BACKEND_ROOT or clone to ~/arena/atrisos-backend.');
44
+ process.exit(1);
45
+ }
46
+ return root;
47
+ }
48
+
49
+ function readJsonSafe(p) {
50
+ if (!fs.existsSync(p)) return null;
51
+ try {
52
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
53
+ } catch (err) {
54
+ console.error(` warning: malformed JSON at ${p} (${err.message})`);
55
+ return null;
56
+ }
57
+ }
58
+
59
+ function pad(s, n) { s = String(s); return s.length >= n ? s.slice(0, n) : s + ' '.repeat(n - s.length); }
60
+
61
+ function assertNoExtras(sub, args, allowedFlags) {
62
+ const allowed = new Set(allowedFlags);
63
+ const unknownFlags = args.filter((a) => a.startsWith('--') && !allowed.has(a));
64
+ const positional = args.filter((a) => !a.startsWith('--'));
65
+ if (unknownFlags.length) {
66
+ console.error(`Unknown flag for aeo ${sub}: ${unknownFlags.join(' ')}. Supported: ${[...allowed].join(' ') || '(none)'}`);
67
+ process.exit(1);
68
+ }
69
+ if (positional.length) {
70
+ console.error(`Unexpected argument for aeo ${sub}: ${positional.join(' ')}`);
71
+ process.exit(1);
72
+ }
73
+ }
74
+
75
+ function readArg(args, ...keys) {
76
+ for (const k of keys) {
77
+ const eqIdx = args.findIndex((a) => a.startsWith(`${k}=`));
78
+ if (eqIdx !== -1) {
79
+ const v = args[eqIdx].slice(k.length + 1);
80
+ args.splice(eqIdx, 1);
81
+ return v;
82
+ }
83
+ const i = args.findIndex((a) => a === k);
84
+ if (i === -1) continue;
85
+ const v = args[i + 1];
86
+ if (v === undefined || v.startsWith('--')) {
87
+ console.error(`Flag ${k} requires a value.`);
88
+ process.exit(1);
89
+ }
90
+ args.splice(i, 2);
91
+ return v;
92
+ }
93
+ return null;
94
+ }
95
+
22
96
  function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
23
97
 
24
98
  async function ensureAwake(token, businessId, maxWaitSec = 90) {
@@ -83,13 +157,25 @@ function pickSlug(args) {
83
157
 
84
158
  function printHelp() {
85
159
  console.log('Usage:');
86
- console.log(' atris aeo init [--workspace <slug>]');
87
- console.log(' atris aeo draft "<topic>" [--workspace <slug>] [--queries q1,q2] [--slug X] [--url URL]');
160
+ console.log(' Backend / EC2:');
161
+ console.log(' atris aeo init [--workspace <slug>]');
162
+ console.log(' atris aeo draft "<topic>" [--workspace <slug>] [--queries q1,q2] [--slug X] [--url URL]');
163
+ console.log('');
164
+ console.log(' Local read (atris/features/aeo/proof/):');
165
+ console.log(' atris aeo log [--engine X] [--limit N] [--json]');
166
+ console.log(' atris aeo status [--json]');
167
+ console.log(' atris aeo packet <slug> [--json]');
168
+ console.log(' atris aeo proofs [--filter X]');
169
+ console.log('');
170
+ console.log(' Script wrappers (scripts/aeo_*.py):');
171
+ console.log(' atris aeo discover <source> [--question Q ...] [--canonical-url URL] [--out-dir DIR]');
172
+ console.log(' atris aeo audit <source> [--baseline B] [--canonical-url URL] [--out-dir DIR]');
88
173
  console.log('');
89
174
  console.log('Examples:');
90
- console.log(' atris aeo init');
91
- console.log(' atris aeo draft "what is example-co" --queries "what is example-co,best freight platform"');
92
- console.log(' atris aeo draft "how does atris work" --workspace doordash --slug atris-overview');
175
+ console.log(' atris aeo log --engine perplexity --limit 5');
176
+ console.log(' atris aeo packet pallet');
177
+ console.log(' atris aeo status');
178
+ console.log(' atris aeo discover https://atris.ai/aeo --canonical-url https://atris.ai/aeo');
93
179
  }
94
180
 
95
181
  async function aeoInit(args) {
@@ -183,12 +269,290 @@ async function aeoDraft(args) {
183
269
  if (data.hint) console.log(` hint: ${data.hint}`);
184
270
  }
185
271
 
272
+ // ---------- LOCAL READ SUBCOMMANDS ----------
273
+
274
+ function loadCitationAttempts(root) {
275
+ const dir = path.join(root, 'atris', 'features', 'aeo', 'proof', 'live-citation-attempts');
276
+ if (!fs.existsSync(dir)) return [];
277
+ const rows = [];
278
+ for (const file of fs.readdirSync(dir)) {
279
+ if (!file.endsWith('.json')) continue;
280
+ const data = readJsonSafe(path.join(dir, file));
281
+ if (!data || !Array.isArray(data.attempts)) continue;
282
+ for (const attempt of data.attempts) {
283
+ if (!attempt || typeof attempt !== 'object') continue;
284
+ const cited = attempt.answer_cites_target_url === true;
285
+ const mentioned = attempt.answer_mentions_target_entity === true;
286
+ let status;
287
+ if (cited) status = 'verified';
288
+ else if (mentioned) status = 'pending';
289
+ else status = 'failed';
290
+ const str = (v) => (typeof v === 'string' ? v : '');
291
+ const arr = (v) => (Array.isArray(v) ? v : []);
292
+ rows.push({
293
+ file,
294
+ attempted_at: str(data.attempted_at),
295
+ engine: str(data.engine),
296
+ prompt_id: str(attempt.prompt_id),
297
+ prompt: str(attempt.exact_prompt),
298
+ target_entity: str(data.target_entity),
299
+ target_urls: arr(data.target_url_candidates),
300
+ answer_evidence_uri: str(attempt.answer_evidence_uri),
301
+ status,
302
+ competitors: arr(attempt.observed_competitor_or_alternative_entities),
303
+ });
304
+ }
305
+ }
306
+ rows.sort((a, b) => (b.attempted_at || '').localeCompare(a.attempted_at || ''));
307
+ return rows;
308
+ }
309
+
310
+ async function aeoLog(args) {
311
+ const engine = readArg(args, '--engine', '-e');
312
+ const limitRaw = readArg(args, '--limit', '-n');
313
+ assertNoExtras('log', args, ['--json']);
314
+ const wantJson = args.includes('--json');
315
+ let limit = 20;
316
+ if (limitRaw != null) {
317
+ const trimmed = String(limitRaw).trim();
318
+ const parsed = parseInt(trimmed, 10);
319
+ if (!/^[+-]?\d+$/.test(trimmed) || !Number.isFinite(parsed) || parsed < 1) {
320
+ console.error(`Invalid --limit value: "${limitRaw}". Expected a positive integer.`);
321
+ process.exit(1);
322
+ }
323
+ limit = parsed;
324
+ }
325
+ const root = requireBackendRoot();
326
+ let rows = loadCitationAttempts(root);
327
+ if (engine) rows = rows.filter((r) => r.engine.toLowerCase() === engine.toLowerCase());
328
+ rows = rows.slice(0, limit);
329
+
330
+ if (wantJson) {
331
+ console.log(JSON.stringify(rows, null, 2));
332
+ return;
333
+ }
334
+
335
+ if (!rows.length) {
336
+ console.log('No citation attempts found.');
337
+ return;
338
+ }
339
+
340
+ const counts = rows.reduce((acc, r) => { acc[r.status] = (acc[r.status] || 0) + 1; return acc; }, {});
341
+ console.log(`AEO citation log (${rows.length} attempt${rows.length === 1 ? '' : 's'})`);
342
+ console.log(` ${Object.entries(counts).map(([k, v]) => `${k}=${v}`).join(' ')}`);
343
+ console.log('');
344
+ console.log(` ${pad('ts', 22)}${pad('engine', 12)}${pad('prompt_id', 26)}${pad('status', 10)}`);
345
+ console.log(` ${'-'.repeat(70)}`);
346
+ for (const r of rows) {
347
+ console.log(` ${pad(r.attempted_at.slice(0, 19), 22)}${pad(r.engine, 12)}${pad(r.prompt_id, 26)}${pad(r.status, 10)}`);
348
+ }
349
+ }
350
+
351
+ async function aeoStatus(args) {
352
+ assertNoExtras('status', args, ['--json']);
353
+ const wantJson = args.includes('--json');
354
+ const root = requireBackendRoot();
355
+ const proofRoot = path.join(root, 'atris', 'features', 'aeo', 'proof');
356
+
357
+ const attempts = loadCitationAttempts(root);
358
+ const enginesSeen = new Set(attempts.map((a) => a.engine).filter(Boolean));
359
+ const verified = attempts.filter((a) => a.status === 'verified').length;
360
+ const pending = attempts.filter((a) => a.status === 'pending').length;
361
+ const failed = attempts.filter((a) => a.status === 'failed').length;
362
+
363
+ const packets = [];
364
+ if (fs.existsSync(proofRoot)) {
365
+ for (const entry of fs.readdirSync(proofRoot)) {
366
+ if (!entry.endsWith('-buyer-packet')) continue;
367
+ const p = path.join(proofRoot, entry, 'packet.json');
368
+ const data = readJsonSafe(p);
369
+ if (!data) continue;
370
+ packets.push({
371
+ slug: entry.replace(/-buyer-packet$/, ''),
372
+ surface: data.surface || entry,
373
+ target_url: data.target_url || '',
374
+ baseline: data?.agent_usability?.baseline_score ?? null,
375
+ proposed: data?.agent_usability?.proposed_score ?? null,
376
+ claim_status: data.claim_status || '',
377
+ });
378
+ }
379
+ }
380
+
381
+ const operator = readJsonSafe(path.join(proofRoot, 'live-citation-operator', 'live-citation-operator.json'));
382
+
383
+ const proofDirs = fs.existsSync(proofRoot)
384
+ ? fs.readdirSync(proofRoot).filter((e) => fs.statSync(path.join(proofRoot, e)).isDirectory()).length
385
+ : 0;
386
+
387
+ if (wantJson) {
388
+ console.log(JSON.stringify({
389
+ backend_root: root,
390
+ proof_dirs: proofDirs,
391
+ citation: { total: attempts.length, verified, pending, failed, engines: [...enginesSeen] },
392
+ packets,
393
+ operator_status: operator?.status || null,
394
+ operator_blocker: operator?.current_blocker || null,
395
+ }, null, 2));
396
+ return;
397
+ }
398
+
399
+ console.log('Atris AEO status');
400
+ console.log(` backend root: ${root}`);
401
+ console.log(` proof receipts: ${proofDirs} categories`);
402
+ console.log('');
403
+ console.log('Live citations');
404
+ console.log(` attempts: ${attempts.length}`);
405
+ console.log(` verified: ${verified}`);
406
+ console.log(` pending: ${pending}`);
407
+ console.log(` failed: ${failed}`);
408
+ console.log(` engines observed: ${[...enginesSeen].join(', ') || '(none)'}`);
409
+ if (operator) {
410
+ console.log(` operator state: ${operator.status || '?'} (blocker: ${operator.current_blocker || 'none'})`);
411
+ }
412
+ console.log('');
413
+ console.log(`Buyer packets (${packets.length})`);
414
+ for (const p of packets) {
415
+ const delta = p.baseline != null && p.proposed != null ? `${p.baseline} → ${p.proposed}` : '?';
416
+ console.log(` ${pad(p.slug, 16)} ${pad(p.target_url || p.surface, 36)} ${pad(delta, 12)} ${p.claim_status}`);
417
+ }
418
+ }
419
+
420
+ async function aeoPacket(args) {
421
+ const known = new Set(['--json']);
422
+ const positional = [];
423
+ for (const a of args) {
424
+ if (a.startsWith('--')) {
425
+ if (!known.has(a)) {
426
+ console.error(`Unknown flag: ${a}. Supported: --json`);
427
+ process.exit(1);
428
+ }
429
+ } else {
430
+ positional.push(a);
431
+ }
432
+ }
433
+ if (positional.length === 0) {
434
+ console.error('Missing slug. Usage: atris aeo packet <slug>');
435
+ process.exit(1);
436
+ }
437
+ if (positional.length > 1) {
438
+ console.error(`Too many arguments: ${positional.join(' ')}. Expected one slug.`);
439
+ process.exit(1);
440
+ }
441
+ const slug = positional[0];
442
+ const wantJson = args.includes('--json');
443
+ const root = requireBackendRoot();
444
+ const file = path.join(root, 'atris', 'features', 'aeo', 'proof', `${slug}-buyer-packet`, 'packet.json');
445
+ const data = readJsonSafe(file);
446
+ if (!data) {
447
+ console.error(`Packet not found: ${file}`);
448
+ process.exit(1);
449
+ }
450
+
451
+ if (wantJson) {
452
+ console.log(JSON.stringify(data, null, 2));
453
+ return;
454
+ }
455
+
456
+ const u = (data && typeof data.agent_usability === 'object' && data.agent_usability) || {};
457
+ const onlyObjects = (v) => (Array.isArray(v) ? v.filter((x) => x && typeof x === 'object') : []);
458
+ const friction = onlyObjects(u.baseline_friction_points);
459
+ const fixes = onlyObjects(u.fix_backlog);
460
+ console.log(`AEO buyer packet — ${slug}`);
461
+ console.log(` surface: ${data.surface || ''}`);
462
+ console.log(` target url: ${data.target_url || ''}`);
463
+ console.log(` claim status: ${data.claim_status || ''}`);
464
+ console.log(` positioning: ${u.positioning || ''}`);
465
+ console.log('');
466
+ console.log('Agent usability scores');
467
+ console.log(` baseline: ${u.baseline_score ?? '?'}`);
468
+ console.log(` proposed: ${u.proposed_score ?? '?'}`);
469
+ console.log(` delta: ${u.score_delta ?? '?'}`);
470
+ console.log(` verified: ${u.movement_verified ? 'yes' : 'no'}`);
471
+ console.log('');
472
+ console.log(`Baseline friction (${friction.length})`);
473
+ for (const f of friction) {
474
+ console.log(` [${f.severity || '?'}] ${f.stage || '?'}: ${f.missing_artifact || f.id || ''}`);
475
+ }
476
+ console.log('');
477
+ console.log(`Fix backlog (${fixes.length})`);
478
+ for (const f of fixes) {
479
+ console.log(` #${f.priority ?? '?'} ${f.stage || '?'}: ${f.action || ''}`);
480
+ }
481
+ }
482
+
483
+ async function aeoProofs(args) {
484
+ const filter = readArg(args, '--filter', '-f');
485
+ assertNoExtras('proofs', args, []);
486
+ const root = requireBackendRoot();
487
+ const proofRoot = path.join(root, 'atris', 'features', 'aeo', 'proof');
488
+ if (!fs.existsSync(proofRoot)) {
489
+ console.error(`Proof root not found: ${proofRoot}`);
490
+ process.exit(1);
491
+ }
492
+ const needle = filter ? filter.toLowerCase() : null;
493
+ const entries = fs.readdirSync(proofRoot)
494
+ .filter((e) => fs.statSync(path.join(proofRoot, e)).isDirectory())
495
+ .filter((e) => !needle || e.toLowerCase().includes(needle))
496
+ .sort();
497
+ console.log(`AEO proof receipts at ${path.relative(process.cwd(), proofRoot)}`);
498
+ console.log('');
499
+ for (const e of entries) {
500
+ const files = fs.readdirSync(path.join(proofRoot, e)).filter((f) => f.endsWith('.json'));
501
+ console.log(` ${pad(e, 44)} ${files.length} file${files.length === 1 ? '' : 's'}`);
502
+ }
503
+ }
504
+
505
+ // ---------- SCRIPT WRAPPERS ----------
506
+
507
+ function runBackendScript(scriptName, args) {
508
+ const root = requireBackendRoot();
509
+ const script = path.join(root, 'scripts', scriptName);
510
+ if (!fs.existsSync(script)) {
511
+ console.error(`Script not found: ${script}`);
512
+ process.exit(1);
513
+ }
514
+ const py = process.env.ATRIS_PYTHON || 'python3';
515
+ const result = spawnSync(py, [script, ...args], { cwd: root, stdio: 'inherit' });
516
+ if (result.error) {
517
+ console.error(`Failed to spawn ${py}: ${result.error.message}`);
518
+ process.exit(1);
519
+ }
520
+ if (result.signal) {
521
+ const signum = os.constants?.signals?.[result.signal] ?? 0;
522
+ console.error(`${scriptName} terminated by signal ${result.signal}`);
523
+ process.exit(128 + signum);
524
+ }
525
+ process.exit(result.status ?? 1);
526
+ }
527
+
528
+ async function aeoDiscover(args) {
529
+ if (!args.length || args[0] === '--help' || args[0] === '-h') {
530
+ console.log('Usage: atris aeo discover <source> [--question Q]... [--canonical-url URL] [--out-dir DIR] [--json]');
531
+ return;
532
+ }
533
+ runBackendScript('aeo_discovery_audit.py', args);
534
+ }
535
+
536
+ async function aeoAudit(args) {
537
+ if (!args.length || args[0] === '--help' || args[0] === '-h') {
538
+ console.log('Usage: atris aeo audit <source> [--baseline B] [--canonical-url URL] [--out-dir DIR] [--task T]');
539
+ return;
540
+ }
541
+ runBackendScript('aeo_agent_usability_audit.py', args);
542
+ }
543
+
186
544
  async function run(args = []) {
187
545
  const sub = args[0];
188
546
  if (!sub || sub === 'help' || sub === '--help' || sub === '-h') return printHelp();
189
547
  const rest = args.slice(1);
190
548
  if (sub === 'init') return aeoInit(rest);
191
549
  if (sub === 'draft') return aeoDraft(rest);
550
+ if (sub === 'log') return aeoLog(rest);
551
+ if (sub === 'status') return aeoStatus(rest);
552
+ if (sub === 'packet') return aeoPacket(rest);
553
+ if (sub === 'proofs') return aeoProofs(rest);
554
+ if (sub === 'discover') return aeoDiscover(rest);
555
+ if (sub === 'audit') return aeoAudit(rest);
192
556
  console.error(`Unknown aeo subcommand: ${sub}`);
193
557
  printHelp();
194
558
  process.exit(1);
package/commands/gm.js CHANGED
@@ -4,6 +4,8 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
6
 
7
+ const AGENTXP_LEADERBOARD_URL = 'https://api.atris.ai/api/agentxp/leaderboard';
8
+
7
9
  const ROLE_PLAYERS_TO_IGNORE = new Set([
8
10
  'game-manager',
9
11
  'navigator',
@@ -26,7 +28,7 @@ function showHelp() {
26
28
  console.log('');
27
29
  console.log('Options:');
28
30
  console.log(' --manager <id> Manager id. Defaults to game-manager when present.');
29
- console.log(' --as <id> Alias for --manager.');
31
+ console.log(' --as <id> Alias for --player.');
30
32
  console.log(' --player <id> Preferred player when seeding a first local mission.');
31
33
  console.log(' --workspace <p> Read missions from another Atris workspace.');
32
34
  console.log(' --no-seed Do not create a starter player mission.');
@@ -102,7 +104,7 @@ function teamMembers(workspaceRoot) {
102
104
  }
103
105
 
104
106
  function inferManager(workspaceRoot, args = []) {
105
- const explicit = flag(args, '--manager') || flag(args, '--as') || positional(args)[0];
107
+ const explicit = flag(args, '--manager') || positional(args)[0];
106
108
  if (explicit) return { manager: slugify(explicit), source: 'flag' };
107
109
 
108
110
  for (const value of [process.env.ATRIS_GM, process.env.ATRIS_MANAGER, process.env.ATRIS_AGENT_ID]) {
@@ -160,7 +162,7 @@ function starterMissionPrompt(player) {
160
162
  }
161
163
 
162
164
  function pickSeedPlayer(workspaceRoot, tasks, args = []) {
163
- const explicit = flag(args, '--player') || flag(args, '--user');
165
+ const explicit = flag(args, '--player') || flag(args, '--user') || flag(args, '--as');
164
166
  if (explicit) return slugify(explicit);
165
167
 
166
168
  const fromTasks = playersFromTasks(tasks);
@@ -261,19 +263,29 @@ function compactTask(task) {
261
263
  };
262
264
  }
263
265
 
264
- function nextCommands({ seeded, reviewQueue, missions, players }) {
266
+ function globalSyncCommands(player) {
267
+ return [
268
+ 'atris login',
269
+ `atris xp sync --local --as ${player}`,
270
+ ];
271
+ }
272
+
273
+ function nextCommands({ seeded, reviewQueue, missions, players, manager }) {
265
274
  if (reviewQueue.length) {
266
- const ref = reviewQueue[0].ref;
275
+ const task = reviewQueue[0];
276
+ const ref = task.ref;
277
+ const player = task.assigned_to || task.claimed_by || players[0]?.player || 'player';
267
278
  return [
268
279
  `atris task show ${ref}`,
269
- `atris task accept ${ref} --proof "<human review>"`,
270
- `atris task revise ${ref} --note "<what must change>"`,
280
+ `atris task accept ${ref} --as ${player} --proof "<human review>"`,
281
+ `atris task revise ${ref} --as ${player} --note "<what must change>"`,
282
+ ...globalSyncCommands(player),
271
283
  ];
272
284
  }
273
285
  if (missions.length) {
274
286
  const mission = missions[0];
275
287
  const player = mission.assigned_to || mission.claimed_by || players[0]?.player || 'player';
276
- if (mission.status === 'open') return [`atris task claim ${mission.ref} --as ${player}`];
288
+ if (mission.status === 'open') return [`atris task claim ${mission.ref} --as ${manager || 'game-manager'}`];
277
289
  return [`atris play --as ${player}`];
278
290
  }
279
291
  if (seeded) return [`atris play --as ${seeded.assigned_to || 'player'}`];
@@ -297,7 +309,7 @@ function gmState(args = []) {
297
309
  const reviewQueue = missions.filter(task => task.status === 'review');
298
310
  const players = groupPlayers(tasks, workspaceRoot);
299
311
  const seeded = compactTask(starter.seeded);
300
- const commands = nextCommands({ seeded, reviewQueue, missions, players });
312
+ const commands = nextCommands({ seeded, reviewQueue, missions, players, manager: detected.manager });
301
313
 
302
314
  return {
303
315
  schema: 'atris.agentxp_gm_mode.v1',
@@ -318,6 +330,8 @@ function gmState(args = []) {
318
330
  review_queue: reviewQueue,
319
331
  next_commands: commands,
320
332
  xp_rule: 'GM can route missions and review proof, but AgentXP still lands only after human accept.',
333
+ global_sync_rule: 'Run atris login once before syncing to the hosted AgentXP leaderboard.',
334
+ leaderboard_url: AGENTXP_LEADERBOARD_URL,
321
335
  };
322
336
  }
323
337
 
@@ -351,6 +365,8 @@ function render(state) {
351
365
 
352
366
  console.log('');
353
367
  console.log('XP rule: no proof, no AgentXP; accept/revise stays human-gated.');
368
+ console.log('Global sync: run atris login once before hosted leaderboard sync.');
369
+ console.log(`Leaderboard: ${state.leaderboard_url}`);
354
370
  console.log('');
355
371
  console.log('Next commands:');
356
372
  for (const command of state.next_commands) console.log(`- ${command}`);
package/commands/play.js CHANGED
@@ -6,6 +6,8 @@ const fs = require('fs');
6
6
  const { spawnSync } = require('child_process');
7
7
  const { getSessionProfile, loadCredentials } = require('../utils/auth');
8
8
 
9
+ const AGENTXP_LEADERBOARD_URL = 'https://api.atris.ai/api/agentxp/leaderboard';
10
+
9
11
  function showHelp() {
10
12
  console.log('');
11
13
  console.log('Usage: atris play [--as <player>] [--workspace <path>] [--json]');
@@ -252,10 +254,17 @@ function starterMissionPrompt(player) {
252
254
  ].join(' ');
253
255
  }
254
256
 
257
+ function globalSyncCommands(player) {
258
+ return [
259
+ 'atris login',
260
+ `atris xp sync --local --as ${player}`,
261
+ ];
262
+ }
263
+
255
264
  function ensureStarterMission(taskDb, db, workspaceRoot, player, tasks, args = []) {
256
265
  if (hasFlag(args, '--no-seed')) return { tasks, seeded: null };
257
266
  if (selectMission(tasks, player)) return { tasks, seeded: null };
258
- if (!fs.existsSync(path.join(workspaceRoot, 'atris'))) return { tasks, seeded: null };
267
+ fs.mkdirSync(path.join(workspaceRoot, 'atris'), { recursive: true });
259
268
 
260
269
  const result = taskDb.addTask(db, {
261
270
  title: starterMissionTitle(),
@@ -282,7 +291,21 @@ function ensureStarterMission(taskDb, db, workspaceRoot, player, tasks, args = [
282
291
  return { tasks: refreshed, seeded };
283
292
  }
284
293
 
294
+ function playWorkspaceRoot(taskDb, workspaceArg) {
295
+ let requested = path.resolve(workspaceArg || process.cwd());
296
+ try { requested = fs.realpathSync(requested); } catch {}
297
+ if (
298
+ fs.existsSync(path.join(requested, '.git'))
299
+ || fs.existsSync(path.join(requested, 'atris'))
300
+ || fs.existsSync(path.join(requested, '.atris'))
301
+ ) {
302
+ return taskDb.workspaceRoot(requested);
303
+ }
304
+ return requested;
305
+ }
306
+
285
307
  function nextCommands(task, player) {
308
+ const helper = 'game-manager';
286
309
  if (!task) {
287
310
  return [
288
311
  `atris task delegate "AgentXP first rep: one proof-backed mission" --to ${player} --tag agent-xp`,
@@ -293,40 +316,45 @@ function nextCommands(task, player) {
293
316
  const ref = taskRef(task);
294
317
  if (task.status === 'open') {
295
318
  return [
296
- `atris task claim ${ref} --as ${player}`,
297
- `atris task ready ${ref} --proof "<artifact path + verifier result>"`,
298
- `atris task accept ${ref} --proof "<human review>"`,
319
+ `atris task claim ${ref} --as ${helper}`,
320
+ `atris task ready ${ref} --as ${helper} --proof "<artifact path + verifier result>"`,
321
+ `atris task accept ${ref} --as ${player} --proof "<human review>"`,
299
322
  'atris xp card --local',
323
+ ...globalSyncCommands(player),
300
324
  ];
301
325
  }
302
326
 
303
327
  if (task.status === 'claimed') {
328
+ const actor = task.claimed_by || helper;
304
329
  return [
305
- `atris task ready ${ref} --proof "<artifact path + verifier result>"`,
306
- `atris task accept ${ref} --proof "<human review>"`,
330
+ `atris task ready ${ref} --as ${actor} --proof "<artifact path + verifier result>"`,
331
+ `atris task accept ${ref} --as ${player} --proof "<human review>"`,
307
332
  'atris xp card --local',
333
+ ...globalSyncCommands(player),
308
334
  ];
309
335
  }
310
336
 
311
337
  if (task.status === 'review') {
312
338
  return [
313
339
  `atris task show ${ref}`,
314
- `atris task accept ${ref} --proof "<human review>"`,
315
- `atris task revise ${ref} --note "<what must change>"`,
340
+ `atris task accept ${ref} --as ${player} --proof "<human review>"`,
341
+ `atris task revise ${ref} --as ${player} --note "<what must change>"`,
316
342
  'atris xp card --local',
343
+ ...globalSyncCommands(player),
317
344
  ];
318
345
  }
319
346
 
320
347
  return [
321
348
  `atris task show ${ref}`,
322
349
  'atris xp card --local',
350
+ ...globalSyncCommands(player),
323
351
  ];
324
352
  }
325
353
 
326
354
  function modeState(args = []) {
327
355
  const taskDb = require('../lib/task-db');
328
356
  const workspaceArg = flag(args, '--workspace') || flag(args, '--root') || process.cwd();
329
- const workspaceRoot = taskDb.workspaceRoot(path.resolve(workspaceArg));
357
+ const workspaceRoot = playWorkspaceRoot(taskDb, workspaceArg);
330
358
  const db = taskDb.open();
331
359
  const rows = taskDb.listTasks(db, {
332
360
  workspaceRoot,
@@ -367,6 +395,8 @@ function modeState(args = []) {
367
395
  prompt: latestMessage(events),
368
396
  } : null,
369
397
  xp_rule: 'AgentXP lands only after proof is ready and a human accepts the task.',
398
+ global_sync_rule: 'Run atris login once before syncing to the hosted AgentXP leaderboard.',
399
+ leaderboard_url: AGENTXP_LEADERBOARD_URL,
370
400
  next_commands: commandList,
371
401
  };
372
402
  }
@@ -399,6 +429,8 @@ function render(state) {
399
429
  console.log('');
400
430
  console.log('Win condition: real artifact + verifier + human accept.');
401
431
  console.log('XP rule: no proof, no AgentXP; accept/revise stays human-gated.');
432
+ console.log('Global sync: run atris login once before hosted leaderboard sync.');
433
+ console.log(`Leaderboard: ${state.leaderboard_url}`);
402
434
  console.log('');
403
435
  console.log('Next commands:');
404
436
  for (const command of state.next_commands) console.log(`- ${command}`);
package/commands/sync.js CHANGED
@@ -48,6 +48,10 @@ function _substituteParams(content, params) {
48
48
  .replace(/\{\{workspace_template\}\}/g, params.workspace_template || 'business');
49
49
  }
50
50
 
51
+ function _templateTargetRelPath(relPath) {
52
+ return relPath === 'persona.md' ? 'PERSONA.md' : relPath;
53
+ }
54
+
51
55
  /**
52
56
  * Sync the canonical skill set from atris-cli/atris/skills/* into a
53
57
  * workspace's atris/skills/* (plus ensure .claude/skills/ symlinks).
@@ -212,14 +216,15 @@ function syncWorkspaceTemplate(targetRoot, bizMeta, options = {}) {
212
216
  const addedList = [], updatedList = [], preservedList = [];
213
217
 
214
218
  for (const relPath of templateFiles) {
219
+ const targetRelPath = _templateTargetRelPath(relPath);
215
220
  const templatePath = path.join(template.dir, relPath);
216
- const targetPath = path.join(targetAtrisDir, relPath);
221
+ const targetPath = path.join(targetAtrisDir, targetRelPath);
217
222
  let templateContent;
218
223
  try { templateContent = fs.readFileSync(templatePath, 'utf-8'); } catch { continue; }
219
224
  const finalContent = _substituteParams(templateContent, params);
220
225
 
221
226
  if (!fs.existsSync(targetPath)) {
222
- addedList.push(relPath); added++;
227
+ addedList.push(targetRelPath); added++;
223
228
  if (!dryRun) {
224
229
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
225
230
  fs.writeFileSync(targetPath, finalContent);
@@ -229,10 +234,10 @@ function syncWorkspaceTemplate(targetRoot, bizMeta, options = {}) {
229
234
  if (existing === finalContent) {
230
235
  skipped++;
231
236
  } else if (force) {
232
- updatedList.push(relPath); updated++;
237
+ updatedList.push(targetRelPath); updated++;
233
238
  if (!dryRun) fs.writeFileSync(targetPath, finalContent);
234
239
  } else {
235
- preservedList.push(relPath); preserved++;
240
+ preservedList.push(targetRelPath); preserved++;
236
241
  }
237
242
  }
238
243
  }