atris 3.58.7 → 3.58.8

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 (67) hide show
  1. package/GETTING_STARTED.md +2 -2
  2. package/PERSONA.md +11 -11
  3. package/README.md +1 -1
  4. package/atris/GEMINI.md +7 -6
  5. package/atris/PERSONA.md +4 -4
  6. package/atris/features/README.md +77 -6
  7. package/atris/skills/README.md +44 -15
  8. package/atris/skills/aeo/SKILL.md +10 -10
  9. package/atris/skills/atris/SKILL.md +1 -1
  10. package/atris/skills/engines/SKILL.md +42 -50
  11. package/atris/skills/meta/SKILL.md +4 -4
  12. package/bin/atris.js +5 -6
  13. package/commands/aeo.js +9 -12
  14. package/commands/analytics.js +8 -11
  15. package/commands/autopilot.js +4 -3
  16. package/commands/avail.js +1 -1
  17. package/commands/brain.js +3 -1
  18. package/commands/business.js +13 -8
  19. package/commands/close.js +2 -1
  20. package/commands/computer.js +15 -10
  21. package/commands/context-sync.js +3 -3
  22. package/commands/doc-health.js +50 -2
  23. package/commands/experiments.js +13 -4
  24. package/commands/improve.js +1 -1
  25. package/commands/integrations.js +2 -5
  26. package/commands/learn.js +1 -1
  27. package/commands/member.js +174 -47
  28. package/commands/mission.js +57 -11
  29. package/commands/pack.js +2 -2
  30. package/commands/probe.js +11 -5
  31. package/commands/pull.js +1 -1
  32. package/commands/push.js +1 -1
  33. package/commands/rainmaker.js +3 -16
  34. package/commands/spaceship.js +1 -1
  35. package/commands/stream.js +27 -2
  36. package/commands/study.js +15 -9
  37. package/commands/sync.js +1 -1
  38. package/commands/task.js +13 -2
  39. package/commands/workflow.js +6 -1
  40. package/lib/codex-flight.js +28 -5
  41. package/lib/fleet.js +4 -1
  42. package/lib/improve-vitals-html.js +3 -2
  43. package/lib/lesson-contradiction.js +2 -2
  44. package/lib/map-refs.js +183 -0
  45. package/lib/mission-room.js +4 -4
  46. package/lib/noninteractive.js +15 -4
  47. package/lib/policy-lessons.js +1 -1
  48. package/lib/receipt-block.js +137 -28
  49. package/lib/runner-command.js +1 -1
  50. package/lib/scorecard.js +4 -4
  51. package/lib/sleep-sync.js +11 -0
  52. package/lib/state-detection.js +28 -9
  53. package/lib/task-db.js +3 -2
  54. package/lib/todo-fallback.js +1 -1
  55. package/lib/wish-delegate.js +19 -5
  56. package/mcp/atris-mcp/index.mjs +77 -24
  57. package/package.json +2 -5
  58. package/scripts/det/det.js +2 -2
  59. package/scripts/det/pr-description.js +2 -2
  60. package/scripts/det/test.js +2 -2
  61. package/utils/api.js +1 -1
  62. package/utils/auth.js +17 -7
  63. package/utils/backend-root.js +31 -0
  64. package/utils/config.js +5 -4
  65. package/utils/owner-identity.js +61 -0
  66. package/atris/policies/LESSONS.md +0 -13
  67. package/atris/skills/autopilot/atris-autopilot.md +0 -71
package/bin/atris.js CHANGED
@@ -47,7 +47,7 @@ const {
47
47
  requireAccountBound,
48
48
  refuseAccountGlobal,
49
49
  } = require('../lib/account-bound');
50
- const { isHelpToken, argsWantHelp, hasYesFlag } = require('../lib/noninteractive');
50
+ const { isHelpToken, argsWantHelp, hasYesFlag, envForcesNonInteractive } = require('../lib/noninteractive');
51
51
 
52
52
  // State detection for smart default
53
53
  const { detectWorkspaceState, loadContext } = require('../lib/state-detection');
