atris 3.15.22 → 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.
@@ -132,6 +132,79 @@ function printJsonOrText(payload, lines, asJson) {
132
132
  for (const line of lines) console.log(line);
133
133
  }
134
134
 
135
+ function loadTaskDb(asJson = false) {
136
+ try {
137
+ return require('../lib/task-db');
138
+ } catch (error) {
139
+ const message = error && error.message ? error.message : String(error);
140
+ if (error?.code === 'ERR_UNKNOWN_BUILTIN_MODULE' || /node:sqlite/.test(message)) {
141
+ exitMissionError('AgentXP mission tasks require Node 22+ with node:sqlite.', 2, asJson);
142
+ }
143
+ throw error;
144
+ }
145
+ }
146
+
147
+ function writeMissionTaskProjection(taskDb, db, workspaceRoot) {
148
+ const projection = taskDb.taskProjection(db, { workspaceRoot, limit: 500 });
149
+ const outPath = path.join(workspaceRoot, '.atris', 'state', 'tasks.projection.json');
150
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
151
+ fs.writeFileSync(outPath, JSON.stringify(projection, null, 2) + '\n', 'utf8');
152
+ return { projection, outPath };
153
+ }
154
+
155
+ function missionTaskRef(task) {
156
+ return task?.display_id || task?.legacy_ref || String(task?.id || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase().slice(0, 8);
157
+ }
158
+
159
+ function createMissionXpTask(mission, root = process.cwd(), asJson = false) {
160
+ const taskDb = loadTaskDb(asJson);
161
+ const db = taskDb.open();
162
+ const workspaceRoot = taskDb.workspaceRoot(root);
163
+ const title = `Mission XP: ${mission.objective}`;
164
+ const metadata = {
165
+ assigned_to: mission.owner,
166
+ delegate_via: 'mission_goal_loop',
167
+ created_for_day: todayName(),
168
+ goal_id: mission.id,
169
+ goal_objective: mission.objective,
170
+ mission_id: mission.id,
171
+ mission_objective: mission.objective,
172
+ mission_owner: mission.owner,
173
+ mission_lane: mission.lane,
174
+ mission_runner: mission.runner,
175
+ verify: mission.verifier || null,
176
+ stop_condition: mission.stop_condition || null,
177
+ };
178
+ const result = taskDb.addTask(db, {
179
+ title,
180
+ tag: 'agent-xp',
181
+ workspaceRoot,
182
+ sourceKey: `mission-xp:${mission.id}`,
183
+ status: 'claimed',
184
+ claimedBy: mission.owner,
185
+ metadata,
186
+ });
187
+ const rows = taskDb.withTaskDisplayRefs(taskDb.listTasks(db, { workspaceRoot }));
188
+ const task = rows.find(row => row.id === result.id);
189
+ if (task) {
190
+ taskDb.noteTask(db, {
191
+ id: task.id,
192
+ actor: process.env.ATRIS_AGENT_ID || mission.owner || 'mission-lead',
193
+ content: `Mission goal loop XP bridge for ${mission.id}. Proof goes through task ready; AgentXP lands only after human accept.`,
194
+ });
195
+ }
196
+ const { outPath } = writeMissionTaskProjection(taskDb, db, workspaceRoot);
197
+ return {
198
+ task_id: result.id,
199
+ ref: missionTaskRef(task) || result.id,
200
+ title,
201
+ status: task?.status || 'claimed',
202
+ assigned_to: mission.owner,
203
+ inserted: result.inserted !== false,
204
+ projection_path: outPath,
205
+ };
206
+ }
207
+
135
208
  function statePaths(root = process.cwd()) {
136
209
  const stateDir = path.join(root, '.atris', 'state');
137
210
  return {
@@ -323,6 +396,7 @@ function renderMemberNowMarkdown(owner, missions) {
323
396
  lines.push(`- cadence: ${mission.cadence}`);
324
397
  lines.push(`- runner: ${mission.runner}`);
325
398
  lines.push(`- lane: ${mission.lane}`);
399
+ if (mission.xp_task?.ref) lines.push(`- AgentXP task: ${mission.xp_task.ref}`);
326
400
  if (mission.verifier) lines.push(`- verifier: ${mission.verifier}`);
327
401
  if (mission.stop_condition) lines.push(`- stop: ${mission.stop_condition}`);
328
402
  if (mission.next_action) lines.push(`- next: ${mission.next_action}`);
@@ -366,6 +440,7 @@ function renderMissionStatus(root = process.cwd()) {
366
440
  lines.push(` - owner: ${mission.owner}`);
367
441
  lines.push(` - state: ${mission.status}`);
368
442
  lines.push(` - next: ${mission.next_action || 'tick or verify'}`);
443
+ if (mission.xp_task?.ref) lines.push(` - AgentXP task: ${mission.xp_task.ref}`);
369
444
  if (mission.receipt_path) lines.push(` - proof: ${mission.receipt_path}`);
370
445
  }
371
446
  lines.push('');
@@ -376,6 +451,18 @@ function renderMissionStatus(root = process.cwd()) {
376
451
  return paths.statusNow;
377
452
  }
378
453
 
454
+ function missionXpTaskRefFromMission(mission) {
455
+ if (mission?.xp_task?.ref) return mission.xp_task.ref;
456
+ if (mission?.xp_task_enabled && mission?.task_ids?.[0]) return mission.task_ids[0];
457
+ return '';
458
+ }
459
+
460
+ function missionXpReadyAction(mission, receiptPath) {
461
+ const ref = missionXpTaskRefFromMission(mission);
462
+ if (!ref || !receiptPath) return null;
463
+ return `queue AgentXP review: atris task ready ${ref} --proof "${receiptPath}"`;
464
+ }
465
+
379
466
  function missionFromArgs(args) {
380
467
  const objective = stripKnownFlags(args, [
381
468
  '--owner',
@@ -387,7 +474,7 @@ function missionFromArgs(args) {
387
474
  '--stop',
388
475
  '--task',
389
476
  '--ask',
390
- ], ['--json', '--always-on']).join(' ').trim();
477
+ ], ['--json', '--always-on', '--xp-task', '--agent-xp']).join(' ').trim();
391
478
  if (!objective) {
392
479
  exitMissionError('Usage: atris mission start "<objective>" --owner <member> [--verify "..."] [--cadence manual]', 1, wantsJson(args));
393
480
  }
@@ -401,6 +488,7 @@ function missionFromArgs(args) {
401
488
  const taskIds = readRepeatedFlag(args, '--task');
402
489
  const humanAsks = readRepeatedFlag(args, '--ask');
403
490
  const alwaysOn = hasFlag(args, '--always-on');
491
+ const xpTaskEnabled = hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp');
404
492
  const id = missionId(objective);
405
493
  const mission = {
406
494
  schema: 'atris.mission.v1',
@@ -414,6 +502,7 @@ function missionFromArgs(args) {
414
502
  lane,
415
503
  verifier,
416
504
  always_on: alwaysOn,
505
+ xp_task_enabled: xpTaskEnabled,
417
506
  stop_condition: stopCondition,
418
507
  task_ids: taskIds,
419
508
  human_asks: humanAsks,
@@ -437,6 +526,14 @@ function missingVerifierWarning(mission) {
437
526
  function startMission(args) {
438
527
  const asJson = wantsJson(args);
439
528
  const mission = missionFromArgs(args);
529
+ if (mission.xp_task_enabled) {
530
+ const xpTask = createMissionXpTask(mission, process.cwd(), asJson);
531
+ mission.xp_task = xpTask;
532
+ mission.task_ids = Array.from(new Set([...(mission.task_ids || []), xpTask.task_id]));
533
+ if (!mission.verifier && !mission.always_on) {
534
+ mission.next_action = `work task then run: atris task ready ${xpTask.ref} --proof "<proof>"`;
535
+ }
536
+ }
440
537
  const warnings = [missingVerifierWarning(mission)].filter(Boolean);
441
538
  ensureMemberMissionFile(mission.owner, process.cwd(), mission.objective);
442
539
  const { mission: saved } = saveMission(mission, process.cwd(), 'mission_started', { objective: mission.objective });
@@ -455,6 +552,7 @@ function startMission(args) {
455
552
  `Owner: ${saved.owner}`,
456
553
  `State: ${saved.status}`,
457
554
  ...warnings.map((warning) => `Warning: ${warning.message}`),
555
+ ...(saved.xp_task ? [`AgentXP task: ${saved.xp_task.ref}`] : []),
458
556
  `Next: atris mission tick ${saved.id}`,
459
557
  ],
460
558
  asJson,
@@ -694,6 +792,10 @@ function codexGoalObjective(mission) {
694
792
  }
695
793
 
696
794
  function codexGoalNextCommand(mission) {
795
+ if (mission.status === 'ready') {
796
+ const xpAction = missionXpReadyAction(mission, mission.receipt_path);
797
+ if (xpAction) return xpAction.replace(/^queue AgentXP review: /, '');
798
+ }
697
799
  if (mission.verifier && missionDueAt(mission)) {
698
800
  return 'atris mission run --due --max-ticks 1 --complete-on-pass';
699
801
  }
@@ -1294,7 +1396,9 @@ async function runMission(args) {
1294
1396
  worktree: tickWorktree,
1295
1397
  });
1296
1398
 
1399
+ const xpReadyAction = missionXpReadyAction(mission, receiptPath);
1297
1400
  const newStatus = (verifierResult?.passed && mission.always_on) ? 'running' :
1401
+ (verifierResult?.passed && xpReadyAction) ? 'ready' :
1298
1402
  (verifierResult?.passed && completeOnPass) ? 'complete' :
1299
1403
  (verifierResult?.passed ? 'ready' :
1300
1404
  (verifierResult ? 'blocked' :
@@ -1302,6 +1406,8 @@ async function runMission(args) {
1302
1406
  let nextAction = mission.next_action;
1303
1407
  if (verifierResult?.passed && mission.always_on) {
1304
1408
  nextAction = nextCandidateTickAction(mission);
1409
+ } else if (verifierResult?.passed && xpReadyAction) {
1410
+ nextAction = xpReadyAction;
1305
1411
  } else if (verifierResult?.passed && completeOnPass) {
1306
1412
  nextAction = 'mission complete';
1307
1413
  } else if (verifierResult?.passed) {
@@ -1482,9 +1588,10 @@ function tickMission(args) {
1482
1588
  let status = 'running';
1483
1589
  let nextAction = mission.verifier ? `run verifier: ${mission.verifier}` : 'attach task, verifier, or proof';
1484
1590
  if (verifierResult?.passed) {
1485
- status = (completeOnPass && !mission.always_on) ? 'complete' : 'ready';
1591
+ const xpReadyAction = missionXpReadyAction(mission, receiptPath);
1592
+ status = (completeOnPass && !mission.always_on && !xpReadyAction) ? 'complete' : 'ready';
1486
1593
  nextAction = mission.always_on ? nextCandidateTickAction(mission) :
1487
- (completeOnPass ? 'mission complete' : `review proof then run: atris mission complete ${mission.id} --proof "${receiptPath}"`);
1594
+ (xpReadyAction || (completeOnPass ? 'mission complete' : `review proof then run: atris mission complete ${mission.id} --proof "${receiptPath}"`));
1488
1595
  } else if (verifierResult) {
1489
1596
  status = 'blocked';
1490
1597
  nextAction = 'fix verifier failure or revise mission';
@@ -1549,9 +1656,10 @@ function completeMission(args) {
1549
1656
  const { mission: saved } = saveMission(next, process.cwd(), 'mission_completed', { proof });
1550
1657
  const logPath = appendMemberLog(saved.owner, 'Mission completed', { mission: saved.objective, proof });
1551
1658
  const codexGoalState = refreshCodexGoalController(process.cwd());
1659
+ const xpNextCommand = missionXpReadyAction(saved, proof);
1552
1660
  printJsonOrText(
1553
- { ok: true, action: 'mission_completed', mission: saved, log_path: logPath, codex_goal_state: codexGoalState },
1554
- [`Completed mission: ${saved.objective}`, `Proof: ${proof}`],
1661
+ { ok: true, action: 'mission_completed', mission: saved, log_path: logPath, codex_goal_state: codexGoalState, xp_next_command: xpNextCommand },
1662
+ [`Completed mission: ${saved.objective}`, `Proof: ${proof}`, ...(xpNextCommand ? [`AgentXP: ${xpNextCommand}`] : [])],
1555
1663
  asJson,
1556
1664
  );
1557
1665
  }
@@ -1683,7 +1791,7 @@ function help() {
1683
1791
  console.log(`
1684
1792
  atris mission - durable goal + loop + owner + proof state
1685
1793
 
1686
- atris mission start "<objective>" --owner <member> [--verify "..."] [--always-on]
1794
+ atris mission start "<objective>" --owner <member> [--verify "..."] [--always-on] [--xp-task]
1687
1795
  atris mission status [id] [--status <state>] [--limit <n>] [--json]
1688
1796
  atris mission goal [--heartbeat] [--json]
1689
1797
  atris mission goal-loop [--max-wall 28800] [--max-iterations 32] [--no-claude] [--json]
@@ -1696,13 +1804,15 @@ atris mission - durable goal + loop + owner + proof state
1696
1804
  Autonomy recipe:
1697
1805
  1. Pick an owner member: atris member create <member> (if missing)
1698
1806
  2. Start a current-agent mission with a verifier:
1699
- atris mission start "ship one proof" --owner <member> --runner codex_goal --lane code --verify "npm test" --stop "verifier passes"
1807
+ atris mission start "ship one proof" --owner <member> --runner codex_goal --lane code --verify "npm test" --stop "verifier passes" --xp-task
1700
1808
  3. Codex sessions: atris mission goal --json, then set /goal to goal.objective
1701
1809
  Overnight controller: atris mission goal --heartbeat --json
1702
1810
  Bounded overnight runner: atris mission goal-loop --max-wall 28800 --no-claude --json
1703
1811
  4. Do one bounded step, then record it:
1704
1812
  atris mission tick <id> --verify --summary "what changed"
1705
1813
  5. Close or continue from the receipt:
1814
+ atris task ready <xp_task_ref> --proof "<receipt_path>" (if --xp-task)
1815
+ atris task accept <xp_task_ref> --reward <n> (human accept mints AgentXP)
1706
1816
  atris mission complete <id> --proof "<receipt_path>"
1707
1817
  repeat status -> step -> tick for current-agent work
1708
1818
  atris mission run <id> --max-ticks 4 --complete-on-pass (Claude/always-on runner)
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
  }
package/commands/xp.js CHANGED
@@ -17,6 +17,7 @@ const CAREER_XP_SESSIONS_DIR = path.join('.atris', 'state', 'career_xp_sessions'
17
17
  const TASK_PROJECTION_FILE = path.join('.atris', 'state', 'tasks.projection.json');
18
18
  const CODEX_STATE_FILE = path.join(os.homedir(), '.codex', 'state_5.sqlite');
19
19
  const AGENT_XP_LABEL = 'AgentXP';
20
+ const AGENTXP_LEADERBOARD_URL = 'https://api.atris.ai/api/agentxp/leaderboard';
20
21
  const LEVEL_XP = 1000;
21
22
  const RECEIPT_CHAIN_VERSION = 'atris.career_xp_receipt_chain.v1';
22
23
  const XP_STATE_FILES = new Set([
@@ -37,14 +38,16 @@ const SEARCH_EXCLUDED_DIRS = new Set([
37
38
  const DEFAULT_SEARCH_DEPTH = 6;
38
39
 
39
40
  function showHelp() {
40
- console.log('Usage: atris xp [card|status|collect|session] [--json] [--workspace <path>] [--all] [--root <path>]');
41
+ console.log('Usage: atris xp [card|status|collect|session|sync] [--json] [--workspace <path>] [--all] [--root <path>]');
41
42
  console.log(' atris xp session [--since today|tonight|YYYY-MM-DD] [--until <time>] [--mission <text>] [--thread <id>] [--no-write]');
43
+ console.log(' atris xp sync [--as <player>] [--local|--all] [--token <token>|logged-in] [--dry-run] [--json]');
42
44
  console.log(' atris xp [--json] [--local] [--workspace <path>] [--operator <name>]');
43
45
  console.log('');
44
46
  console.log('Show your AgentXP graph for the active Atris account.');
45
47
  console.log('Use status to show account-level AgentXP across verified local ledgers.');
46
48
  console.log('Use collect or status --local to project accepted task proof in the current workspace.');
47
49
  console.log('Use session to encapsulate the current work window into a local XP capsule.');
50
+ console.log('Use sync to upload a path-private AgentXP packet to the hosted leaderboard.');
48
51
  console.log('Use status --all to explicitly aggregate verified local XP ledgers across workspaces.');
49
52
  console.log('Use --local to render from proof receipts in the current workspace.');
50
53
  }
@@ -187,6 +190,14 @@ function readFlag(args, name, fallback = null) {
187
190
  return fallback;
188
191
  }
189
192
 
193
+ function readFirstFlag(args, names, fallback = null) {
194
+ for (const name of names) {
195
+ const value = readFlag(args, name, null);
196
+ if (value !== null && value !== undefined && value !== '') return value;
197
+ }
198
+ return fallback;
199
+ }
200
+
190
201
  function readFlagValues(args, names) {
191
202
  const wanted = Array.isArray(names) ? names : [names];
192
203
  const values = [];
@@ -312,6 +323,15 @@ function hashPayload(value) {
312
323
  return sha256(canonicalJson(value));
313
324
  }
314
325
 
326
+ function slugify(value) {
327
+ return String(value || '')
328
+ .trim()
329
+ .toLowerCase()
330
+ .replace(/@.*$/, '')
331
+ .replace(/[^a-z0-9]+/g, '-')
332
+ .replace(/^-+|-+$/g, '');
333
+ }
334
+
315
335
  function readJsonFile(filePath, fallback = null) {
316
336
  if (!fs.existsSync(filePath)) return fallback;
317
337
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
@@ -1418,7 +1438,191 @@ function loadLocalPayload(args) {
1418
1438
  return normalizeLocalScore(JSON.parse(result.stdout), workspace);
1419
1439
  }
1420
1440
 
1441
+ function publicAgentXp(value) {
1442
+ return Math.max(0, Math.min(99, asNumber(value)));
1443
+ }
1444
+
1445
+ function verifiedProjection(projection) {
1446
+ return projection?.integrity_status === 'verified'
1447
+ && projection?.integrity?.status === 'verified';
1448
+ }
1449
+
1450
+ function projectionWorkspaceSummaries(projection) {
1451
+ if (Array.isArray(projection?.workspaces)) {
1452
+ return projection.workspaces.map(workspace => ({
1453
+ name: workspace.name || workspaceName(workspace.workspace_root || 'workspace'),
1454
+ workspace_root_hash: workspace.workspace_root ? sha256(path.resolve(workspace.workspace_root)) : null,
1455
+ included: Boolean(workspace.included),
1456
+ agent_xp: asNumber(workspace.total_xp),
1457
+ receipts_count: asNumber(workspace.receipts_count),
1458
+ integrity_status: workspace.integrity_status || 'unknown',
1459
+ }));
1460
+ }
1461
+
1462
+ const workspaceRoot = projection?.workspace_root || path.resolve(process.cwd());
1463
+ return [{
1464
+ name: workspaceName(workspaceRoot),
1465
+ workspace_root_hash: sha256(path.resolve(workspaceRoot)),
1466
+ included: verifiedProjection(projection),
1467
+ agent_xp: asNumber(projection?.total_agent_xp ?? projection?.total_xp),
1468
+ receipts_count: asNumber(projection?.receipts_count),
1469
+ integrity_status: projection?.integrity_status || projection?.integrity?.status || 'unknown',
1470
+ }];
1471
+ }
1472
+
1473
+ function syncPlayer(args, projection) {
1474
+ const explicit = readFirstFlag(args, ['--as', '--player', '--user', '--operator'], null);
1475
+ return slugify(
1476
+ explicit
1477
+ || process.env.ATRIS_PLAYER
1478
+ || process.env.ATRIS_USERNAME
1479
+ || process.env.ATRIS_PROFILE
1480
+ || process.env.USER
1481
+ || os.userInfo().username
1482
+ || projection?.operator
1483
+ || 'player'
1484
+ ) || 'player';
1485
+ }
1486
+
1487
+ function buildAgentXpSyncPacket(args = []) {
1488
+ const localMode = hasFlag(args, '--local') || hasFlag(args, '--workspace') || hasFlag(args, '--operator');
1489
+ const projectionArgs = args.filter(arg => !['--dry-run', '--no-post', '--packet'].includes(arg));
1490
+ const projection = hasFlag(args, '--all') || !localMode
1491
+ ? collectAllLocalXpProjection(projectionArgs)
1492
+ : collectLocalXpProjection(projectionArgs);
1493
+ const player = syncPlayer(args, projection);
1494
+ const workspaces = projectionWorkspaceSummaries(projection);
1495
+ const totalXp = asNumber(projection.total_agent_xp ?? projection.agent_xp ?? projection.total_xp ?? projection.career_xp);
1496
+ const receiptsCount = asNumber(projection.receipts_count);
1497
+ const eligible = verifiedProjection(projection) && receiptsCount > 0 && totalXp > 0;
1498
+ const publicXp = publicAgentXp(totalXp);
1499
+ const entry = {
1500
+ user_id: player,
1501
+ username: player,
1502
+ agent_xp: publicXp,
1503
+ career_xp: publicXp,
1504
+ current_form: publicXp,
1505
+ ovr: publicXp,
1506
+ level: Math.max(1, asNumber(projection.level, 1)),
1507
+ verified_receipts: receiptsCount,
1508
+ reviewed_tasks: receiptsCount,
1509
+ recent_verified_receipts: receiptsCount,
1510
+ recent_reviewed_tasks: receiptsCount,
1511
+ leaderboard_eligible: eligible,
1512
+ integrity_status: eligible ? 'trusted' : (projection.integrity_status || projection.integrity?.status || 'unknown'),
1513
+ lock_reason: eligible ? null : 'not_enough_trusted_proof',
1514
+ public_adjustment: null,
1515
+ next_move: eligible ? 'Play the next proof-backed AgentXP mission.' : 'Complete one proof-backed AgentXP rep.',
1516
+ };
1517
+ const packet = {
1518
+ schema: 'atris.agentxp_sync_packet.v1',
1519
+ generated_at: new Date().toISOString(),
1520
+ workspace_root_hash: sha256(workspaces.map(item => item.workspace_root_hash || item.name).sort().join(':')),
1521
+ computer: projection.workspace_name || workspaces[0]?.name || 'local',
1522
+ operator: player,
1523
+ privacy: {
1524
+ raw_proofs_included: false,
1525
+ raw_receipts_included: false,
1526
+ contains_absolute_workspace_root: false,
1527
+ public_user_board_omits_lifetime_xp: true,
1528
+ },
1529
+ sync_contract: {
1530
+ anti_bs_rule: 'No accepted proof-backed task episodes means no AgentXP leaderboard movement.',
1531
+ trust_rule: 'Only verified local ledgers are uploaded; raw proof and paths stay local.',
1532
+ },
1533
+ local_evidence: {
1534
+ workspaces,
1535
+ verified_workspace_count: asNumber(projection.verified_workspace_count, verifiedProjection(projection) ? 1 : 0),
1536
+ receipts_count: receiptsCount,
1537
+ integrity_status: projection.integrity?.status || projection.integrity_status || 'unknown',
1538
+ ledger_head_hash: projection.integrity?.head_hash || null,
1539
+ },
1540
+ user_leaderboard: {
1541
+ schema: 'atris.agentxp_user_leaderboard.v1',
1542
+ score_name: AGENT_XP_LABEL,
1543
+ entries: [entry],
1544
+ },
1545
+ };
1546
+ packet.packet_hash = hashPayload(packet);
1547
+ return {
1548
+ schema: 'atris.agentxp_sync_preview.v1',
1549
+ generated_at: new Date().toISOString(),
1550
+ dry_run: true,
1551
+ player,
1552
+ entry,
1553
+ packet,
1554
+ };
1555
+ }
1556
+
1557
+ async function syncAgentXp(args = []) {
1558
+ const preview = buildAgentXpSyncPacket(args);
1559
+ const dryRun = hasFlag(args, '--dry-run') || hasFlag(args, '--no-post') || hasFlag(args, '--packet');
1560
+ if (dryRun) return preview;
1561
+
1562
+ const token = readFlag(args, '--token', process.env.ATRIS_AGENTXP_SYNC_TOKEN || process.env.AGENTXP_SYNC_TOKEN || '');
1563
+ const options = {
1564
+ method: 'POST',
1565
+ body: preview.packet,
1566
+ retries: 0,
1567
+ };
1568
+ if (token) {
1569
+ options.headers = { 'X-AgentXP-Sync-Token': token };
1570
+ } else {
1571
+ const ensured = await ensureValidCredentials(apiRequestJson);
1572
+ if (ensured.error) {
1573
+ throw new Error(`Missing sync auth. Run atris login, or set ATRIS_AGENTXP_SYNC_TOKEN${ensured.detail ? ` (${ensured.detail})` : ''}.`);
1574
+ }
1575
+ options.token = ensured.credentials.token;
1576
+ }
1577
+
1578
+ const response = await apiRequestJson('/agentxp/leaderboard/sync', options);
1579
+ if (!response.ok) {
1580
+ throw new Error(`AgentXP sync failed: ${response.error || response.status}`);
1581
+ }
1582
+ return {
1583
+ schema: 'atris.agentxp_sync_result.v1',
1584
+ generated_at: new Date().toISOString(),
1585
+ dry_run: false,
1586
+ player: preview.player,
1587
+ entry: preview.entry,
1588
+ packet_hash: preview.packet.packet_hash,
1589
+ server: response.data || {},
1590
+ };
1591
+ }
1592
+
1593
+ function renderSync(payload) {
1594
+ const entry = payload.entry || {};
1595
+ console.log('AgentXP Sync');
1596
+ console.log(`Player ${payload.player || entry.username || 'player'} | AgentXP ${formatNumber(entry.agent_xp)} | receipts ${formatNumber(entry.verified_receipts)}`);
1597
+ if (payload.dry_run) {
1598
+ console.log(`Packet ${payload.packet?.packet_hash || 'unhashed'} ready; no network upload ran.`);
1599
+ console.log('Run with ATRIS_AGENTXP_SYNC_TOKEN set to publish to the hosted leaderboard.');
1600
+ console.log(`Leaderboard: ${AGENTXP_LEADERBOARD_URL}`);
1601
+ return;
1602
+ }
1603
+ const server = payload.server || {};
1604
+ console.log(`Uploaded: accepted ${formatNumber(server.accepted_count)} | stored ${formatNumber(server.stored_count)}`);
1605
+ const acceptedUsernames = Array.isArray(server.accepted_usernames)
1606
+ ? server.accepted_usernames.map(value => String(value || '').trim()).filter(Boolean)
1607
+ : [];
1608
+ if (acceptedUsernames.length) {
1609
+ console.log(`Public identity: ${acceptedUsernames.join(', ')}`);
1610
+ }
1611
+ const player = String(payload.player || entry.username || '').trim().toLowerCase();
1612
+ const accepted = acceptedUsernames.map(value => value.toLowerCase());
1613
+ if (server.mapped_to_authenticated_user === true && player && !accepted.includes(player)) {
1614
+ console.log('Login auth mapped this sync to your Atris account.');
1615
+ }
1616
+ console.log(`Packet ${payload.packet_hash}`);
1617
+ console.log(`Leaderboard: ${AGENTXP_LEADERBOARD_URL}`);
1618
+ }
1619
+
1421
1620
  function render(payload) {
1621
+ if (payload.schema === 'atris.agentxp_sync_preview.v1' || payload.schema === 'atris.agentxp_sync_result.v1') {
1622
+ renderSync(payload);
1623
+ return;
1624
+ }
1625
+
1422
1626
  if (payload.schema === 'atris.career_xp_session_capsule.v1') {
1423
1627
  const xp = payload.xp || {};
1424
1628
  const tasks = payload.tasks || {};
@@ -1508,6 +1712,23 @@ async function xpCommand(...args) {
1508
1712
  }
1509
1713
 
1510
1714
  const subcommand = args[0] && !args[0].startsWith('--') ? args[0] : null;
1715
+ if (subcommand === 'sync') {
1716
+ const commandArgs = args.slice(1);
1717
+ let payload;
1718
+ try {
1719
+ payload = await syncAgentXp(commandArgs);
1720
+ } catch (error) {
1721
+ console.error(`Failed to sync AgentXP: ${error.message}`);
1722
+ process.exit(1);
1723
+ }
1724
+ if (args.includes('--json')) {
1725
+ console.log(JSON.stringify(payload, null, 2));
1726
+ return;
1727
+ }
1728
+ render(payload);
1729
+ return;
1730
+ }
1731
+
1511
1732
  if (subcommand === 'session') {
1512
1733
  const commandArgs = args.slice(1);
1513
1734
  let payload;
@@ -1603,6 +1824,8 @@ module.exports = {
1603
1824
  buildCareerXpSessionCapsule,
1604
1825
  collectAllLocalXpProjection,
1605
1826
  collectLocalXpProjection,
1827
+ buildAgentXpSyncPacket,
1828
+ syncAgentXp,
1606
1829
  receiptFromTaskEpisode,
1607
1830
  render,
1608
1831
  };