atris 3.15.23 → 3.15.31

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.
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * atris computer — Open SMART mode (cloud in business workspace, local elsewhere)
5
5
  * atris computer --cloud — Open CLOUD workspace mode
6
+ * atris computer create <name> — Create and wake a business computer
6
7
  * atris computer wake — Start the computer
7
8
  * atris computer sleep — Stop (files persist)
8
9
  * atris computer card — Show the local computer card
@@ -19,7 +20,7 @@ const path = require('path');
19
20
  const readline = require('readline');
20
21
  const { spawnSync } = require('child_process');
21
22
  const { loadCredentials, decodeJwtClaims } = require('../utils/auth');
22
- const { apiRequestJson, getApiBaseUrl } = require('../utils/api');
23
+ const { apiRequestJson, getApiBaseUrl, getAppBaseUrl } = require('../utils/api');
23
24
  const { loadBusinesses, saveBusinesses } = require('./business');
24
25
  const { consoleCommand, gatherAtrisContext, buildSystemPrompt } = require('./console');
25
26
  const { streamSession } = require('./serve');
@@ -544,6 +545,36 @@ function parseComputerOptions(argv) {
544
545
  };
545
546
  }
546
547
 
548
+ function parseComputerCreateArgs(argv = []) {
549
+ const nameParts = [];
550
+ let businessSlug = null;
551
+ let help = false;
552
+
553
+ for (let i = 0; i < argv.length; i++) {
554
+ const arg = argv[i];
555
+ if (arg === '--help' || arg === '-h' || arg === 'help') {
556
+ help = true;
557
+ continue;
558
+ }
559
+ if ((arg === '--business' || arg === '-b') && argv[i + 1]) {
560
+ businessSlug = argv[i + 1];
561
+ i++;
562
+ continue;
563
+ }
564
+ if (arg.startsWith('--business=')) {
565
+ businessSlug = arg.split('=', 2)[1] || null;
566
+ continue;
567
+ }
568
+ nameParts.push(arg);
569
+ }
570
+
571
+ return {
572
+ name: nameParts.join(' ').trim(),
573
+ businessSlug: businessSlug ? String(businessSlug).trim() : null,
574
+ help,
575
+ };
576
+ }
577
+
547
578
  function formatCloudSelection(options = {}) {
548
579
  const worker = activeWorker(options.worker);
549
580
  const parts = [`worker=${worker}`];
@@ -953,6 +984,55 @@ async function resolveBusinessContextBySlug(token, slug) {
953
984
  return null;
954
985
  }
955
986
 
987
+ async function resolveBusinessOwnerForCreate(token, businessSlug = null) {
988
+ const wantedSlug = businessSlug ? String(businessSlug).trim() : null;
989
+ if (wantedSlug) {
990
+ const fromApi = await resolveBusinessContextBySlug(token, wantedSlug);
991
+ if (fromApi) return fromApi;
992
+
993
+ const cached = loadBusinesses()[wantedSlug];
994
+ if (cached?.business_id) {
995
+ return {
996
+ slug: cached.slug || wantedSlug,
997
+ businessId: cached.business_id,
998
+ workspaceId: cached.workspace_id || null,
999
+ businessName: cached.name || cached.slug || wantedSlug,
1000
+ };
1001
+ }
1002
+ return null;
1003
+ }
1004
+
1005
+ const binding = readBusinessBinding();
1006
+ if (binding?.business_id) {
1007
+ return {
1008
+ slug: binding.slug || binding.canonical_slug || null,
1009
+ businessId: binding.business_id,
1010
+ workspaceId: binding.workspace_id || null,
1011
+ businessName: binding.name || binding.slug || 'business',
1012
+ };
1013
+ }
1014
+
1015
+ return resolveBusinessContext(token);
1016
+ }
1017
+
1018
+ function rememberCreatedComputer(ctx, workspace, endpoint = null) {
1019
+ const slug = ctx.slug || (ctx.businessName || '').toLowerCase().replace(/\s+/g, '-');
1020
+ if (!slug) return;
1021
+ const businesses = loadBusinesses();
1022
+ businesses[slug] = {
1023
+ ...(businesses[slug] || {}),
1024
+ business_id: ctx.businessId,
1025
+ workspace_id: workspace.id,
1026
+ name: ctx.businessName,
1027
+ slug,
1028
+ computer_name: workspace.name,
1029
+ endpoint: endpoint || undefined,
1030
+ added_at: businesses[slug]?.added_at || new Date().toISOString(),
1031
+ updated_at: new Date().toISOString(),
1032
+ };
1033
+ saveBusinesses(businesses);
1034
+ }
1035
+
956
1036
  function shellQuote(value) {
957
1037
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
958
1038
  }
@@ -1224,6 +1304,90 @@ async function computerWake(token, ctx = null) {
1224
1304
  console.log(` Endpoint: ${result.data.endpoint}`);
1225
1305
  }
1226
1306
 
1307
+ async function computerCreate(token, args = []) {
1308
+ const options = parseComputerCreateArgs(args);
1309
+ if (options.help || !options.name) {
1310
+ console.log('Usage: atris computer create <name> --business <slug>');
1311
+ console.log('');
1312
+ console.log('Create a business computer, activate it, and wake it in one command.');
1313
+ console.log('');
1314
+ console.log('Examples:');
1315
+ console.log(' atris computer create "My Business Computer" --business atris-labs');
1316
+ console.log(' atris computer create "Recruiting Computer"');
1317
+ if (!options.name && !options.help) process.exitCode = 1;
1318
+ return;
1319
+ }
1320
+
1321
+ const ctx = await resolveBusinessOwnerForCreate(token, options.businessSlug);
1322
+ if (!ctx?.businessId) {
1323
+ console.error('No business found.');
1324
+ console.error('Run inside a bound business workspace or pass: --business <slug>');
1325
+ process.exitCode = 1;
1326
+ return;
1327
+ }
1328
+
1329
+ console.log(`Creating computer "${options.name}" for ${ctx.businessName}...`);
1330
+ const created = await apiRequestJson(`/business/${ctx.businessId}/workspaces`, {
1331
+ method: 'POST',
1332
+ token,
1333
+ body: { name: options.name, type: 'general' },
1334
+ });
1335
+ if (!created.ok) {
1336
+ console.error(`Failed to create workspace: ${created.errorMessage || created.error || created.status}`);
1337
+ process.exitCode = 1;
1338
+ return;
1339
+ }
1340
+
1341
+ const workspace = created.data || {};
1342
+ const workspaceId = workspace.id || workspace.workspace_id;
1343
+ if (!workspaceId) {
1344
+ console.error('Failed to create workspace: response did not include workspace id');
1345
+ process.exitCode = 1;
1346
+ return;
1347
+ }
1348
+
1349
+ const activate = await apiRequestJson(`/business/${ctx.businessId}/workspaces/${workspaceId}/activate`, {
1350
+ method: 'POST',
1351
+ token,
1352
+ body: {},
1353
+ });
1354
+ if (!activate.ok && activate.status !== 409) {
1355
+ console.error(`Failed to activate computer: ${activate.errorMessage || activate.error || activate.status}`);
1356
+ process.exitCode = 1;
1357
+ return;
1358
+ }
1359
+
1360
+ const wake = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/wake`, {
1361
+ method: 'POST',
1362
+ token,
1363
+ body: {},
1364
+ });
1365
+ if (!wake.ok && !activate.ok) {
1366
+ console.error(`Failed to wake computer: ${wake.errorMessage || wake.error || wake.status}`);
1367
+ process.exitCode = 1;
1368
+ return;
1369
+ }
1370
+
1371
+ const endpoint = activate.data?.endpoint || wake.data?.endpoint || null;
1372
+ const status = endpoint
1373
+ ? 'running'
1374
+ : (wake.data?.status || (activate.ok ? 'activated' : 'warming_up'));
1375
+ rememberCreatedComputer(ctx, { ...workspace, id: workspaceId, name: workspace.name || options.name }, endpoint);
1376
+
1377
+ const appBase = getAppBaseUrl();
1378
+ console.log('');
1379
+ console.log(`Computer created: ${workspaceId}`);
1380
+ console.log(` Name: ${workspace.name || options.name}`);
1381
+ console.log(` Business: ${ctx.businessName}`);
1382
+ console.log(` Status: ${status}`);
1383
+ if (endpoint) console.log(` Endpoint: ${endpoint}`);
1384
+ console.log(` Dashboard: ${appBase}/dashboard/gm/${ctx.businessId}`);
1385
+ console.log('');
1386
+ console.log('Next:');
1387
+ console.log(` atris pull ${ctx.slug || ctx.businessId}`);
1388
+ console.log(' atris computer');
1389
+ }
1390
+
1227
1391
  async function computerSleep(token, ctx = null) {
1228
1392
  if (ctx) {
1229
1393
  console.log(`Sleeping computer for ${ctx.businessName}...`);
@@ -2562,6 +2726,14 @@ async function runComputer() {
2562
2726
  return;
2563
2727
  }
2564
2728
 
2729
+ if (sub === 'create') {
2730
+ const createOptions = parseComputerCreateArgs(args.slice(1));
2731
+ if (createOptions.help || !createOptions.name) {
2732
+ await computerCreate(null, args.slice(1));
2733
+ return;
2734
+ }
2735
+ }
2736
+
2565
2737
  if (sub === '--help') {
2566
2738
  console.log('Usage: atris computer [mode|command]');
2567
2739
  console.log('');
@@ -2593,6 +2765,7 @@ async function runComputer() {
2593
2765
  console.log(' claude|codex Legacy local console backends');
2594
2766
  console.log('');
2595
2767
  console.log('Cloud commands:');
2768
+ console.log(' create <name> Create and wake a business computer');
2596
2769
  console.log(' chat Interactive cloud workspace chat');
2597
2770
  console.log(' Ctrl-C during a cloud run interrupts it');
2598
2771
  console.log(' /start shows the beginner flow');
@@ -2614,6 +2787,7 @@ async function runComputer() {
2614
2787
  console.log('Examples:');
2615
2788
  console.log(' atris computer');
2616
2789
  console.log(' atris computer card --write');
2790
+ console.log(' atris computer create "My Business Computer" --business atris-labs');
2617
2791
  console.log(' atris business init "My Lab" # shared owner + first/default computer');
2618
2792
  console.log(' atris computer proof');
2619
2793
  console.log(' atris computer local');
@@ -2695,6 +2869,7 @@ async function runComputer() {
2695
2869
  switch (sub) {
2696
2870
  case 'chat': return computerChat(token, ctx, cloudOptions);
2697
2871
  case 'card': return computerCard(args.slice(1));
2872
+ case 'create': return computerCreate(token, args.slice(1));
2698
2873
  case 'proof': return computerProof(token, ctx, cloudOptions);
2699
2874
  case 'status': return computerStatus(token, ctx);
2700
2875
  case 'wake': return computerWake(token, ctx);
package/commands/gm.js CHANGED
@@ -4,6 +4,9 @@ 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
+ const AGENTXP_GLOBAL_SYNC_RULE = 'Use the owner-provided sync token first; fallback is atris login before sync.';
9
+
7
10
  const ROLE_PLAYERS_TO_IGNORE = new Set([
8
11
  'game-manager',
9
12
  'navigator',
@@ -26,7 +29,7 @@ function showHelp() {
26
29
  console.log('');
27
30
  console.log('Options:');
28
31
  console.log(' --manager <id> Manager id. Defaults to game-manager when present.');
29
- console.log(' --as <id> Alias for --manager.');
32
+ console.log(' --as <id> Alias for --player.');
30
33
  console.log(' --player <id> Preferred player when seeding a first local mission.');
31
34
  console.log(' --workspace <p> Read missions from another Atris workspace.');
32
35
  console.log(' --no-seed Do not create a starter player mission.');
@@ -102,7 +105,7 @@ function teamMembers(workspaceRoot) {
102
105
  }
103
106
 
104
107
  function inferManager(workspaceRoot, args = []) {
105
- const explicit = flag(args, '--manager') || flag(args, '--as') || positional(args)[0];
108
+ const explicit = flag(args, '--manager') || positional(args)[0];
106
109
  if (explicit) return { manager: slugify(explicit), source: 'flag' };
107
110
 
108
111
  for (const value of [process.env.ATRIS_GM, process.env.ATRIS_MANAGER, process.env.ATRIS_AGENT_ID]) {
@@ -160,7 +163,7 @@ function starterMissionPrompt(player) {
160
163
  }
161
164
 
162
165
  function pickSeedPlayer(workspaceRoot, tasks, args = []) {
163
- const explicit = flag(args, '--player') || flag(args, '--user');
166
+ const explicit = flag(args, '--player') || flag(args, '--user') || flag(args, '--as');
164
167
  if (explicit) return slugify(explicit);
165
168
 
166
169
  const fromTasks = playersFromTasks(tasks);
@@ -261,19 +264,30 @@ function compactTask(task) {
261
264
  };
262
265
  }
263
266
 
264
- function nextCommands({ seeded, reviewQueue, missions, players }) {
267
+ function globalSyncCommands(player) {
268
+ return [
269
+ `atris xp sync --local --as ${player} --token <owner-provided-token>`,
270
+ 'atris login',
271
+ `atris xp sync --local --as ${player}`,
272
+ ];
273
+ }
274
+
275
+ function nextCommands({ seeded, reviewQueue, missions, players, manager }) {
265
276
  if (reviewQueue.length) {
266
- const ref = reviewQueue[0].ref;
277
+ const task = reviewQueue[0];
278
+ const ref = task.ref;
279
+ const player = task.assigned_to || task.claimed_by || players[0]?.player || 'player';
267
280
  return [
268
281
  `atris task show ${ref}`,
269
- `atris task accept ${ref} --proof "<human review>"`,
270
- `atris task revise ${ref} --note "<what must change>"`,
282
+ `atris task accept ${ref} --as ${player} --proof "<human review>"`,
283
+ `atris task revise ${ref} --as ${player} --note "<what must change>"`,
284
+ ...globalSyncCommands(player),
271
285
  ];
272
286
  }
273
287
  if (missions.length) {
274
288
  const mission = missions[0];
275
289
  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}`];
290
+ if (mission.status === 'open') return [`atris task claim ${mission.ref} --as ${manager || 'game-manager'}`];
277
291
  return [`atris play --as ${player}`];
278
292
  }
