atris 3.37.0 → 3.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +2 -0
  2. package/atris/PERSONA.md +3 -7
  3. package/atris.md +10 -2
  4. package/ax +158 -33
  5. package/bin/atris.js +49 -2
  6. package/commands/activate.js +16 -3
  7. package/commands/agent-spawn.js +0 -7
  8. package/commands/autoland.js +92 -14
  9. package/commands/autopilot-front.js +0 -4
  10. package/commands/autopilot.js +0 -12
  11. package/commands/brain.js +0 -7
  12. package/commands/brief.js +0 -2
  13. package/commands/business-sync.js +0 -1
  14. package/commands/business.js +107 -12
  15. package/commands/clean.js +31 -2
  16. package/commands/close.js +46 -11
  17. package/commands/cloud.js +0 -1
  18. package/commands/codex-goal.js +0 -2
  19. package/commands/computer.js +6 -5
  20. package/commands/console.js +46 -9
  21. package/commands/deck.js +29 -8
  22. package/commands/dream.js +0 -7
  23. package/commands/drill.js +0 -1
  24. package/commands/engine.js +10 -18
  25. package/commands/experiments.js +0 -4
  26. package/commands/fleet-report.js +12 -4
  27. package/commands/game.js +261 -0
  28. package/commands/gm.js +10 -2
  29. package/commands/goal.js +0 -6
  30. package/commands/harvest.js +0 -1
  31. package/commands/improve.js +29 -8
  32. package/commands/init.js +18 -34
  33. package/commands/integrations.js +0 -7
  34. package/commands/land.js +71 -8
  35. package/commands/launchpad.js +0 -2
  36. package/commands/lesson.js +17 -7
  37. package/commands/loop-front.js +0 -2
  38. package/commands/loops.js +6 -5
  39. package/commands/meet.js +0 -2
  40. package/commands/member.js +68 -3
  41. package/commands/mission.js +359 -102
  42. package/commands/next.js +0 -1
  43. package/commands/now.js +0 -12
  44. package/commands/one-lap.js +8 -8
  45. package/commands/orb.js +0 -4
  46. package/commands/pack-craft.js +0 -1
  47. package/commands/pack.js +670 -77
  48. package/commands/play.js +0 -2
  49. package/commands/probe.js +10 -4
  50. package/commands/pulse.js +73 -48
  51. package/commands/push.js +0 -4
  52. package/commands/radar.js +14 -11
  53. package/commands/recover.js +340 -0
  54. package/commands/report.js +0 -1
  55. package/commands/router.js +319 -0
  56. package/commands/run-front.js +0 -2
  57. package/commands/search.js +0 -3
  58. package/commands/skill.js +7 -2
  59. package/commands/slop.js +225 -7
  60. package/commands/status.js +6 -0
  61. package/commands/stream.js +0 -2
  62. package/commands/study.js +0 -3
  63. package/commands/sync-checkout.js +51 -0
  64. package/commands/sync.js +11 -40
  65. package/commands/task.js +125 -39
  66. package/commands/teach.js +86 -0
  67. package/commands/unknowns.js +0 -10
  68. package/commands/update.js +0 -1
  69. package/commands/visualize.js +0 -5
  70. package/commands/watch.js +0 -6
  71. package/commands/wiki.js +41 -11
  72. package/commands/wish.js +0 -1
  73. package/commands/worktree.js +87 -14
  74. package/commands/xp.js +1 -6
  75. package/commands/youtube.js +0 -2
  76. package/lib/accept-verify-gate.js +122 -0
  77. package/lib/activity-stream.js +0 -2
  78. package/lib/auto-accept-certified.js +145 -2
  79. package/lib/autoland.js +0 -3
  80. package/lib/ax-auto-lane.js +94 -10
  81. package/lib/bench/context.js +0 -2
  82. package/lib/bench/report.js +0 -1
  83. package/lib/bench/runner.js +0 -11
  84. package/lib/brief-ledger.js +0 -6
  85. package/lib/business-simulate.js +382 -0
  86. package/lib/chat-log-scan.js +0 -5
  87. package/lib/checkout-sync.js +52 -0
  88. package/lib/claude-boot-block.js +48 -0
  89. package/lib/cloud-mission.js +0 -6
  90. package/lib/codex-flight.js +0 -1
  91. package/lib/conductor-artifacts.js +25 -0
  92. package/lib/context-gatherer.js +0 -1
  93. package/lib/deck-compose.js +0 -1
  94. package/lib/deck-history.js +0 -1
  95. package/lib/deck-layout.js +0 -5
  96. package/lib/deck-review.js +0 -7
  97. package/lib/deck-schema.js +0 -1
  98. package/lib/default-verifier.js +0 -2
  99. package/lib/engine-registry.js +16 -8
  100. package/lib/experiments/daily.js +0 -6
  101. package/lib/fleet.js +210 -27
  102. package/lib/functional-owner.js +0 -3
  103. package/lib/inspect-fields.js +0 -1
  104. package/lib/known-commands.js +2 -2
  105. package/lib/learnings.js +0 -2
  106. package/lib/loop-doctor.js +11 -17
  107. package/lib/manifest.js +0 -2
  108. package/lib/member-alive.js +0 -1
  109. package/lib/member-switches.js +0 -5
  110. package/lib/mission-artifact.js +0 -1
  111. package/lib/mission-protected-lane.js +293 -0
  112. package/lib/mission-room.js +0 -2
  113. package/lib/mission-runtime-loop.js +0 -1
  114. package/lib/next-moves.js +0 -18
  115. package/lib/official-cli-integration.js +0 -4
  116. package/lib/orb-context.js +6 -3
  117. package/lib/orb-scorecard.js +0 -1
  118. package/lib/outbound-send-gate.js +0 -6
  119. package/lib/permission-grants.js +0 -6
  120. package/lib/policy-lessons.js +0 -2
  121. package/lib/pulse.js +68 -15
  122. package/lib/review-integrity.js +0 -2
  123. package/lib/router-brain.js +0 -2
  124. package/lib/runner-command.js +0 -4
  125. package/lib/runtime-bootstrap.js +0 -1
  126. package/lib/scorecard.js +0 -1
  127. package/lib/self-drive.js +1 -1
  128. package/lib/state-detection.js +0 -6
  129. package/lib/task-db.js +0 -4
  130. package/lib/team-presence.js +0 -1
  131. package/lib/voice-gate.js +63 -2
  132. package/lib/wiki.js +152 -9
  133. package/lib/wish-audit.js +20 -6
  134. package/lib/wish-delegate.js +26 -20
  135. package/lib/wish-stats.js +0 -5
  136. package/lib/wish-store.js +0 -1
  137. package/lib/workspace-safety.js +0 -2
  138. package/lib/zip.js +0 -1
  139. package/package.json +1 -1