@@ -577,7 +577,7 @@ function showHelpAll() {
577
577
  console.log(' watch - Turn one sentence into an always-on background watcher');
578
578
  console.log(' ctop - Show a process-first live agent CPU/memory view');
579
579
  console.log(' doctor - Node/task/auth/workspace readiness (--json for agents)');
580
- console.log(' doc-health - workspace document size, navigation, and freshness (--json)');
580
+ console.log(' doc-health - workspace document size, navigation, freshness, and map line refs (--json, --fix-refs)');
581
581
  console.log(' launchpad - Show the next action from local brain, task, mission, and proof state');
582
582
  console.log(' brief - Show the one-glance operator brief');
583
583
  console.log(' status - See local work and completions (`atris status <business>` for remote)');
@@ -1299,8 +1299,7 @@ function useInteractiveAtrisUi() {
1299
1299
  return Boolean(
1300
1300
  process.stdin.isTTY
1301
1301
  && process.stdout.isTTY
1302
- && !process.env.ATRIS_NO_INTERACTIVE
1303
- && process.env.ATRIS_NONINTERACTIVE !== '1'
1302
+ && !envForcesNonInteractive()
1304
1303
  );
1305
1304
  }
1306
1305
 
@@ -1464,7 +1463,7 @@ async function interactiveEntry(userInput, options = {}) {
1464
1463
  : (context.inProgressFeatures || []).length;
1465
1464
 
1466
1465
  // Pull active missions (durable goals), these outrank dev-pipeline state
1467
- // because a mission with an unverified verifier is a Keshav-attributable
1466
+ // because a mission with an unverified verifier is an owner commitment
1468
1467
  // commitment that hasn't been closed yet.
1469
1468
  const activeMissions = loadActiveMissions(workspaceDir);
1470
1469
  const liveMissionsCount = activeMissions.length;
@@ -2959,7 +2958,7 @@ if (command === 'guide') {
2959
2958
  const subcommand = process.argv[3];
2960
2959
  const args = process.argv.slice(4);
2961
2960
  Promise.resolve(require('../commands/member').memberCommand(subcommand, ...args))
2962
- .then(() => process.exit(0))
2961
+ .then(() => process.exit(subcommand === 'run' ? (process.exitCode || 0) : 0))
2963
2962
  .catch((err) => { console.error(`✗ Error: ${err.message || err}`); process.exit(1); });
2964
2963
  } else if (command === 'app') {
2965
2964
  const subcommand = process.argv[3];
package/commands/aeo.js CHANGED
@@ -15,32 +15,29 @@
15
15
  * atris aeo discover <source> [...] # discovery audit
16
16
  * atris aeo audit <source> [...] # agent-usability audit
17
17
  *
18
- * Local workspace resolution: $ATRIS_AEO_ROOT or legacy $ATRIS_BACKEND_ROOT.
18
+ * Local workspace resolution: $ATRIS_AEO_ROOT or the configured backend root.
19
19
  */
20
20
 
21
21
  const fs = require('fs');
22
22
  const os = require('os');
23
23
  const path = require('path');
24
24
  const { spawnSync } = require('child_process');
25
+ const { BACKEND_ROOT_HINT, resolveBackendRoot: resolveConfiguredBackendRoot } = require('../utils/backend-root');
25
26
  const { loadCredentials, abortOnAuthFailure } = require('../utils/auth');
26
27
  const { apiRequestJson } = require('../utils/api');
27
28
  const { loadBusinesses, saveBusinesses } = require('./business');
28
29
 
29
- function resolveBackendRoot() {
30
- const candidates = [
31
- process.env.ATRIS_AEO_ROOT,
32
- process.env.ATRIS_BACKEND_ROOT,
33
- ].filter(Boolean);
34
- for (const root of candidates) {
35
- if (fs.existsSync(path.join(root, 'atris', 'features', 'aeo'))) return root;
36
- }
37
- return null;
30
+ function resolveAeoRoot() {
31
+ const explicit = process.env.ATRIS_AEO_ROOT;
32
+ if (explicit && fs.existsSync(path.join(explicit, 'atris', 'features', 'aeo'))) return explicit;
33
+ const backend = resolveConfiguredBackendRoot();
34
+ return backend && fs.existsSync(path.join(backend, 'atris', 'features', 'aeo')) ? backend : null;
38
35
  }
39
36
 
40
37
  function requireBackendRoot() {
41
- const root = resolveBackendRoot();
38
+ const root = resolveAeoRoot();
42
39
  if (!root) {
43
- console.error('Cannot find local AEO workspace. Set ATRIS_AEO_ROOT to a workspace containing atris/features/aeo.');
40
+ console.error(`cannot find local aeo workspace; ${BACKEND_ROOT_HINT}`);
44
41
  process.exit(1);
45
42
  }
46
43
  return root;
@@ -68,17 +68,14 @@ function analyticsAtris() {
68
68
  }
69
69
 
70
70
  // Parse timestamps for productivity hours. Match the journal heading
71
- // format `### Title -- HH:MM` (the dashed form) and the legacy `**HH:MM:SS**` form.
72
- const timestampMatches = content.match(/(?:\u2014|--)\s*(\d{2}):\d{2}(?::\d{2})?\b|\*\*(\d{2}):\d{2}(?::\d{2})?\*\*/g);
73
- if (timestampMatches) {
74
- timestampMatches.forEach(ts => {
75
- const m = ts.match(/(\d{2}):/);
76
- if (!m) return;
77
- const hour = parseInt(m[1], 10);
78
- if (Number.isFinite(hour) && hour >= 0 && hour < 24) {
79
- hourCounts[hour] = (hourCounts[hour] || 0) + 1;
80
- }
81
- });
71
+ // formats `### Title -- HH:MM`, `### Title - HH:MM`, and legacy `**HH:MM:SS**`.
72
+ // Only heading lines count, so a prose range like "10:00 - 12:30" is not a timestamp.
73
+ const timestampPattern = /^#{2,4} .*(?:\u2014|--| - )\s*(\d{2}):\d{2}(?::\d{2})?\s*$|\*\*(\d{2}):\d{2}(?::\d{2})?\*\*/gm;
74
+ for (const match of content.matchAll(timestampPattern)) {
75
+ const hour = parseInt(match[1] || match[2], 10);
76
+ if (Number.isFinite(hour) && hour >= 0 && hour < 24) {
77
+ hourCounts[hour] = (hourCounts[hour] || 0) + 1;
78
+ }
82
79
  }
83
80
  });
84
81
 
@@ -25,14 +25,15 @@ const {
25
25
  detectEndgameCompletion
26
26
  } = require('../lib/scorecard');
27
27
  const { REWARD_CONFIG, REWARD_CHECKSUM } = require('../lib/reward-config');
28
+ const { ownerIdentity } = require('../utils/owner-identity');
28
29
 
29
30
  const pkg = require('../package.json');
30
31
 
31
32
  const PHASE_TIMEOUT = 600000; // 10 min per phase
32
33
 
33
34
  function looksOwnerClaimed(claimed) {
34
- const text = String(claimed || '').toLowerCase();
35
- return /\bkeshav(?:rao)?\b/.test(text) || /\b(owner|human|operator)\b/.test(text);
35
+ const text = String(claimed || '').trim().toLowerCase();
36
+ return /\b(owner|human|operator)\b/.test(text) || ownerIdentity().names.includes(text.split(/\s+/)[0]);
36
37
  }
37
38
 
38
39
  function looksOwnerGatedTitle(title) {
@@ -640,7 +641,7 @@ function getContextFiles(phase, options = {}) {
640
641
  // checkout is forbidden (COORDINATION.md Rule 4). Interpolated into the
641
642
  // default and self-heal do prompts, never the benchmark prompt (it never
642
643
  // commits).
643
- const SHARED_CHECKOUT_GIT_CONTRACT = `- Shared-checkout git safety (COORDINATION.md Rule 4): edits to any repo OTHER than this tick's cwd (../atrisos-backend-style sibling repos) go through a per-tick worktree, start with \`atris worktree start --member <member> --task "<task>"\`, land with \`atris worktree ship --message "<msg>" --verify "<cmd>"\`. Never edit a sibling repo's shared checkout directly.
644
+ const SHARED_CHECKOUT_GIT_CONTRACT = `- Shared-checkout git safety (COORDINATION.md Rule 4): edits to any repo OTHER than this tick's cwd (including sibling repos) go through a per-tick worktree, start with \`atris worktree start --member <member> --task "<task>"\`, land with \`atris worktree ship --message "<msg>" --verify "<cmd>"\`. Never edit a sibling repo's shared checkout directly.
644
645
  - On a shared checkout, \`git reset\`, \`git checkout --\`, \`git clean\`, and stashing other agents' work are FORBIDDEN, concurrent ticks' uncommitted work lives there.`;
645
646
 
646
647
  /**
package/commands/avail.js CHANGED
@@ -308,7 +308,7 @@ async function slotsAvail(argv) {
308
308
  if (!username) {
309
309
  const identity = await apiRequestJson('/identity/me', { method: 'GET', token });
310
310
  if (!identity.ok || !identity.data?.username) {
311
- console.error('Could not resolve username, pass --user keshav');
311
+ console.error('could not resolve username; pass --user <username>.');
312
312
  return 1;
313
313
  }
314
314
  username = identity.data.username;
package/commands/brain.js CHANGED
@@ -3,6 +3,7 @@ const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const { spawnSync } = require('child_process');
5
5
  const { refreshNowFile } = require('./now');
6
+ const { ownerIdentity } = require('../utils/owner-identity');
6
7
  const escapeRegExp = require('../lib/escape-regexp');
7
8
  const { hasRenderedSections, isOpenSection, isDoneSection } = require('../lib/todo-sections');
8
9
  const { isNonInteractive } = require('../lib/noninteractive');
@@ -794,7 +795,8 @@ function memberNextMove(member, state = null) {
794
795
  if (member.slug === 'justin' || /justin/i.test(member.name || '')) {
795
796
  return `${name}: run one customer-moving GTM rep, update the relevant workspace state within 10 minutes, and leave a scorecard.`;
796
797
  }
797
- if (member.slug === 'keshav' || /keshav/i.test(member.name || '')) {
798
+ const ownerNames = ownerIdentity().names;
799
+ if (ownerNames.includes(String(member.slug || '').toLowerCase()) || ownerNames.includes(String(member.name || '').toLowerCase())) {
798
800
  return `${name}: act as Customer 0: choose one allocation, narrative, system, or talent leverage move; leave proof and route the next owner.`;
799
801
  }
800
802
  if (/opus|overnight/i.test(identity)) {
@@ -13,6 +13,10 @@ const {
13
13
  renderDryRun,
14
14
  } = require('../lib/business-simulate');
15
15
 
16
+ function businessWorkspaceBase() {
17
+ return process.env.ATRIS_BUSINESS_ROOT || path.join(os.homedir(), 'arena', 'atris-business');
18
+ }
19
+
16
20
  function getBusinessConfigPath() {
17
21
  const home = require('os').homedir();
18
22
  const dir = path.join(home, '.atris');
@@ -179,7 +183,7 @@ function buildBusinessCacheEntry(business, localSlug, existing = {}) {
179
183
  return entry;
180
184
  }
181
185
 
182
- function readBusinessFolderBindings(rootDir = path.join(os.homedir(), 'arena', 'atris-business')) {
186
+ function readBusinessFolderBindings(rootDir = businessWorkspaceBase()) {
183
187
  const skip = new Set([
184
188
  '.git', '.DS_Store', 'archive', 'archives', '_archive', 'bench', 'deals',
185
189
  'node_modules', 'shelf', '_shelf', 'templates',
@@ -510,7 +514,7 @@ function resolveWorkspaceRoot(slug, options = {}) {
510
514
  if (options.noLocal) return null;
511
515
  if (options.here) return options.cwd || process.cwd();
512
516
  if (options.root) return path.join(options.root, slug);
513
- return path.join(os.homedir(), 'arena', 'atris-business', slug);
517
+ return path.join(businessWorkspaceBase(), slug);
514
518
  }
515
519
 
516
520
  function createCanonicalBusinessWorkspace(targetRoot, bizMeta, options = {}) {
@@ -1851,7 +1855,7 @@ async function addBusiness(slug) {
1851
1855
  }
1852
1856
 
1853
1857
  async function listBusinesses(opts = {}) {
1854
- // --local mode: walk ~/arena/atris-business/ and show fleet status table
1858
+ // --local mode: walk the configured business workspace folder
1855
1859
  // (no API calls, rate-limit safe). Different from API-mode below which lists
1856
1860
  // businesses cached from the API.
1857
1861
  if (opts.local) {
@@ -1877,7 +1881,7 @@ async function listBusinesses(opts = {}) {
1877
1881
  }
1878
1882
 
1879
1883
  /**
1880
- * Walk ~/arena/atris-business/ and print a fleet status table for every
1884
+ * Walk the configured business folder and print a fleet status table for every
1881
1885
  * customer workspace. Pure local, no API calls, no rate-limit risk.
1882
1886
  *
1883
1887
  * Classifies each dir as: ready, flat, unbound, nested, bare, or superseded.
@@ -1891,7 +1895,7 @@ function listBusinessesLocal(opts = {}) {
1891
1895
  const SKIP_DIRS = new Set(['deals', 'archive', 'archives', '_archive', 'templates', 'node_modules', '.git']);
1892
1896
  const SKIP_FILES = new Set(['.DS_Store', 'Thumbs.db']);
1893
1897
 
1894
- const rootDir = opts.root || path.join(os.homedir(), 'arena', 'atris-business');
1898
+ const rootDir = opts.root || businessWorkspaceBase();
1895
1899
  const jsonMode = opts.json === true;
1896
1900
 
1897
1901
  if (!fs.existsSync(rootDir)) {
@@ -2609,7 +2613,7 @@ function parseBusinessDoctorOptions(args = []) {
2609
2613
  const options = {
2610
2614
  fix: args.includes('--fix'),
2611
2615
  json: args.includes('--json'),
2612
- root: path.join(os.homedir(), 'arena', 'atris-business'),
2616
+ root: businessWorkspaceBase(),
2613
2617
  };
2614
2618
  const rootIdx = args.indexOf('--root');
2615
2619
  if (rootIdx !== -1 && args[rootIdx + 1]) {
@@ -2623,7 +2627,7 @@ function printBusinessDoctorHelp() {
2623
2627
  console.log('');
2624
2628
  console.log('Checks cloud-active businesses against:');
2625
2629
  console.log(' - ~/.atris/businesses.json');
2626
- console.log(' - ~/arena/atris-business/*/.atris/business.json');
2630
+ console.log(` - ${businessWorkspaceBase()}/*/.atris/business.json`);
2627
2631
  console.log(' - canonical slug + alias bindings');
2628
2632
  console.log('');
2629
2633
  console.log('--fix rewrites only safe local cache entries. It does not rename folders or touch cloud data.');
@@ -3454,7 +3458,7 @@ async function quickstart() {
3454
3458
  atris business init "My Company" --template saas
3455
3459
 
3456
3460
  2. Open the local workspace:
3457
- cd ~/arena/atris-business/my-company
3461
+ cd ${businessWorkspaceBase()}/my-company
3458
3462
 
3459
3463
  3. See what Atris knows:
3460
3464
  atris
@@ -3675,6 +3679,7 @@ async function businessCommand(subcommand, ...args) {
3675
3679
 
3676
3680
  module.exports = {
3677
3681
  businessCommand,
3682
+ businessWorkspaceBase,
3678
3683
  loadBusinesses,
3679
3684
  saveBusinesses,
3680
3685
  businessMatchesSlug,
package/commands/close.js CHANGED
@@ -5,6 +5,7 @@ const { spawnSync } = require('child_process');
5
5
  const { knownCommands } = require('../lib/known-commands');
6
6
  const { readUsage, usagePath } = require('../lib/usage');
7
7
  const pulse = require('../lib/pulse');
8
+ const { isOwner } = require('../utils/owner-identity');
8
9
  const { parseLessons } = require('./autopilot');
9
10
 
10
11
  const DAY_MS = 24 * 60 * 60 * 1000;
@@ -240,7 +241,7 @@ function parkedFlags(cwd = process.cwd(), options = {}) {
240
241
  }
241
242
 
242
243
  function ownerWaitRank(flag) {
243
- return /^(you|me|operator|human|keshav)$/.test(String(flag.owner || '').toLowerCase()) ? 0 : 1;
244
+ return isOwner(flag.owner) ? 0 : 1;
244
245
  }
245
246
 
246
247
  function compareOpenFlags(a, b) {
@@ -23,8 +23,9 @@ const path = require('path');
23
23
  const readline = require('readline');
24
24
  const { spawnSync } = require('child_process');
25
25
  const { loadCredentials, decodeJwtClaims, promptUser, abortOnAuthFailure } = require('../utils/auth');
26
+ const { resolveBackendRoot } = require('../utils/backend-root');
26
27
  const { apiRequestJson, getApiBaseUrl, getAppBaseUrl } = require('../utils/api');
27
- const { loadBusinesses, saveBusinesses } = require('./business');
28
+ const { businessWorkspaceBase, loadBusinesses, saveBusinesses } = require('./business');
28
29
  const {
29
30
  engineLoginSeatError,
30
31
  formatEngineSeats,
@@ -37,6 +38,7 @@ const {
37
38
  const { consoleCommand, gatherAtrisContext, buildSystemPrompt } = require('./console');
38
39
  const { streamSession } = require('./serve');
39
40
  const { buildRemoteAtrisBootstrapCommand } = require('../lib/runtime-bootstrap');
41
+ const { envForcesNonInteractive } = require('../lib/noninteractive');
40
42
 
41
43
  function getToken() {
42
44
  const creds = loadCredentials();
@@ -270,11 +272,11 @@ const ui = {
270
272
  };
271
273
 
272
274
  function useInteractiveCloudUi() {
273
- return Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.ATRIS_NO_INTERACTIVE);
275
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY && !envForcesNonInteractive());
274
276
  }
275
277
 
276
278
  function useInteractiveTerminalUi() {
277
- return Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.ATRIS_NO_INTERACTIVE);
279
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY && !envForcesNonInteractive());
278
280
  }
279
281
 
280
282
  async function readPipedStdin() {
@@ -517,7 +519,7 @@ function displayHomeRelativePath(targetPath) {
517
519
  }
518
520
 
519
521
  function recruitingBusinessWorkspacePath(slug = RECRUITING_BUSINESS_SLUG) {
520
- const root = process.env.ATRIS_BUSINESS_ROOT || path.join(os.homedir(), 'arena', 'atris-business');
522
+ const root = businessWorkspaceBase();
521
523
  return path.join(root, slug);
522
524
  }
523
525
 
@@ -1371,11 +1373,12 @@ function printModeBanner(mode, root, lines = []) {
1371
1373
 
1372
1374
  function findAtrisCodeTerminal() {
1373
1375
  const envPath = process.env.ATRIS_CODE_PY;
1376
+ const backend = resolveBackendRoot();
1374
1377
  const candidates = [
1375
1378
  envPath,
1376
1379
  path.join(__dirname, '..', 'cli', 'atris_code.py'),
1377
1380
  path.join(process.cwd(), 'cli', 'atris_code.py'),
1378
- path.join(os.homedir(), 'arena', 'atrisos-backend', 'cli', 'atris_code.py'),
1381
+ ...(backend ? [path.join(backend, 'cli', 'atris_code.py')] : []),
1379
1382
  ].filter(Boolean);
1380
1383
 
1381
1384
  let dir = process.cwd();
@@ -2534,7 +2537,7 @@ async function computerCreate(token, args = [], defaults = {}) {
2534
2537
  console.log(` atris computer --business ${owner} --workspace ${workspaceId}`);
2535
2538
  console.log('');
2536
2539
  console.log('Org workspace:');
2537
- console.log(` cd ~/arena/atris-business/${owner}`);
2540
+ console.log(` cd ${businessWorkspaceBase()}/${owner}`);
2538
2541
  console.log(' atris member activate operator');
2539
2542
  console.log(' atris member activate validator');
2540
2543
  console.log('');
@@ -3500,7 +3503,7 @@ async function sendBusinessChat(token, ctx, message, sessionId, resetContext = f
3500
3503
  async function computerChat(token, ctx, initialOptions = {}) {
3501
3504
  if (!ctx) {
3502
3505
  console.error('Cloud computer mode requires a bound business workspace.');
3503
- console.error('Run this inside ~/arena/atris-business/<slug>/, or use `atris computer --local` for local mode.');
3506
+ console.error('run this inside a bound business workspace, or use `atris computer --local` for local mode.');
3504
3507
  return;
3505
3508
  }
3506
3509
 
@@ -3741,7 +3744,7 @@ async function computerChat(token, ctx, initialOptions = {}) {
3741
3744
  async function computerLocalAtris(token, ctx, initialOptions = {}) {
3742
3745
  if (!ctx) {
3743
3746
  console.error('Atris local mode needs a bound business workspace for the cloud brain.');
3744
- console.error('Run inside ~/arena/atris-business/<slug>/, or use `atris computer local-byo`.');
3747
+ console.error('run inside a bound business workspace, or use `atris computer local-byo`.');
3745
3748
  return;
3746
3749
  }
3747
3750
 
@@ -3976,7 +3979,7 @@ async function computerLocalAtris(token, ctx, initialOptions = {}) {
3976
3979
  async function computerProof(token, ctx, initialOptions = {}) {
3977
3980
  if (!ctx) {
3978
3981
  console.error('Atris computer proof needs a bound business workspace.');
3979
- console.error('Run inside ~/arena/atris-business/<slug>/ first.');
3982
+ console.error('run inside a bound business workspace first.');
3980
3983
  process.exitCode = 1;
3981
3984
  return;
3982
3985
  }
@@ -4290,7 +4293,7 @@ async function runComputer(argv = process.argv.slice(3), deps = {}) {
4290
4293
  console.log('A business can be a company, lab, collective, community, artist, team, or project.');
4291
4294
  console.log('');
4292
4295
  console.log('First use:');
4293
- console.log(' cd ~/arena/atris-business/<business>');
4296
+ console.log(` cd ${businessWorkspaceBase()}/<business>`);
4294
4297
  console.log(' atris computer');
4295
4298
  console.log(' Choose Cloud workspace or Local folder, then type the outcome in plain English.');
4296
4299
  console.log('');
@@ -4470,6 +4473,8 @@ module.exports = {
4470
4473
  extractAttachedWorkspaceMismatch,
4471
4474
  contextForAttachedWorkspaceMismatch,
4472
4475
  printRecruitingLocalSyncOutcome,
4476
+ useInteractiveCloudUi,
4477
+ useInteractiveTerminalUi,
4473
4478
  // Hermetic parsing/formatting layer, exported for test/computer.test.js.
4474
4479
  parseComputerOptions,
4475
4480
  parseComputerCreateArgs,
@@ -64,7 +64,7 @@ async function businessStatus(slug) {
64
64
  const timeSince = manifest ? _timeSince(manifest.last_sync) : null;
65
65
 
66
66
  console.log('');
67
- console.log(`${biz.name}` + (timeSince ? ` \u2014 last synced ${timeSince}` : ' \u2014 never synced'));
67
+ console.log(`${biz.name}` + (timeSince ? `: last synced ${timeSince}` : ': never synced'));
68
68
 
69
69
  // Determine local directory
70
70
  let localDir = null;
@@ -254,11 +254,11 @@ async function businessLog(slug) {
254
254
 
255
255
  const commits = result.data.commits || [];
256
256
  if (commits.length === 0) {
257
- console.log(`\n ${biz.name} \u2014 no history yet.\n`);
257
+ console.log(`\n ${biz.name}: no history yet.\n`);
258
258
  return;
259
259
  }
260
260
 
261
- console.log(`\n ${biz.name} \u2014 history\n`);
261
+ console.log(`\n ${biz.name}: history\n`);
262
262
 
263
263
  for (const commit of commits) {
264
264
  const date = _timeSince(commit.date) || commit.date;
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { resolveWorkspaceRoot } = require('../lib/mission-root');
6
6
  const { readText } = require('./brain');
7
+ const { checkMapRefs, fixMapRefs } = require('../lib/map-refs');
7
8
 
8
9
  const BOOT_FILES = [
9
10
  'CLAUDE.md', 'AGENTS.md', 'atris/atris.md', 'atris/MAP.md',
@@ -74,9 +75,17 @@ function collectMap(root, text, featureNames, memberNames) {
74
75
  }
75
76
  const files = [...paths].map(file => ({ path: file, exists: fs.existsSync(path.resolve(root, file)) }));
76
77
  const existing = files.filter(file => file.exists).length;
78
+ const pathScore = files.length ? existing / files.length : 0;
79
+ const { refs: all, ...refs } = checkMapRefs(root, text);
80
+ refs.flagged = all.filter(ref => ref.status !== 'ok').map(ref => ({
81
+ map_line: ref.map_line, ref: ref.raw, symbol: ref.symbol, status: ref.status,
82
+ ...(ref.line ? { line: ref.line } : {}), ...(ref.lines ? { lines: ref.lines } : {}),
83
+ ...(ref.reason ? { reason: ref.reason } : {}),
84
+ }));
77
85
  return {
78
86
  rows, paths: files.length, existing, files,
79
- score: files.length ? existing / files.length : 0,
87
+ // A path that exists but a line ref that points at the wrong code is still a wrong map.
88
+ path_score: pathScore, refs, score: pathScore * refs.score,
80
89
  features: folderCoverage(text, 'atris/features', featureNames),
81
90
  members: folderCoverage(text, 'atris/team', memberNames),
82
91
  };
@@ -272,6 +281,34 @@ function table(headers, rows) {
272
281
  return cells.map(row => row.map((cell, i) => cell.padEnd(widths[i])).join(' ').trimEnd());
273
282
  }
274
283
 
284
+ function refsLine(refs) {
285
+ const gone = refs.missing + refs.missing_file;
286
+ return `map refs: ${refs.ok} right, ${refs.moved} moved, ${refs.ambiguous} unclear, ${gone} gone`;
287
+ }
288
+
289
+ function describeLeft(ref) {
290
+ const where = `MAP line ${ref.map_line}: ${ref.raw}${ref.symbol ? ` (${ref.symbol})` : ''}`;
291
+ if (ref.status === 'ambiguous') return `${where} found on lines ${ref.lines.join(', ')}`;
292
+ if (ref.status === 'missing-file') return `${where} file is gone`;
293
+ return `${where} ${ref.reason}`;
294
+ }
295
+
296
+ function fixRefsCommand(root, asJson) {
297
+ const { changes, left } = fixMapRefs(root);
298
+ if (asJson) {
299
+ console.log(JSON.stringify({ ok: true, action: 'doc-health fix-refs', changes, left }, null, 2));
300
+ return 0;
301
+ }
302
+ const lines = [changes.length ? `map refs: moved ${changes.length}` : 'map refs: nothing to move.'];
303
+ for (const change of changes) lines.push(` MAP line ${change.map_line}: ${change.from} -> ${change.to} (${change.symbol})`);
304
+ if (left.length) {
305
+ lines.push(`left for a human: ${left.length}`);
306
+ for (const ref of left) lines.push(` ${describeLeft(ref)}`);
307
+ }
308
+ console.log(lines.join('\n'));
309
+ return 0;
310
+ }
311
+
275
312
  function renderDocHealth(payload) {
276
313
  if (!payload.ok) return payload.message;
277
314
  const { boot_load: boot, map_coverage: map, lookup_hops: lookup, staleness, overall } = payload;
@@ -286,6 +323,8 @@ function renderDocHealth(payload) {
286
323
  ['routing rows', map.rows, map.rows], ['existing paths', map.existing, map.paths],
287
324
  ['features mentioned', map.features.mentioned, map.features.total], ['members mentioned', map.members.mentioned, map.members.total],
288
325
  ]),
326
+ refsLine(map.refs),
327
+ ...(map.refs.moved ? ['run atris doc-health --fix-refs to update the moved ones.'] : []),
289
328
  '', 'lookup hops',
290
329
  ];
291
330
  if (lookup.missing) {
@@ -315,9 +354,18 @@ function renderDocHealth(payload) {
315
354
 
316
355
  function docHealthCommand(args = [], options = {}) {
317
356
  if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
318
- console.log('usage: atris doc-health [--json] [--questions <path>]');
357
+ console.log('usage: atris doc-health [--json] [--questions <path>] [--fix-refs]');
358
+ console.log(' --fix-refs rewrite map line refs whose code moved; list the ones a human must fix');
319
359
  return 0;
320
360
  }
361
+ if (args.includes('--fix-refs')) {
362
+ const root = resolveWorkspaceRoot(options.cwd || process.cwd());
363
+ if (!stat(path.join(root, 'atris', 'MAP.md'))?.isFile()) {
364
+ console.log('no atris/MAP.md in this workspace.');
365
+ return 1;
366
+ }
367
+ return fixRefsCommand(root, args.includes('--json'));
368
+ }
321
369
  const index = args.indexOf('--questions');
322
370
  const questions = index >= 0 && args[index + 1] && !args[index + 1].startsWith('--') ? args[index + 1]
323
371
  : args.find(arg => arg.startsWith('--questions='))?.slice('--questions='.length);
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { spawnSync } = require('child_process');
4
+ const { BACKEND_ROOT_HINT, resolveBackendRoot } = require('../utils/backend-root');
4
5
  const { runTaskOnce } = require('./autopilot');
5
6
  const {
6
7
  appendResultsRow,
@@ -391,7 +392,7 @@ function parseRunOptions(args) {
391
392
  const options = {
392
393
  dryRun: false,
393
394
  verbose: false,
394
- backendPath: path.resolve(process.cwd(), '../atrisos-backend'),
395
+ backendPath: null,
395
396
  interventions: [],
396
397
  };
397
398
 
@@ -527,8 +528,10 @@ function buildBenchmarkArtifact(name, packDir, options) {
527
528
  'atris/features/endstate/contract.md',
528
529
  'atris/features/endstate/artifact-schema.json',
529
530
  path.relative(cwd, path.join(packDir, 'program.md')),
530
- '../atrisos-backend/atris/MAP.md',
531
- '../atrisos-backend/atris/TODO.md',
531
+ ...(backendPath ? [
532
+ path.relative(cwd, path.join(backendPath, 'atris', 'MAP.md')),
533
+ path.relative(cwd, path.join(backendPath, 'atris', 'TODO.md')),
534
+ ] : []),
532
535
  ],
533
536
  contextNote: [
534
537
  `Project Endstate track: ${track}`,
@@ -555,7 +558,7 @@ function buildBenchmarkArtifact(name, packDir, options) {
555
558
  ? []
556
559
  : [
557
560
  ...collectChangedFiles(cwd, beforeCli, afterCli),
558
- ...collectChangedFiles(backendPath, beforeBackend, afterBackend, '../atrisos-backend/'),
561
+ ...(backendPath ? collectChangedFiles(backendPath, beforeBackend, afterBackend, `${path.relative(cwd, backendPath)}/`) : []),
559
562
  ];
560
563
  const wikiAfter = readTextIfExists(path.join(cwd, 'atris', 'wiki', 'STATUS.md'));
561
564
 
@@ -637,6 +640,12 @@ function experimentsRun(name, ...args) {
637
640
  return;
638
641
  }
639
642
 
643
+ if (!options.backendPath) options.backendPath = resolveBackendRoot();
644
+ if (!options.backendPath) {
645
+ console.error(BACKEND_ROOT_HINT);
646
+ process.exit(1);
647
+ }
648
+
640
649
  const schemaPath = path.join(process.cwd(), 'atris', 'features', 'endstate', 'artifact-schema.json');
641
650
  const contractPath = path.join(process.cwd(), 'atris', 'features', 'endstate', 'contract.md');
642
651
  if (!fs.existsSync(schemaPath) || !fs.existsSync(contractPath)) {
@@ -37,7 +37,7 @@ const rsi = require('../lib/rsi-record');
37
37
  * writes. The path sent to the backend is left untouched, a remote tick
38
38
  * may target a server-side `~/...` workspace the server expands itself, but
39
39
  * local scorecard/journal writes must never create a literal `~` directory.
40
- * (Surfaced by a live plan tick whose `~/arena/...` arg wrote junk locally.)
40
+ * (Surfaced by a live plan tick whose home-relative path wrote junk locally.)
41
41
  */
42
42
  function expandHome(p) {
43
43
  if (typeof p === 'string' && (p === '~' || p.startsWith('~/'))) {
@@ -35,6 +35,7 @@ const os = require('os');
35
35
  const path = require('path');
36
36
  const readline = require('readline');
37
37
  const { spawnSync } = require('child_process');
38
+ const { sleepSync } = require('../lib/sleep-sync');
38
39
 
39
40
  const CALENDAR_CACHE_PATH = path.join(os.homedir(), '.atris', 'calendar-events-cache.json');
40
41
  const GMAIL_CONNECT_POLL_MS = 3000;
@@ -1812,10 +1813,6 @@ function writeImessageLookupCache(cache) {
1812
1813
  return cachePath;
1813
1814
  }
1814
1815
 
1815
- function sleepMs(ms) {
1816
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1817
- }
1818
-
1819
1816
  function readLatestOutgoingImessage(handle, sinceMs) {
1820
1817
  const chatDb = path.join(os.homedir(), 'Library', 'Messages', 'chat.db');
1821
1818
  const sinceUnix = Math.max(0, Math.floor(Number(sinceMs || Date.now()) / 1000) - 5);
@@ -1873,7 +1870,7 @@ function imessageVerifyLatestOutgoing(handle, sinceMs, options = {}) {
1873
1870
  && !(latest.is_sent || latest.is_delivered || latest.is_finished)
1874
1871
  && Date.now() - startedAt < timeoutMs
1875
1872
  ) {
1876
- sleepMs(intervalMs);
1873
+ sleepSync(intervalMs);
1877
1874
  latest = readLatestOutgoingImessage(handle, sinceMs);
1878
1875
  }
1879
1876
  return {
package/commands/learn.js CHANGED
@@ -460,7 +460,7 @@ function harvestFromJournals(deps = {}) {
460
460
  const lines = notesMatch[1].trim().split('\n').filter(l => l.startsWith('- '));
461
461
  for (const line of lines) {
462
462
  // Strip bullet and optional timestamp prefix
463
- const insight = line.replace(/^- (\d{2}:\d{2} \u2014 )?/, '').trim();
463
+ const insight = line.replace(/^- (\d{2}:\d{2} [-\u2014] )?/, '').trim();
464
464
  if (leftoverClaimableInsight(insight)) continue;
465
465
  if (insight.length > 10) {
466
466
  candidates.push({ insight, source: path.basename(logPath) });