279
293
  if (seeded) return [`atris play --as ${seeded.assigned_to || 'player'}`];
@@ -297,7 +311,7 @@ function gmState(args = []) {
297
311
  const reviewQueue = missions.filter(task => task.status === 'review');
298
312
  const players = groupPlayers(tasks, workspaceRoot);
299
313
  const seeded = compactTask(starter.seeded);
300
- const commands = nextCommands({ seeded, reviewQueue, missions, players });
314
+ const commands = nextCommands({ seeded, reviewQueue, missions, players, manager: detected.manager });
301
315
 
302
316
  return {
303
317
  schema: 'atris.agentxp_gm_mode.v1',
@@ -318,6 +332,8 @@ function gmState(args = []) {
318
332
  review_queue: reviewQueue,
319
333
  next_commands: commands,
320
334
  xp_rule: 'GM can route missions and review proof, but AgentXP still lands only after human accept.',
335
+ global_sync_rule: AGENTXP_GLOBAL_SYNC_RULE,
336
+ leaderboard_url: AGENTXP_LEADERBOARD_URL,
321
337
  };
322
338
  }
323
339
 
@@ -351,6 +367,8 @@ function render(state) {
351
367
 
352
368
  console.log('');
353
369
  console.log('XP rule: no proof, no AgentXP; accept/revise stays human-gated.');
370
+ console.log('Global sync: use owner token first; fallback to atris login before hosted leaderboard sync.');
371
+ console.log(`Leaderboard: ${state.leaderboard_url}`);
354
372
  console.log('');
355
373
  console.log('Next commands:');
356
374
  for (const command of state.next_commands) console.log(`- ${command}`);
package/commands/play.js CHANGED
@@ -6,6 +6,9 @@ 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
+ const AGENTXP_GLOBAL_SYNC_RULE = 'Use the owner-provided sync token first; fallback is atris login before sync.';
11
+
9
12
  function showHelp() {
10
13
  console.log('');
11
14
  console.log('Usage: atris play [--as <player>] [--workspace <path>] [--json]');
@@ -252,10 +255,18 @@ function starterMissionPrompt(player) {
252
255
  ].join(' ');
253
256
  }
254
257
 
258
+ function globalSyncCommands(player) {
259
+ return [
260
+ `atris xp sync --local --as ${player} --token <owner-provided-token>`,
261
+ 'atris login',
262
+ `atris xp sync --local --as ${player}`,
263
+ ];
264
+ }
265
+
255
266
  function ensureStarterMission(taskDb, db, workspaceRoot, player, tasks, args = []) {
256
267
  if (hasFlag(args, '--no-seed')) return { tasks, seeded: null };
257
268
  if (selectMission(tasks, player)) return { tasks, seeded: null };
258
- if (!fs.existsSync(path.join(workspaceRoot, 'atris'))) return { tasks, seeded: null };
269
+ fs.mkdirSync(path.join(workspaceRoot, 'atris'), { recursive: true });
259
270
 
260
271
  const result = taskDb.addTask(db, {
261
272
  title: starterMissionTitle(),
@@ -282,7 +293,21 @@ function ensureStarterMission(taskDb, db, workspaceRoot, player, tasks, args = [
282
293
  return { tasks: refreshed, seeded };
283
294
  }
284
295
 
296
+ function playWorkspaceRoot(taskDb, workspaceArg) {
297
+ let requested = path.resolve(workspaceArg || process.cwd());
298
+ try { requested = fs.realpathSync(requested); } catch {}
299
+ if (
300
+ fs.existsSync(path.join(requested, '.git'))
301
+ || fs.existsSync(path.join(requested, 'atris'))
302
+ || fs.existsSync(path.join(requested, '.atris'))
303
+ ) {
304
+ return taskDb.workspaceRoot(requested);
305
+ }
306
+ return requested;
307
+ }
308
+
285
309
  function nextCommands(task, player) {
310
+ const helper = 'game-manager';
286
311
  if (!task) {
287
312
  return [
288
313
  `atris task delegate "AgentXP first rep: one proof-backed mission" --to ${player} --tag agent-xp`,
@@ -293,40 +318,45 @@ function nextCommands(task, player) {
293
318
  const ref = taskRef(task);
294
319
  if (task.status === 'open') {
295
320
  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>"`,
321
+ `atris task claim ${ref} --as ${helper}`,
322
+ `atris task ready ${ref} --as ${helper} --proof "<artifact path + verifier result>"`,
323
+ `atris task accept ${ref} --as ${player} --proof "<human review>"`,
299
324
  'atris xp card --local',
325
+ ...globalSyncCommands(player),
300
326
  ];
301
327
  }
302
328
 
303
329
  if (task.status === 'claimed') {
330
+ const actor = task.claimed_by || helper;
304
331
  return [
305
- `atris task ready ${ref} --proof "<artifact path + verifier result>"`,
306
- `atris task accept ${ref} --proof "<human review>"`,
332
+ `atris task ready ${ref} --as ${actor} --proof "<artifact path + verifier result>"`,
333
+ `atris task accept ${ref} --as ${player} --proof "<human review>"`,
307
334
  'atris xp card --local',
335
+ ...globalSyncCommands(player),
308
336
  ];
309
337
  }
310
338
 
311
339
  if (task.status === 'review') {
312
340
  return [
313
341
  `atris task show ${ref}`,
314
- `atris task accept ${ref} --proof "<human review>"`,
315
- `atris task revise ${ref} --note "<what must change>"`,
342
+ `atris task accept ${ref} --as ${player} --proof "<human review>"`,
343
+ `atris task revise ${ref} --as ${player} --note "<what must change>"`,
316
344
  'atris xp card --local',
345
+ ...globalSyncCommands(player),
317
346
  ];
318
347
  }
319
348
 
320
349
  return [
321
350
  `atris task show ${ref}`,
322
351
  'atris xp card --local',
352
+ ...globalSyncCommands(player),
323
353
  ];
324
354
  }
325
355
 
326
356
  function modeState(args = []) {
327
357
  const taskDb = require('../lib/task-db');
328
358
  const workspaceArg = flag(args, '--workspace') || flag(args, '--root') || process.cwd();
329
- const workspaceRoot = taskDb.workspaceRoot(path.resolve(workspaceArg));
359
+ const workspaceRoot = playWorkspaceRoot(taskDb, workspaceArg);
330
360
  const db = taskDb.open();
331
361
  const rows = taskDb.listTasks(db, {
332
362
  workspaceRoot,
@@ -367,6 +397,8 @@ function modeState(args = []) {
367
397
  prompt: latestMessage(events),
368
398
  } : null,
369
399
  xp_rule: 'AgentXP lands only after proof is ready and a human accepts the task.',
400
+ global_sync_rule: AGENTXP_GLOBAL_SYNC_RULE,
401
+ leaderboard_url: AGENTXP_LEADERBOARD_URL,
370
402
  next_commands: commandList,
371
403
  };
372
404
  }
@@ -399,6 +431,8 @@ function render(state) {
399
431
  console.log('');
400
432
  console.log('Win condition: real artifact + verifier + human accept.');
401
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.');
435
+ console.log(`Leaderboard: ${state.leaderboard_url}`);
402
436
  console.log('');
403
437
  console.log('Next commands:');
404
438
  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
  }