atris 3.15.31 → 3.15.36

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.
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { loadCredentials } = require('../utils/auth');
4
4
  const { apiRequestJson } = require('../utils/api');
5
+ const { loadBusinesses, saveBusinesses, businessMatchesSlug } = require('./business');
5
6
 
6
7
  /**
7
8
  * Resolve business slug from CLI arg or local .atris/business.json.
@@ -22,6 +23,69 @@ function resolveSlug() {
22
23
  return slug;
23
24
  }
24
25
 
26
+ async function resolveBusiness(token, slug) {
27
+ const businesses = loadBusinesses();
28
+
29
+ const listResult = await apiRequestJson('/business/', { method: 'GET', token });
30
+ if (listResult.ok && Array.isArray(listResult.data)) {
31
+ const match = listResult.data.find((business) =>
32
+ businessMatchesSlug(business, slug, { includeName: true })
33
+ );
34
+ if (match && match.id) {
35
+ businesses[slug] = {
36
+ business_id: match.id,
37
+ workspace_id: match.workspace_id,
38
+ name: match.name || slug,
39
+ slug: match.slug || slug,
40
+ canonical_slug: match.slug || slug,
41
+ added_at: businesses[slug]?.added_at || new Date().toISOString(),
42
+ updated_at: new Date().toISOString(),
43
+ };
44
+ saveBusinesses(businesses);
45
+ return businesses[slug];
46
+ }
47
+ }
48
+
49
+ const wanted = String(slug || '').toLowerCase();
50
+ return businesses[slug] || Object.values(businesses).find((entry) =>
51
+ entry
52
+ && (
53
+ String(entry.slug || '').toLowerCase() === wanted
54
+ || String(entry.canonical_slug || '').toLowerCase() === wanted
55
+ || String(entry.name || '').toLowerCase() === wanted
56
+ )
57
+ ) || null;
58
+ }
59
+
60
+ async function activateBusinessWorkspace(token, business) {
61
+ if (!business?.business_id || !business?.workspace_id) return;
62
+ const result = await apiRequestJson(`/business/${business.business_id}/workspaces/${business.workspace_id}/activate`, {
63
+ method: 'POST',
64
+ token,
65
+ body: {},
66
+ });
67
+ if (!result.ok && result.status !== 409) {
68
+ throw new Error(`Failed to activate business workspace: ${result.errorMessage || result.error || result.status}`);
69
+ }
70
+ }
71
+
72
+ async function waitForBusinessComputer(token, business, maxWait = 90000) {
73
+ const start = Date.now();
74
+ while (Date.now() - start < maxWait) {
75
+ await new Promise((r) => setTimeout(r, 3000));
76
+
77
+ const status = await apiRequestJson(`/business/${business.business_id}/ai-computer/status`, {
78
+ method: 'GET',
79
+ token,
80
+ });
81
+
82
+ if (status.ok && status.data && status.data.status === 'running' && status.data.endpoint) {
83
+ return status.data;
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
25
89
  /**
26
90
  * Pause a workspace to save compute (storage only mode).
27
91
  * @returns {Promise<void>}
@@ -39,6 +103,23 @@ async function sleepAtris() {
39
103
  const creds = loadCredentials();
40
104
  if (!creds || !creds.token) { console.error('Not logged in. Run: atris login'); process.exit(1); }
41
105
 
106
+ const business = await resolveBusiness(creds.token, slug);
107
+ if (business?.business_id) {
108
+ const result = await apiRequestJson(`/business/${business.business_id}/ai-computer/sleep`, {
109
+ method: 'POST',
110
+ token: creds.token,
111
+ body: {},
112
+ });
113
+
114
+ if (!result.ok) {
115
+ console.error(`Failed to sleep business computer: ${result.error || result.errorMessage || result.status}`);
116
+ process.exit(1);
117
+ }
118
+
119
+ console.log(`Business computer '${business.name || slug}' is now sleeping. Files persist. Wake it with: atris wake ${slug}`);
120
+ return;
121
+ }
122
+
42
123
  const result = await apiRequestJson(`/workspace/${slug}/sleep`, {
43
124
  method: 'POST',
44
125
  token: creds.token,
@@ -70,6 +151,40 @@ async function wakeAtris() {
70
151
  const creds = loadCredentials();
71
152
  if (!creds || !creds.token) { console.error('Not logged in. Run: atris login'); process.exit(1); }
72
153
 
154
+ const business = await resolveBusiness(creds.token, slug);
155
+ if (business?.business_id) {
156
+ if (business.workspace_id) {
157
+ await activateBusinessWorkspace(creds.token, business);
158
+ }
159
+
160
+ const result = await apiRequestJson(`/business/${business.business_id}/ai-computer/wake`, {
161
+ method: 'POST',
162
+ token: creds.token,
163
+ body: {},
164
+ });
165
+
166
+ if (!result.ok) {
167
+ console.error(`Failed to wake business computer: ${result.error || result.errorMessage || result.status}`);
168
+ process.exit(1);
169
+ }
170
+
171
+ console.log(`Waking business computer '${business.name || slug}'...`);
172
+
173
+ if (result.data && result.data.status === 'running' && result.data.endpoint) {
174
+ console.log(`Business computer '${business.name || slug}' is alive. Agents resuming.`);
175
+ return;
176
+ }
177
+
178
+ const running = await waitForBusinessComputer(creds.token, business);
179
+ if (running) {
180
+ console.log(`Business computer '${business.name || slug}' is alive. Agents resuming.`);
181
+ return;
182
+ }
183
+
184
+ console.log('Still starting up. Check with: atris computer status --business ' + slug);
185
+ return;
186
+ }
187
+
73
188
  const result = await apiRequestJson(`/workspace/${slug}/wake`, {
74
189
  method: 'POST',
75
190
  token: creds.token,
@@ -707,7 +707,8 @@ function worktreeReceipt(before, after, { verifier = '' } = {}) {
707
707
  // ---------------------------------------------------------------------------
708
708
  // `atris mission run <id>` — bounded local headless loop. v0.1.
709
709
  // Spawns `claude -p --resume <session>` per tick. Honors cadence, active-hours,
710
- // rate-limit info, and a flock per mission. Only consumes max-ticks on `ran`.
710
+ // rate-limit info, and a flock per mission. `max-ticks` bounds total attempts;
711
+ // `ran_ticks` separately reports ticks that actually made progress.
711
712
  // ---------------------------------------------------------------------------
712
713
 
713
714
  const MISSION_RUN_DEFAULTS = {
@@ -1284,7 +1285,7 @@ async function runMission(args) {
1284
1285
  const sessionLabel = skipWorker ? 'caller-session' : (sessionId || `pending=${pendingSessionId}`);
1285
1286
  console.error(`[mission run] ${mission.id}\n objective: ${mission.objective}\n lane: ${frozen.lane}\n cadence: ${cadence} (${cadenceSeconds}s)\n max_ticks: ${effectiveMaxTicks}, max_wall: ${maxWallSeconds}s\n session: ${sessionLabel}`);
1286
1287
 
1287
- while (ranTicks < effectiveMaxTicks) {
1288
+ while (ticks.length < effectiveMaxTicks) {
1288
1289
  const elapsedSec = (Date.now() - startedAt) / 1000;
1289
1290
  const remainingWall = maxWallSeconds - elapsedSec;
1290
1291
  if (remainingWall <= 0) { pauseReason = 'max-wall-reached'; break; }
@@ -1462,12 +1463,17 @@ async function runMission(args) {
1462
1463
  }
1463
1464
  const remainingMs = remainingWall * 1000 - 1;
1464
1465
  sleepMs = Math.min(Math.max(0, sleepMs), Math.max(0, remainingMs));
1465
- if (sleepMs > 0 && ranTicks < effectiveMaxTicks) {
1466
+ if (sleepMs > 0 && ticks.length < effectiveMaxTicks) {
1466
1467
  try { await sleep(sleepMs, controller.signal); }
1467
1468
  catch (e) { if (e.code === 'ABORTED') { pauseReason = 'aborted'; break; } throw e; }
1468
1469
  }
1469
1470
  }
1470
1471
 
1472
+ if (!pauseReason && ticks.length >= effectiveMaxTicks) {
1473
+ const lastTick = ticks[ticks.length - 1];
1474
+ if (lastTick && lastTick.status !== 'ran') pauseReason = 'max-ticks-reached';
1475
+ }
1476
+
1471
1477
  if (pauseReason && !['complete', 'ready', 'max-wall-reached'].includes(pauseReason)) {
1472
1478
  mission = saveMission({
1473
1479
  ...mission,
package/commands/play.js CHANGED
@@ -7,7 +7,7 @@ const { spawnSync } = require('child_process');
7
7
  const { getSessionProfile, loadCredentials } = require('../utils/auth');
8
8
 
9
9
  const AGENTXP_LEADERBOARD_URL = 'https://api.atris.ai/api/agentxp/leaderboard';
10
- const AGENTXP_GLOBAL_SYNC_RULE = 'Use the owner-provided sync token first; fallback is atris login before sync.';
10
+ const AGENTXP_GLOBAL_SYNC_RULE = 'Run atris login, then sync. Owner-provided sync tokens are guided-demo fallback only.';
11
11
 
12
12
  function showHelp() {
13
13
  console.log('');
@@ -257,9 +257,9 @@ function starterMissionPrompt(player) {
257
257
 
258
258
  function globalSyncCommands(player) {
259
259
  return [
260
- `atris xp sync --local --as ${player} --token <owner-provided-token>`,
261
260
  'atris login',
262
261
  `atris xp sync --local --as ${player}`,
262
+ `atris xp sync --local --as ${player} --token <owner-provided-token>`,
263
263
  ];
264
264
  }
265
265
 
@@ -431,7 +431,7 @@ function render(state) {
431
431
  console.log('');
432
432
  console.log('Win condition: real artifact + verifier + human accept.');
433
433
  console.log('XP rule: no proof, no AgentXP; accept/revise stays human-gated.');
434
- console.log('Global sync: use owner token first; fallback to atris login before hosted leaderboard sync.');
434
+ console.log('Global sync: run atris login, then sync; owner tokens are guided-demo fallback only.');
435
435
  console.log(`Leaderboard: ${state.leaderboard_url}`);
436
436
  console.log('');
437
437
  console.log('Next commands:');
package/commands/xp.js CHANGED
@@ -20,6 +20,50 @@ const AGENT_XP_LABEL = 'AgentXP';
20
20
  const AGENTXP_LEADERBOARD_URL = 'https://api.atris.ai/api/agentxp/leaderboard';
21
21
  const LEVEL_XP = 1000;
22
22
  const RECEIPT_CHAIN_VERSION = 'atris.career_xp_receipt_chain.v1';
23
+ const VALIDATION_TERMS = ['verify', 'verified', 'verifier', 'passed', 'pytest', 'receipt', 'score', 'smoke', 'git diff --check'];
24
+ const QUALITY_TERMS = ['craft', 'scorecard', 'quality', 'critique', 'review', 'taste'];
25
+ const ATRIS_ACCEPTED_STATUSES = new Set(['accepted', 'approved', 'done', 'review', 'reviewed', 'unknown']);
26
+ const ATRIS_ACCEPTED_EVENTS = new Set(['accepted', 'approved', 'completed', 'done', 'reviewed', 'unknown']);
27
+ const EARNING_MODEL = {
28
+ schema: 'atris.agentxp_earning_model.v1',
29
+ score_name: AGENT_XP_LABEL,
30
+ primary_public_source: 'accepted_task_receipt',
31
+ public_rule: 'No accepted proof-backed task episodes means no AgentXP leaderboard movement.',
32
+ weights: [
33
+ {
34
+ id: 'accepted_task_receipt',
35
+ label: 'Accepted proof-backed task',
36
+ relative_weight: 1,
37
+ public_leaderboard: true,
38
+ rl_value: 'high',
39
+ },
40
+ {
41
+ id: 'validated_agent_loop',
42
+ label: 'Verifier or agent loop inside an accepted task',
43
+ relative_weight: 0.35,
44
+ public_leaderboard: true,
45
+ rl_value: 'high',
46
+ },
47
+ {
48
+ id: 'atris_action_signal',
49
+ label: 'Atris action signal',
50
+ relative_weight: 0.1,
51
+ public_leaderboard: false,
52
+ rl_value: 'medium',
53
+ },
54
+ {
55
+ id: 'local_assistant_activity',
56
+ label: 'Local assistant activity',
57
+ relative_weight: 0.05,
58
+ public_leaderboard: false,
59
+ rl_value: 'medium',
60
+ },
61
+ ],
62
+ caps: {
63
+ local_assistant_activity: 'Capped below 10% of accepted-task XP and never unlocks public ranking alone.',
64
+ atris_action_signal: 'Routes RL and next moves, but does not outrank an accepted proof-backed task.',
65
+ },
66
+ };
23
67
  const XP_STATE_FILES = new Set([
24
68
  path.basename(TASK_EPISODES_FILE),
25
69
  path.basename(CAREER_XP_RECEIPTS_FILE),
@@ -421,6 +465,199 @@ function sqlString(value) {
421
465
  return `'${String(value).replace(/'/g, "''")}'`;
422
466
  }
423
467
 
468
+ function existingSignal(label, filePath) {
469
+ if (!filePath) return null;
470
+ try {
471
+ const stat = fs.statSync(expandHome(filePath));
472
+ return {
473
+ label,
474
+ kind: stat.isDirectory() ? 'dir' : 'file',
475
+ size_bytes: stat.isFile() ? stat.size : null,
476
+ mtime: stat.mtime.toISOString(),
477
+ };
478
+ } catch (_) {
479
+ return null;
480
+ }
481
+ }
482
+
483
+ function localAssistantProviderSignals(workspace) {
484
+ const codexStatePath = process.env.CODEX_STATE_DB || CODEX_STATE_FILE;
485
+ const candidates = [
486
+ {
487
+ id: 'codex',
488
+ label: 'Codex',
489
+ signals: [
490
+ existingSignal('codex_state', codexStatePath),
491
+ existingSignal('codex_history', path.join(os.homedir(), '.codex', 'history.jsonl')),
492
+ existingSignal('codex_sessions', path.join(os.homedir(), '.codex', 'sessions')),
493
+ ],
494
+ },
495
+ {
496
+ id: 'claude',
497
+ label: 'Claude',
498
+ signals: [
499
+ existingSignal('claude_home', path.join(os.homedir(), '.claude')),
500
+ existingSignal('claude_config', path.join(os.homedir(), '.claude.json')),
501
+ existingSignal('workspace_claude', path.join(workspace, '.claude')),
502
+ ],
503
+ },
504
+ {
505
+ id: 'cursor',
506
+ label: 'Cursor',
507
+ signals: [
508
+ existingSignal('workspace_cursor', path.join(workspace, '.cursor')),
509
+ existingSignal('cursor_home', path.join(os.homedir(), '.cursor')),
510
+ existingSignal('cursor_app_support', path.join(os.homedir(), 'Library', 'Application Support', 'Cursor')),
511
+ ],
512
+ },
513
+ {
514
+ id: 'devin',
515
+ label: 'Devin',
516
+ signals: [
517
+ existingSignal('workspace_devin', path.join(workspace, '.devin')),
518
+ existingSignal('devin_home', path.join(os.homedir(), '.devin')),
519
+ ],
520
+ },
521
+ ];
522
+
523
+ return candidates.map((provider) => {
524
+ const signals = provider.signals.filter(Boolean);
525
+ return {
526
+ id: provider.id,
527
+ label: provider.label,
528
+ detected: signals.length > 0,
529
+ signal_count: signals.length,
530
+ signals,
531
+ };
532
+ });
533
+ }
534
+
535
+ function buildLocalAssistantActivity(workspace, acceptedTaskXp = 0) {
536
+ const providers = localAssistantProviderSignals(workspace);
537
+ const detected = providers.filter(provider => provider.detected);
538
+ const rawSignalCount = detected.reduce((sum, provider) => sum + provider.signal_count, 0);
539
+ const weight = EARNING_MODEL.weights.find(item => item.id === 'local_assistant_activity')?.relative_weight || 0.05;
540
+ const weightedContext = Math.round(rawSignalCount * weight * 100) / 100;
541
+ const acceptedXp = asNumber(acceptedTaskXp);
542
+ const contextCap = Math.round(acceptedXp * 0.1 * 100) / 100;
543
+ const contextAgentXp = acceptedXp > 0 ? Math.min(weightedContext, contextCap) : 0;
544
+
545
+ return {
546
+ schema: 'atris.agentxp_local_assistant_activity.v1',
547
+ public_leaderboard: false,
548
+ source_type: 'local_assistant_activity',
549
+ weight,
550
+ context_weight: weight,
551
+ cap: {
552
+ max_public_xp_ratio: 0.1,
553
+ requires_accepted_task_xp: true,
554
+ },
555
+ cap_ratio_to_accepted_task_xp: 0.1,
556
+ cap_agent_xp: contextCap,
557
+ accepted_task_agent_xp: acceptedXp,
558
+ detected_providers: detected.map(provider => provider.id).sort(),
559
+ provider_count: detected.length,
560
+ providers,
561
+ raw_signal_count: rawSignalCount,
562
+ weighted_context_score: weightedContext,
563
+ context_agent_xp: contextAgentXp,
564
+ included_in_total_agent_xp: false,
565
+ role: 'context_only',
566
+ note: 'Local assistant activity helps context and RL; accepted proof-backed tasks are the public AgentXP source.',
567
+ };
568
+ }
569
+
570
+ function safeSignalKey(value) {
571
+ const text = String(value || '').trim().toLowerCase().replace(/[\s_]+/g, '-');
572
+ const clean = [...text].filter(char => /[a-z0-9_-]/.test(char)).join('');
573
+ return clean.slice(0, 40) || 'unknown';
574
+ }
575
+
576
+ function hasAnyTerm(text, terms) {
577
+ const lower = String(text || '').toLowerCase();
578
+ return terms.some(term => lower.includes(term));
579
+ }
580
+
581
+ function incrementCount(counts, key) {
582
+ counts[key] = (counts[key] || 0) + 1;
583
+ }
584
+
585
+ function buildAtrisActionSignalFromRows(rows = []) {
586
+ const eventTypeCounts = {};
587
+ const statusCounts = {};
588
+ const actors = new Set();
589
+ const claimants = new Set();
590
+ let proofBackedEpisodeCount = 0;
591
+ let acceptedEpisodeCount = 0;
592
+ let validationReceiptCount = 0;
593
+ let qualityReceiptCount = 0;
594
+ let claimedEpisodeCount = 0;
595
+
596
+ for (const row of rows || []) {
597
+ const action = row?.action && typeof row.action === 'object' ? row.action : {};
598
+ const state = row?.state && typeof row.state === 'object' ? row.state : {};
599
+ const eventType = safeSignalKey(action.event_type || action.type || action.name);
600
+ const status = safeSignalKey(state.status);
601
+ incrementCount(eventTypeCounts, eventType);
602
+ incrementCount(statusCounts, status);
603
+
604
+ const actor = slugify(action.actor || '');
605
+ if (actor) actors.add(actor);
606
+ const claimant = slugify(state.claimed_by || state.assigned_to || '');
607
+ if (claimant) {
608
+ claimants.add(claimant);
609
+ claimedEpisodeCount += 1;
610
+ }
611
+
612
+ const proof = String(row?.proof || '').trim();
613
+ const proofBacked = proof.length >= 20;
614
+ if (proofBacked) proofBackedEpisodeCount += 1;
615
+ if (proofBacked && hasAnyTerm(proof, VALIDATION_TERMS)) validationReceiptCount += 1;
616
+ if (proofBacked && hasAnyTerm(proof, QUALITY_TERMS)) qualityReceiptCount += 1;
617
+ if (
618
+ proofBacked
619
+ && status !== 'failed'
620
+ && (ATRIS_ACCEPTED_STATUSES.has(status) || ATRIS_ACCEPTED_EVENTS.has(eventType))
621
+ ) {
622
+ acceptedEpisodeCount += 1;
623
+ }
624
+ }
625
+
626
+ return {
627
+ schema: 'atris.agentxp_atris_action_signal.v1',
628
+ source: '.atris/state/task_episodes.jsonl',
629
+ episode_count: rows.length,
630
+ accepted_episode_count: acceptedEpisodeCount,
631
+ proof_backed_episode_count: proofBackedEpisodeCount,
632
+ validation_receipt_count: validationReceiptCount,
633
+ quality_receipt_count: qualityReceiptCount,
634
+ claimed_episode_count: claimedEpisodeCount,
635
+ reviewed_episode_count: eventTypeCounts.reviewed || 0,
636
+ distinct_actor_count: actors.size,
637
+ distinct_claimant_count: claimants.size,
638
+ event_type_counts: Object.fromEntries(Object.entries(eventTypeCounts).sort()),
639
+ status_counts: Object.fromEntries(Object.entries(statusCounts).sort()),
640
+ included_in_total_agent_xp: false,
641
+ public_leaderboard: false,
642
+ role: 'rl_routing_only',
643
+ };
644
+ }
645
+
646
+ function readTaskEpisodesForWorkspace(workspace) {
647
+ if (!workspace) return [];
648
+ return readJsonl(path.join(workspace, TASK_EPISODES_FILE));
649
+ }
650
+
651
+ function buildAtrisActionSignal(projection) {
652
+ const workspaceRoots = Array.isArray(projection?.workspaces)
653
+ ? projection.workspaces
654
+ .filter(workspace => workspace?.included && workspace.workspace_root)
655
+ .map(workspace => workspace.workspace_root)
656
+ : [projection?.workspace_root].filter(Boolean);
657
+ const rows = workspaceRoots.flatMap(workspace => readTaskEpisodesForWorkspace(workspace));
658
+ return buildAtrisActionSignalFromRows(rows);
659
+ }
660
+
424
661
  function runSqliteJsonOptional(dbPath, sql) {
425
662
  if (!dbPath || !fs.existsSync(dbPath)) return [];
426
663
  const result = spawnSync('sqlite3', ['-readonly', '-json', dbPath, sql], { encoding: 'utf8' });
@@ -682,6 +919,7 @@ function buildCareerXpProjection(receipts, workspace, integrity = {}) {
682
919
  percent: Math.round((currentLevelXp / LEVEL_XP) * 1000) / 10,
683
920
  };
684
921
  const latest = latestReceipt(accepted);
922
+ const localActivity = buildLocalAssistantActivity(workspace, totalXp);
685
923
 
686
924
  return {
687
925
  schema: 'atris.career_xp_projection.v1',
@@ -707,6 +945,8 @@ function buildCareerXpProjection(receipts, workspace, integrity = {}) {
707
945
  contribution_graph: buildAgentXpContributionGraph(accepted),
708
946
  receipts_count: accepted.length,
709
947
  sources: countBySource(accepted),
948
+ earning_model: EARNING_MODEL,
949
+ local_activity: localActivity,
710
950
  latest_accepted_proof: latest ? {
711
951
  label: receiptLabel(latest),
712
952
  receipt_id: latest.receipt_id,
@@ -929,6 +1169,7 @@ function buildAllCareerXpProjection(projections, searchRoots = []) {
929
1169
  integrity_status: projection?.integrity_status || projection?.integrity?.status || 'unknown',
930
1170
  leaderboard_eligible: Boolean(projection?.leaderboard_eligible),
931
1171
  latest_accepted_proof: projection?.latest_accepted_proof || null,
1172
+ local_activity: projection?.local_activity || null,
932
1173
  ledger: projection?.ledger || null,
933
1174
  };
934
1175
  }).sort((a, b) => {
@@ -960,6 +1201,17 @@ function buildAllCareerXpProjection(projections, searchRoots = []) {
960
1201
  const contributionGraph = combineAgentXpContributionGraphs(
961
1202
  verified.map(projection => projection.contribution_graph),
962
1203
  );
1204
+ const activityProviders = new Set();
1205
+ let rawActivitySignals = 0;
1206
+ let contextAgentXp = 0;
1207
+ for (const projection of verified) {
1208
+ const activity = projection.local_activity || {};
1209
+ rawActivitySignals += asNumber(activity.raw_signal_count);
1210
+ contextAgentXp += asNumber(activity.context_agent_xp);
1211
+ for (const provider of activity.detected_providers || []) {
1212
+ activityProviders.add(provider);
1213
+ }
1214
+ }
963
1215
 
964
1216
  return {
965
1217
  schema: 'atris.career_xp_profile.v1',
@@ -985,8 +1237,19 @@ function buildAllCareerXpProjection(projections, searchRoots = []) {
985
1237
  },
986
1238
  contribution_graph: contributionGraph,
987
1239
  receipts_count: verified.reduce((sum, projection) => sum + asNumber(projection.receipts_count), 0),
1240
+ earning_model: EARNING_MODEL,
1241
+ local_activity: {
1242
+ schema: 'atris.agentxp_local_activity_summary.v1',
1243
+ public_leaderboard: false,
1244
+ detected_providers: Array.from(activityProviders).sort(),
1245
+ raw_signal_count: rawActivitySignals,
1246
+ context_agent_xp: Math.round(contextAgentXp * 100) / 100,
1247
+ included_in_total_agent_xp: false,
1248
+ note: 'Aggregated local assistant activity is context only; verified receipts define total AgentXP.',
1249
+ },
988
1250
  latest_accepted_proof: latest,
989
1251
  workspaces,
1252
+ integrity_status: warnings.length ? 'warnings' : 'verified',
990
1253
  integrity: {
991
1254
  status: warnings.length ? 'warnings' : 'verified',
992
1255
  warnings,
@@ -1408,6 +1671,13 @@ function renderContributionGraph(graph) {
1408
1671
  console.log('Legend: blank none | . started | : solid | * heavy | # breakout');
1409
1672
  }
1410
1673
 
1674
+ function renderLocalActivity(activity) {
1675
+ if (!activity || !Array.isArray(activity.detected_providers) || !activity.detected_providers.length) return;
1676
+ console.log(
1677
+ `Local activity: ${activity.detected_providers.join(', ')} | context ${formatNumber(activity.context_agent_xp)} | not public ${AGENT_XP_LABEL}`
1678
+ );
1679
+ }
1680
+
1411
1681
  function loadLocalPayload(args) {
1412
1682
  const workspace = path.resolve(readFlag(args, '--workspace', process.cwd()));
1413
1683
  const operator = readFlag(
@@ -1532,11 +1802,28 @@ function buildAgentXpSyncPacket(args = []) {
1532
1802
  trust_rule: 'Only verified local ledgers are uploaded; raw proof and paths stay local.',
1533
1803
  },
1534
1804
  local_evidence: {
1805
+ schema: 'atris.agentxp_local_evidence.v1',
1806
+ workspace_root_hash: workspaceRootHash,
1535
1807
  workspaces,
1536
1808
  verified_workspace_count: asNumber(projection.verified_workspace_count, verifiedProjection(projection) ? 1 : 0),
1537
1809
  receipts_count: receiptsCount,
1538
1810
  integrity_status: projection.integrity?.status || projection.integrity_status || 'unknown',
1539
1811
  ledger_head_hash: projection.integrity?.head_hash || null,
1812
+ local_activity: {
1813
+ schema: projection.local_activity?.schema || 'atris.agentxp_local_assistant_activity.v1',
1814
+ detected_providers: projection.local_activity?.detected_providers || [],
1815
+ provider_count: asNumber(projection.local_activity?.provider_count, (projection.local_activity?.detected_providers || []).length),
1816
+ raw_signal_count: asNumber(projection.local_activity?.raw_signal_count),
1817
+ context_agent_xp: asNumber(projection.local_activity?.context_agent_xp),
1818
+ context_weight: asNumber(projection.local_activity?.context_weight, 0.05),
1819
+ cap_ratio_to_accepted_task_xp: asNumber(projection.local_activity?.cap_ratio_to_accepted_task_xp, 0.1),
1820
+ cap_agent_xp: asNumber(projection.local_activity?.cap_agent_xp),
1821
+ accepted_task_agent_xp: asNumber(projection.local_activity?.accepted_task_agent_xp, totalXp),
1822
+ included_in_total_agent_xp: false,
1823
+ public_leaderboard: false,
1824
+ role: 'context_only',
1825
+ },
1826
+ atris_actions: buildAtrisActionSignal(projection),
1540
1827
  },
1541
1828
  gm_projection: {
1542
1829
  schema: 'atris.gm_xp_projection.v1',
@@ -1583,7 +1870,10 @@ async function syncAgentXp(args = []) {
1583
1870
  } else {
1584
1871
  const ensured = await ensureValidCredentials(apiRequestJson);
1585
1872
  if (ensured.error) {
1586
- throw new Error(`Missing sync auth. Run atris login, or set ATRIS_AGENTXP_SYNC_TOKEN${ensured.detail ? ` (${ensured.detail})` : ''}.`);
1873
+ throw new Error(
1874
+ `Missing sync auth. Run atris login, then retry atris xp sync. `
1875
+ + `Guided demos can pass --token <owner-provided-token>${ensured.detail ? ` (${ensured.detail})` : ''}.`
1876
+ );
1587
1877
  }
1588
1878
  options.token = ensured.credentials.token;
1589
1879
  }
@@ -1609,7 +1899,8 @@ function renderSync(payload) {
1609
1899
  console.log(`Player ${payload.player || entry.username || 'player'} | AgentXP ${formatNumber(entry.agent_xp)} | receipts ${formatNumber(entry.verified_receipts)}`);
1610
1900
  if (payload.dry_run) {
1611
1901
  console.log(`Packet ${payload.packet?.packet_hash || 'unhashed'} ready; no network upload ran.`);
1612
- console.log('Run with ATRIS_AGENTXP_SYNC_TOKEN set to publish to the hosted leaderboard.');
1902
+ console.log(`Run atris login, then atris xp sync --local --as ${payload.player || entry.username || '<player>'} to publish to the hosted leaderboard.`);
1903
+ console.log('Guided demos can pass --token <owner-provided-token>.');
1613
1904
  console.log(`Leaderboard: ${AGENTXP_LEADERBOARD_URL}`);
1614
1905
  return;
1615
1906
  }
@@ -1673,6 +1964,7 @@ function render(payload) {
1673
1964
  } else {
1674
1965
  console.log('Latest proof: none accepted yet');
1675
1966
  }
1967
+ renderLocalActivity(payload.local_activity);
1676
1968
  const integrity = payload.integrity || {};
1677
1969
  console.log(`Integrity: ${integrity.status || 'unknown'} (${integrity.local_trust || 'local'})`);
1678
1970
  for (const warning of integrity.warnings || []) {
@@ -1694,6 +1986,7 @@ function render(payload) {
1694
1986
  } else {
1695
1987
  console.log('Latest proof: none accepted yet');
1696
1988
  }
1989
+ renderLocalActivity(payload.local_activity);
1697
1990
  console.log(`Ledger: ${payload.ledger?.projection_path || CAREER_XP_PROJECTION_FILE}`);
1698
1991
  return;
1699
1992
  }