@@ -379,7 +379,5 @@ function codexGoalCommand(args = []) {
379
379
 
380
380
  module.exports = {
381
381
  codexGoalCommand,
382
- CONFIRM_RESET_FLAG,
383
- resolveStatePath,
384
382
  sqlString,
385
383
  };
@@ -1405,7 +1405,7 @@ function computerLocalLegacy(extraArgs = []) {
1405
1405
  }
1406
1406
  }
1407
1407
 
1408
- function computerLocal(extraArgs = []) {
1408
+ function computerLocal(extraArgs = [], options = {}) {
1409
1409
  printLocalWordmark();
1410
1410
  printModeBanner('LOCAL', process.cwd(), [
1411
1411
  'Claude Code + Atris workspace context.',
@@ -1417,7 +1417,7 @@ function computerLocal(extraArgs = []) {
1417
1417
  const originalArgv = process.argv;
1418
1418
  process.argv = [originalArgv[0], originalArgv[1], originalArgv[2], 'claude', ...extraArgs];
1419
1419
  try {
1420
- consoleCommand();
1420
+ consoleCommand(options);
1421
1421
  } finally {
1422
1422
  process.argv = originalArgv;
1423
1423
  }
@@ -4390,11 +4390,12 @@ async function runComputer(argv = process.argv.slice(3), deps = {}) {
4390
4390
  module.exports = {
4391
4391
  runComputer,
4392
4392
  computerSetup,
4393
- setupComputerChoices,
4393
+ // `atris pack run` starts a packet as a free local computer, so it needs the
4394
+ // local starter and the business-binding check as seams, not copies.
4395
+ computerLocal,
4396
+ readBusinessBinding,
4394
4397
  formatEngineSeats,
4395
4398
  buildComputerCard,
4396
- renderComputerCard,
4397
- renderComputerCardMarkdown,
4398
4399
  extractAttachedWorkspaceMismatch,
4399
4400
  contextForAttachedWorkspaceMismatch,
4400
4401
  printRecruitingLocalSyncOutcome,
@@ -7,10 +7,40 @@ const { resolveClaudeRunnerBin } = require('../lib/runner-command');
7
7
 
8
8
  // ── Context Gathering ──────────────────────────────────────────────
9
9
 
10
+ // Atris content lives in two shapes on disk, and the console has to read both.
11
+ //
12
+ // workspace: <dir>/atris/{skills,team,TODO.md,...}
13
+ // The normal repo shape. A project keeps its Atris brain in a subfolder so
14
+ // it does not collide with the project's own files. This is the common case.
15
+ //
16
+ // packet: <dir>/{pack.json,skills,team,docs,...}
17
+ // A packet built in root mode (pack.js exports with prefix '' when the
18
+ // source dir has pack.json at its root — this is how the shipped packs are
19
+ // built and what the web /packs/<slug>/download route serves). `pack
20
+ // install` preserves that flat layout, so the packet folder IS the atris
21
+ // root; there is no nested atris/ to find.
22
+ //
23
+ // Nested wins when both exist: never change what an existing workspace loads.
24
+ // Returns the directory that actually holds the atris content, or null.
25
+ function resolveAtrisRoot(workspaceDir) {
26
+ const nested = path.join(workspaceDir, 'atris');
27
+ try {
28
+ if (fs.statSync(nested).isDirectory()) return nested;
29
+ } catch {}
30
+ const manifest = path.join(workspaceDir, 'pack.json');
31
+ try {
32
+ if (!fs.statSync(manifest).isFile()) return null;
33
+ JSON.parse(fs.readFileSync(manifest, 'utf8'));
34
+ return workspaceDir;
35
+ } catch {}
36
+ return null;
37
+ }
38
+
10
39
  function gatherAtrisContext(workspaceDir) {
11
- const atrisDir = path.join(workspaceDir, 'atris');
40
+ const atrisDir = resolveAtrisRoot(workspaceDir);
12
41
  const ctx = {
13
- hasAtris: fs.existsSync(atrisDir),
42
+ hasAtris: Boolean(atrisDir),
43
+ atrisRoot: atrisDir,
14
44
  skills: [],
15
45
  teamMembers: [],
16
46
  backlogCount: 0,
@@ -119,7 +149,8 @@ function buildSystemPrompt(ctx) {
119
149
  if (ctx.teamMembers.length > 0) {
120
150
  lines.push('## Team Members');
121
151
  lines.push(`Available: ${ctx.teamMembers.join(', ')}`);
122
- lines.push('Each has a MEMBER.md in atris/team/<name>/ defining their role.');
152
+ const teamRoot = ctx.atrisRoot ? path.join(ctx.atrisRoot, 'team') : 'atris/team';
153
+ lines.push(`Each has a MEMBER.md in ${teamRoot}/<name>/ defining their role.`);
123
154
  lines.push('');
124
155
  }
125
156
 
@@ -135,7 +166,8 @@ function buildSystemPrompt(ctx) {
135
166
 
136
167
  if (ctx.backlogCount > 0) {
137
168
  lines.push(`## Current Work`);
138
- lines.push(`${ctx.backlogCount} task${ctx.backlogCount > 1 ? 's' : ''} in backlog. Check atris/TODO.md for details.`);
169
+ const todoPath = ctx.atrisRoot ? path.join(ctx.atrisRoot, 'TODO.md') : 'atris/TODO.md';
170
+ lines.push(`${ctx.backlogCount} task${ctx.backlogCount > 1 ? 's' : ''} in backlog. Check ${todoPath} for details.`);
139
171
  lines.push('');
140
172
  }
141
173
 
@@ -280,10 +312,11 @@ function checkAuth(backend) {
280
312
 
281
313
  // ── Launch ──────────────────────────────────────────────────────────
282
314
 
283
- function launchClaude(systemPrompt, extraArgs) {
315
+ function launchClaude(systemPrompt, extraArgs, options = {}) {
316
+ const skipPermissions = options.skipPermissions !== false;
284
317
  const runnerBin = resolveClaudeRunnerBin();
285
318
  const args = [
286
- '--dangerously-skip-permissions',
319
+ ...(skipPermissions ? ['--dangerously-skip-permissions'] : []),
287
320
  '--append-system-prompt', systemPrompt,
288
321
  ...extraArgs,
289
322
  ];
@@ -342,7 +375,11 @@ function launchCodex(systemPrompt, extraArgs) {
342
375
 
343
376
  // ── Main ────────────────────────────────────────────────────────────
344
377
 
345
- function consoleCommand() {
378
+ // skipPermissions defaults to true: `atris console` runs on a workspace you
379
+ // wrote. Callers that launch an agent inside content someone else authored
380
+ // (`atris pack run`) pass false so permission prompts stay on.
381
+ function consoleCommand(options = {}) {
382
+ const skipPermissions = options.skipPermissions !== false;
346
383
  const args = process.argv.slice(3);
347
384
  let requested = args[0];
348
385
 
@@ -385,7 +422,7 @@ function consoleCommand() {
385
422
 
386
423
  // Launch
387
424
  if (backend === 'claude') {
388
- launchClaude(systemPrompt, extraArgs);
425
+ launchClaude(systemPrompt, extraArgs, { skipPermissions });
389
426
  } else {
390
427
  launchCodex(systemPrompt, extraArgs);
391
428
  }
@@ -399,4 +436,4 @@ function consoleCommand() {
399
436
  }
400
437
  }
401
438
 
402
- module.exports = { consoleCommand, gatherAtrisContext, buildSystemPrompt };
439
+ module.exports = { consoleCommand, gatherAtrisContext, buildSystemPrompt, resolveAtrisRoot, launchClaude };
package/commands/deck.js CHANGED
@@ -141,6 +141,23 @@ function readSpec(specPath) {
141
141
  return JSON.parse(fs.readFileSync(specPath, 'utf8'));
142
142
  }
143
143
 
144
+ // Pluralize a count + noun ("1 error", "2 errors").
145
+ function countLabel(n, word) {
146
+ return `${n} ${word}${n === 1 ? '' : 's'}`;
147
+ }
148
+
149
+ // True when the args carry an explicit help flag.
150
+ function wantsHelp(argv) {
151
+ return argv.includes('--help') || argv.includes('-h');
152
+ }
153
+
154
+ // One-line lint tally with correct singular/plural nouns.
155
+ function lintSummary(findings) {
156
+ const errs = findings.filter((f) => f.severity === 'error').length;
157
+ const warns = findings.filter((f) => f.severity === 'warn').length;
158
+ return `${countLabel(errs, 'error')}, ${countLabel(warns, 'warning')}`;
159
+ }
160
+
144
161
  function printLint(findings) {
145
162
  if (!findings.length) {
146
163
  console.log('\n ✓ spec lint clean\n');
@@ -369,15 +386,13 @@ async function run(argv) {
369
386
  const findings = checkSpec(spec, lintSpec);
370
387
  if (out) {
371
388
  fs.writeFileSync(out, `${JSON.stringify(spec, null, 2)}\n`);
372
- console.error(`\n ✓ composed ${spec.slides.length} slides (${spec.theme}) -> ${out}`);
389
+ console.error(`\n ✓ composed ${countLabel(spec.slides.length, 'slide')} (${spec.theme}) -> ${out}`);
373
390
  printLint(findings);
374
391
  console.error(` next: atris deck build ${out} --review\n`);
375
392
  } else {
376
393
  // stdout stays clean JSON for piping; lint goes to stderr
377
394
  console.log(JSON.stringify(spec, null, 2));
378
- const errs = findings.filter((f) => f.severity === 'error').length;
379
- const warns = findings.filter((f) => f.severity === 'warn').length;
380
- console.error(` composed ${spec.slides.length} slides (${spec.theme}) · lint: ${errs} errors, ${warns} warnings`);
395
+ console.error(` composed ${countLabel(spec.slides.length, 'slide')} (${spec.theme}) · lint: ${lintSummary(findings)}`);
381
396
  }
382
397
  return hasErrors(findings) ? 1 : 0;
383
398
  }
@@ -385,8 +400,11 @@ async function run(argv) {
385
400
  if (sub === 'lint') {
386
401
  const specPath = argv.slice(1).find((a) => !a.startsWith('-'));
387
402
  if (!specPath) {
388
- console.error(' usage: atris deck lint <spec.json>');
389
- return 2;
403
+ // Explicit -h/--help is a help request (stdout, exit 0); a genuinely
404
+ // missing spec path is a usage error (stderr, exit 2).
405
+ const help = wantsHelp(argv);
406
+ console[help ? 'log' : 'error'](' usage: atris deck lint <spec.json>');
407
+ return help ? 0 : 2;
390
408
  }
391
409
  let spec;
392
410
  try { spec = readSpec(specPath); }
@@ -488,8 +506,9 @@ async function run(argv) {
488
506
  }
489
507
  const specPath = specPaths[0];
490
508
  if (!specPath) {
491
- console.error(' usage: atris deck build <spec.json> [<spec2.json> ...] [--title T] [--theme x] [--review]');
492
- return 2;
509
+ const help = wantsHelp(argv);
510
+ console[help ? 'log' : 'error'](' usage: atris deck build <spec.json> [<spec2.json> ...] [--title T] [--theme x] [--review]');
511
+ return help ? 0 : 2;
493
512
  }
494
513
  let spec;
495
514
  try { spec = readSpec(specPath); }
@@ -576,6 +595,8 @@ module.exports = {
576
595
  lintSpec,
577
596
  validateSpec,
578
597
  checkSpec,
598
+ countLabel,
599
+ lintSummary,
579
600
  planBatch,
580
601
  publishDeck,
581
602
  videoToDeck,
package/commands/dream.js CHANGED
@@ -353,13 +353,6 @@ async function dreamCommand(args = [], root = process.cwd(), options = {}) {
353
353
  }
354
354
 
355
355
  module.exports = {
356
- DREAM_TIMEOUT_MS,
357
356
  dreamsPath,
358
- collectDreamMaterial,
359
- buildDreamPrompt,
360
- parseDreamCards,
361
- dreamModels,
362
- defaultDreamRunner,
363
- runDream,
364
357
  dreamCommand,
365
358
  };
package/commands/drill.js CHANGED
@@ -365,7 +365,6 @@ async function drillCommand(args = []) {
365
365
 
366
366
  module.exports = {
367
367
  COAUTHOR,
368
- VERIFY_COMMAND,
369
368
  drillCommand,
370
369
  runDrill,
371
370
  };
@@ -33,6 +33,7 @@ const {
33
33
  engineRegistryView,
34
34
  readEngineRegistry,
35
35
  resolveEngineForRole,
36
+ resolveEngineForRoleRanked,
36
37
  setEngineHealth,
37
38
  } = require('../lib/engine-registry');
38
39
  const { FLEET_CAPABLE, runDispatchFlight } = require('../lib/fleet');
@@ -932,22 +933,26 @@ function runResolveCommand(args, root) {
932
933
  else console.error(message);
933
934
  return 2;
934
935
  }
935
- let engine;
936
+ let picked;
936
937
  try {
937
- engine = resolveEngineForRole(role, root);
938
+ picked = resolveEngineForRoleRanked(role, root);
938
939
  } catch (err) {
939
940
  if (json) console.log(JSON.stringify({ ok: false, error: err.message }, null, 2));
940
941
  else console.error(err.message);
941
942
  return 2;
942
943
  }
944
+ const engine = picked.engine;
943
945
  if (!engine) {
944
946
  const message = `No ready installed engine can fill role "${role}".`;
945
- if (json) console.log(JSON.stringify({ ok: false, role, error: message }, null, 2));
947
+ if (json) console.log(JSON.stringify({ ok: false, role, error: message, won_reason: picked.reason }, null, 2));
946
948
  else console.error(message);
947
949
  return 1;
948
950
  }
949
- if (json) console.log(JSON.stringify(engine, null, 2));
950
- else console.log(engine.id);
951
+ if (json) console.log(JSON.stringify({ ...engine, won_reason: picked.reason }, null, 2));
952
+ else {
953
+ console.error(picked.reason);
954
+ console.log(engine.id);
955
+ }
951
956
  return 0;
952
957
  }
953
958
 
@@ -1294,36 +1299,23 @@ module.exports = {
1294
1299
  resolveDefaultEngine,
1295
1300
  canonicalEngineName,
1296
1301
  readSavedEngine,
1297
- setEngine,
1298
- resetEngine,
1299
1302
  roster,
1300
- registryPayload,
1301
1303
  engineRegistryFile,
1302
1304
  readEngineRegistry,
1303
1305
  resolveEngineForRole,
1304
1306
  setEngineHealth,
1305
- probeEngine,
1306
- runEngineTest,
1307
1307
  parseDispatchArgs,
1308
1308
  runDispatchCommand,
1309
1309
  ENGINE_LOGIN_MANIFESTS,
1310
- MAX_ENGINE_LOGIN_FILE_BYTES,
1311
1310
  expandHomePath,
1312
1311
  normalizeLoginProvider,
1313
1312
  normalizeEngineLoginSeat,
1314
1313
  validEngineLoginSeat,
1315
1314
  engineLoginSeatError,
1316
- detectedEmailFromContent,
1317
- readEngineLoginFiles,
1318
1315
  parseEngineLoginArgs,
1319
- parseEngineSeedArgs,
1320
- redactBackendResponse,
1321
- buildEngineLoginPayload,
1322
- listEngineLogins,
1323
1316
  listEngineSeats,
1324
1317
  readyCheckEngineLogin,
1325
1318
  formatEngineSeats,
1326
- runEngineSeatsCommand,
1327
1319
  runEngineDeviceLoginCommand,
1328
1320
  runEngineLoginCommand,
1329
1321
  runEngineSeedCommand,
@@ -588,8 +588,4 @@ function experimentsCommand(subcommand, ...args) {
588
588
  module.exports = {
589
589
  experimentsCommand,
590
590
  ensureExperimentsFramework,
591
- buildBenchmarkArtifact,
592
- parseRunOptions,
593
- experimentsDaily,
594
- experimentsQueue,
595
591
  };
@@ -79,6 +79,16 @@ async function writeReportToComputer(token, businessId, workspaceId, report) {
79
79
  return remotePath;
80
80
  }
81
81
 
82
+ // A missing scoreboard field must never reach the owner as "$NaN" or
83
+ // "$undefined"; coerce every number to 0 before it renders.
84
+ function formatScoreboardPnl(board) {
85
+ if (!board) return '';
86
+ const mrr = Number(board.revenue_mrr_usd) || 0;
87
+ const cost = (Number(board.compute_cost_usd) || 0) + (Number(board.ec2_cost_usd) || 0);
88
+ const profit = Number(board.profit_daily_usd) || 0;
89
+ return ` | mrr $${mrr} cost $${cost.toFixed(2)}/d profit $${profit}/d`;
90
+ }
91
+
82
92
  async function deliverToBusiness(token, biz, { wake, dryRun }) {
83
93
  const label = biz.slug || biz.name || biz.id;
84
94
  const status = await computerStatus(token, biz.id);
@@ -97,9 +107,7 @@ async function deliverToBusiness(token, biz, { wake, dryRun }) {
97
107
 
98
108
  const report = await fetchDailyReport(token, biz.id);
99
109
  const board = report.scoreboard || null;
100
- const pnl = board
101
- ? ` | mrr $${board.revenue_mrr_usd} cost $${(board.compute_cost_usd + (board.ec2_cost_usd || 0)).toFixed(2)}/d profit $${board.profit_daily_usd}/d`
102
- : '';
110
+ const pnl = formatScoreboardPnl(board);
103
111
  if (dryRun) {
104
112
  console.log(` ${label}: dry-run, report ${String(report.markdown || '').length} chars${pnl}`);
105
113
  return { business: label, delivered: false, reason: 'dry-run', scoreboard: board };
@@ -203,4 +211,4 @@ async function fleetReport() {
203
211
  process.exitCode = delivered > 0 || dryRun || results.every((r) => r.reason === 'asleep') ? 0 : 1;
204
212
  }
205
213
 
206
- module.exports = { fleetReport };
214
+ module.exports = { fleetReport, formatScoreboardPnl };
@@ -0,0 +1,261 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execFileSync } = require('child_process');
6
+
7
+ const DEFAULT_WIDTH = 80;
8
+ const MIN_WIDTH = 20;
9
+ const MAX_WIDTH = 120;
10
+ const STORYLINE_SLUG_PATTERN = /^[a-z0-9-]+$/;
11
+
12
+ function isValidStorylineSlug(slug) {
13
+ return typeof slug === 'string' && STORYLINE_SLUG_PATTERN.test(slug);
14
+ }
15
+
16
+ function parseGameArgs(args = []) {
17
+ let storyline = null;
18
+
19
+ for (let index = 0; index < args.length; index += 1) {
20
+ const arg = args[index];
21
+ if (arg === '--storyline') {
22
+ const value = args[index + 1];
23
+ if (!value || value.startsWith('--')) {
24
+ return { error: 'missing storyline slug' };
25
+ }
26
+ if (storyline) return { error: 'choose one storyline' };
27
+ storyline = value;
28
+ index += 1;
29
+ continue;
30
+ }
31
+ if (arg.startsWith('--')) return { error: `unknown game option: ${arg}` };
32
+ if (storyline) return { error: 'choose one storyline' };
33
+ storyline = arg;
34
+ }
35
+
36
+ return {
37
+ storyline,
38
+ publisherArgs: storyline ? ['--json', '--storyline', storyline] : ['--json'],
39
+ };
40
+ }
41
+
42
+ function availableStorylines(workspaceRoot) {
43
+ const directory = path.join(workspaceRoot, 'atris', 'storylines');
44
+ try {
45
+ return fs.readdirSync(directory, { withFileTypes: true })
46
+ .filter(entry => entry.isFile() && entry.name.endsWith('.md'))
47
+ .map(entry => path.basename(entry.name, '.md'))
48
+ .filter(slug => slug !== 'resume-log')
49
+ .sort();
50
+ } catch {
51
+ return [];
52
+ }
53
+ }
54
+
55
+ function publisherSupportsStoryline(script) {
56
+ try {
57
+ return fs.readFileSync(script, 'utf8').includes('--storyline');
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ function clean(value, fallback = '') {
64
+ const text = String(value == null ? '' : value)
65
+ .replace(/[—–]/g, '-')
66
+ .replace(/\s+/g, ' ')
67
+ .trim();
68
+ return text || fallback;
69
+ }
70
+
71
+ function clip(value, width) {
72
+ const chars = Array.from(clean(value));
73
+ if (chars.length <= width) return chars.join('');
74
+ if (width <= 3) return chars.slice(0, width).join('');
75
+ return `${chars.slice(0, width - 3).join('').trimEnd()}...`;
76
+ }
77
+
78
+ function pad(value, width) {
79
+ const text = clip(value, width);
80
+ return `${text}${' '.repeat(Math.max(0, width - Array.from(text).length))}`;
81
+ }
82
+
83
+ function dashboardWidth(opts) {
84
+ const requested = Number(opts && opts.width);
85
+ if (!Number.isFinite(requested)) return DEFAULT_WIDTH;
86
+ return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.trunc(requested)));
87
+ }
88
+
89
+ function styles(enabled) {
90
+ const wrap = code => value => (
91
+ enabled ? `\x1b[${code}m${value}\x1b[0m` : String(value)
92
+ );
93
+ return {
94
+ bold: wrap('1'),
95
+ dim: wrap('2'),
96
+ green: wrap('32'),
97
+ yellow: wrap('33'),
98
+ cyan: wrap('36'),
99
+ };
100
+ }
101
+
102
+ function chapterLine(chapter, width, style) {
103
+ const state = clean(chapter && chapter.state, 'locked').toLowerCase();
104
+ const current = state === 'current';
105
+ const done = state === 'complete' || state === 'done';
106
+ const marker = current ? '[NOW ]' : done ? '[DONE]' : '[LOCK]';
107
+ const number = chapter && chapter.n != null ? `${chapter.n}.` : '';
108
+ const location = current ? ' YOU ARE HERE' : '';
109
+ const line = clip(`${marker} ${number} ${clean(chapter && chapter.title, 'Untitled chapter')}${location}`, width);
110
+ if (current) return style.yellow(style.bold(line));
111
+ if (done) return style.green(line);
112
+ return style.dim(line);
113
+ }
114
+
115
+ function missionBox(mission, width, style) {
116
+ const innerWidth = width - 2;
117
+ const contentWidth = width - 4;
118
+ const heading = ' CURRENT MISSION ';
119
+ const top = heading.length <= innerWidth
120
+ ? `╔${heading}${'═'.repeat(innerWidth - heading.length)}╗`
121
+ : `╔${'═'.repeat(innerWidth)}╗`;
122
+ const row = value => `║ ${pad(value, contentWidth)} ║`;
123
+ const bottom = `╚${'═'.repeat(innerWidth)}╝`;
124
+ const title = clean(mission && mission.title, 'No current mission');
125
+ const status = clean(mission && mission.status, 'unavailable').toUpperCase();
126
+ return [
127
+ style.cyan(style.bold(top)),
128
+ style.bold(row(title)),
129
+ row(`STATUS: ${status}`),
130
+ style.cyan(style.bold(bottom)),
131
+ ];
132
+ }
133
+
134
+ /**
135
+ * Render an atris.game_state.v1 payload without reading process or filesystem state.
136
+ * Callers opt into ANSI with opts.color and provide their terminal width via opts.width.
137
+ */
138
+ function renderGameDashboard(payload, opts = {}) {
139
+ const state = payload && typeof payload === 'object' ? payload : {};
140
+ const storyline = state.storyline && typeof state.storyline === 'object'
141
+ ? state.storyline
142
+ : null;
143
+ const missions = state.missions && typeof state.missions === 'object'
144
+ ? state.missions
145
+ : {};
146
+ const active = Array.isArray(missions.active) ? missions.active.slice(0, 3) : [];
147
+ const recentComplete = Array.isArray(missions.recent_complete)
148
+ ? missions.recent_complete.length
149
+ : Number(missions.recent_complete) || 0;
150
+ const width = dashboardWidth(opts);
151
+ const style = styles(opts.color === true);
152
+ const title = clean(storyline && storyline.name, 'No storyline loaded');
153
+ const destination = clean(storyline && storyline.destination, 'No destination loaded');
154
+ const chapters = Array.isArray(storyline && storyline.chapters)
155
+ ? storyline.chapters.slice(0, 7)
156
+ : [];
157
+ const streak = storyline && storyline.streak && typeof storyline.streak === 'object'
158
+ ? storyline.streak
159
+ : {};
160
+ const streakCurrent = Number.isFinite(Number(streak.current)) ? Number(streak.current) : 0;
161
+ const streakTarget = Number.isFinite(Number(streak.target)) ? Number(streak.target) : 0;
162
+ const streakLabel = clean(streak.label, 'mission streak');
163
+ const lines = [];
164
+
165
+ lines.push(style.cyan(style.bold(clip(title, width))));
166
+ lines.push(style.dim(clip(`DESTINATION: ${destination}`, width)));
167
+ lines.push('');
168
+ lines.push(style.bold('CHAPTER RAIL'));
169
+ if (chapters.length) {
170
+ for (const chapter of chapters) lines.push(chapterLine(chapter, width, style));
171
+ } else {
172
+ lines.push(style.dim(clip('[LOCK] No chapters loaded', width)));
173
+ }
174
+
175
+ lines.push('');
176
+ lines.push(...missionBox(storyline && storyline.mission, width, style));
177
+ lines.push('');
178
+ lines.push(style.yellow(style.bold(clip(`STREAK: ${streakCurrent} / ${streakTarget} | ${streakLabel}`, width))));
179
+ lines.push('');
180
+ lines.push(style.bold('LIVE MISSIONS'));
181
+ if (active.length) {
182
+ for (const mission of active) {
183
+ const id = mission && mission.id != null ? `#${mission.id}` : '#?';
184
+ lines.push(style.bold(clip(`${id} ${clean(mission && mission.title, 'Untitled mission')}`, width)));
185
+ lines.push(style.dim(clip(` owner: ${clean(mission && mission.owner, 'unassigned')}`, width)));
186
+ lines.push(clip(` next: ${clean(mission && mission.next, 'No next action recorded')}`, width));
187
+ }
188
+ } else {
189
+ lines.push(style.dim(clip('No active missions.', width)));
190
+ }
191
+ lines.push(clip(`RECENT COMPLETES: ${recentComplete}`, width));
192
+ lines.push('');
193
+ lines.push(style.cyan(style.bold(clip(`NEXT MOVE: ${clean(active[0] && active[0].next, 'No active mission')}`, width))));
194
+
195
+ return lines.join('\n');
196
+ }
197
+
198
+ function loadGameState(workspaceRoot = process.cwd(), publisherArgs = ['--json']) {
199
+ const script = path.join(workspaceRoot, 'scripts', 'game_state_sync.py');
200
+ const python = path.join(workspaceRoot, 'venv', 'bin', 'python');
201
+ if (!fs.existsSync(script) || !fs.existsSync(python)) return null;
202
+
203
+ try {
204
+ const stdout = execFileSync(python, [script, ...publisherArgs], {
205
+ cwd: workspaceRoot,
206
+ encoding: 'utf8',
207
+ stdio: ['ignore', 'pipe', 'ignore'],
208
+ maxBuffer: 4 * 1024 * 1024,
209
+ });
210
+ const payload = JSON.parse(stdout.trim());
211
+ return payload && typeof payload === 'object' ? payload : null;
212
+ } catch {
213
+ return null;
214
+ }
215
+ }
216
+
217
+ async function gameCommand(args = [], opts = {}) {
218
+ const parsed = parseGameArgs(args);
219
+ if (parsed.error) {
220
+ console.error(parsed.error);
221
+ return 1;
222
+ }
223
+ if (parsed.storyline && !isValidStorylineSlug(parsed.storyline)) {
224
+ console.error(`invalid storyline slug: ${parsed.storyline}`);
225
+ return 1;
226
+ }
227
+
228
+ const workspaceRoot = process.cwd();
229
+ if (parsed.storyline) {
230
+ const storylines = availableStorylines(workspaceRoot);
231
+ if (!storylines.includes(parsed.storyline)) {
232
+ console.error(`storyline not found: ${parsed.storyline}. available: ${storylines.join(', ') || 'none'}`);
233
+ return 1;
234
+ }
235
+ }
236
+
237
+ const script = path.join(workspaceRoot, 'scripts', 'game_state_sync.py');
238
+ const publisherArgs = parsed.storyline && publisherSupportsStoryline(script)
239
+ ? parsed.publisherArgs
240
+ : ['--json'];
241
+ const payload = loadGameState(workspaceRoot, publisherArgs);
242
+ if (!payload) {
243
+ if (!opts.silentMissing) {
244
+ console.log('no game state in this workspace (needs atris/storylines/)');
245
+ }
246
+ return false;
247
+ }
248
+
249
+ console.log(renderGameDashboard(payload, {
250
+ color: Boolean(process.stdout.isTTY) && !process.env.NO_COLOR,
251
+ width: process.stdout.columns || DEFAULT_WIDTH,
252
+ }));
253
+ return true;
254
+ }
255
+
256
+ module.exports = {
257
+ renderGameDashboard,
258
+ isValidStorylineSlug,
259
+ parseGameArgs,
260
+ gameCommand,
261
+ };
package/commands/gm.js CHANGED
@@ -658,6 +658,16 @@ async function gmCommand(...args) {
658
658
  return memberCommand('wake', member, ...passthrough);
659
659
  }
660
660
 
661
+ if (!hasFlag(args, '--json') && !hasFlag(args, '--watch')) {
662
+ try {
663
+ const { gameCommand } = require('./game');
664
+ const dashboardShown = await gameCommand([], { silentMissing: true });
665
+ if (dashboardShown) console.log('');
666
+ } catch {
667
+ // The dashboard is an optional opening screen and must never block GM mode.
668
+ }
669
+ }
670
+
661
671
  const state = gmState(args);
662
672
  if (hasFlag(args, '--json')) {
663
673
  console.log(JSON.stringify(state, null, 2));
@@ -671,6 +681,4 @@ async function gmCommand(...args) {
671
681
 
672
682
  module.exports = {
673
683
  gmCommand,
674
- gmState,
675
- gmWatchSnapshot,
676
684
  };
package/commands/goal.js CHANGED
@@ -237,11 +237,5 @@ function run(args = [], context = {}) {
237
237
  }
238
238
 
239
239
  module.exports = {
240
- SCOREBOARD_FILE,
241
- buildGoalView,
242
- readScoreboard,
243
- renderGoalView,
244
240
  run,
245
- scoreboardPath,
246
- writeScoreboard,
247
241
  };
@@ -174,6 +174,5 @@ function harvestCommand(args = []) {
174
174
  }
175
175
 
176
176
  module.exports = {
177
- harvestActions,
178
177
  harvestCommand,
179
178
  };