atris 3.58.7 → 3.59.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 (95) hide show
  1. package/AGENTS.md +3 -2
  2. package/GETTING_STARTED.md +2 -2
  3. package/PERSONA.md +11 -11
  4. package/README.md +1 -1
  5. package/atris/GEMINI.md +7 -6
  6. package/atris/PERSONA.md +4 -4
  7. package/atris/features/README.md +77 -6
  8. package/atris/skills/README.md +44 -15
  9. package/atris/skills/aeo/SKILL.md +10 -10
  10. package/atris/skills/atris/SKILL.md +1 -1
  11. package/atris/skills/engines/SKILL.md +69 -50
  12. package/atris/skills/meta/SKILL.md +4 -4
  13. package/bin/atris.js +8 -6
  14. package/commands/aeo.js +9 -12
  15. package/commands/analytics.js +8 -11
  16. package/commands/autopilot.js +79 -8
  17. package/commands/avail.js +1 -1
  18. package/commands/brain.js +3 -1
  19. package/commands/business.js +13 -8
  20. package/commands/clean.js +17 -0
  21. package/commands/close.js +2 -1
  22. package/commands/computer.js +15 -10
  23. package/commands/context-sync.js +3 -3
  24. package/commands/doc-health.js +55 -2
  25. package/commands/engine.js +384 -4
  26. package/commands/experiments.js +13 -4
  27. package/commands/host.js +53 -0
  28. package/commands/improve.js +1 -1
  29. package/commands/integrations.js +2 -5
  30. package/commands/learn.js +1 -1
  31. package/commands/member.js +231 -53
  32. package/commands/mission.js +129 -19
  33. package/commands/one-lap.js +58 -3
  34. package/commands/pack.js +2 -2
  35. package/commands/probe.js +11 -5
  36. package/commands/pull.js +1 -1
  37. package/commands/push.js +1 -1
  38. package/commands/rainmaker.js +3 -16
  39. package/commands/run.js +4 -1
  40. package/commands/scout.js +1 -1
  41. package/commands/search.js +5 -3
  42. package/commands/spaceship.js +1 -1
  43. package/commands/stream.js +27 -2
  44. package/commands/study.js +15 -9
  45. package/commands/sync.js +1 -1
  46. package/commands/task.js +13 -2
  47. package/commands/unknowns.js +5 -0
  48. package/commands/verify.js +12 -4
  49. package/commands/visualize.js +20 -3
  50. package/commands/workflow.js +23 -7
  51. package/lib/codex-flight.js +28 -5
  52. package/lib/dispatch-scout.js +31 -22
  53. package/lib/engine-registry.js +1482 -13
  54. package/lib/feedback-triage.js +2 -0
  55. package/lib/fleet.js +111 -12
  56. package/lib/host.js +336 -0
  57. package/lib/improve-vitals-html.js +3 -2
  58. package/lib/known-commands.js +1 -1
  59. package/lib/lesson-contradiction.js +2 -2
  60. package/lib/map-refs.js +226 -0
  61. package/lib/member-engine.js +253 -0
  62. package/lib/mission-room.js +4 -4
  63. package/lib/noninteractive.js +15 -4
  64. package/lib/policy-lessons.js +1 -1
  65. package/lib/receipt-block.js +137 -28
  66. package/lib/roster-markdown.js +206 -0
  67. package/lib/roster-models.js +179 -0
  68. package/lib/runner-command.js +95 -15
  69. package/lib/scorecard.js +4 -4
  70. package/lib/self-drive.js +9 -1
  71. package/lib/sleep-sync.js +11 -0
  72. package/lib/state-detection.js +28 -9
  73. package/lib/task-db.js +3 -2
  74. package/lib/todo-fallback.js +1 -1
  75. package/lib/wish-audit.js +10 -6
  76. package/lib/wish-delegate.js +29 -5
  77. package/mcp/atris-mcp/index.mjs +77 -24
  78. package/package.json +2 -5
  79. package/scripts/det/det.js +2 -2
  80. package/scripts/det/pr-description.js +2 -2
  81. package/scripts/det/test.js +2 -2
  82. package/templates/members/host/MEMBER.md +23 -0
  83. package/templates/members/host/MISSION.md +11 -0
  84. package/templates/members/host/SOUL.md +7 -0
  85. package/templates/members/host/context/why.md +3 -0
  86. package/templates/members/host/goals.json +5 -0
  87. package/templates/members/host/goals.md +3 -0
  88. package/templates/members/host/skills/host/SKILL.md +13 -0
  89. package/utils/api.js +1 -1
  90. package/utils/auth.js +17 -7
  91. package/utils/backend-root.js +31 -0
  92. package/utils/config.js +5 -4
  93. package/utils/owner-identity.js +61 -0
  94. package/atris/policies/LESSONS.md +0 -13
  95. package/atris/skills/autopilot/atris-autopilot.md +0 -71
@@ -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/clean.js CHANGED
@@ -358,9 +358,26 @@ function findStaleTasks(atrisDir) {
358
358
  * Detects both out-of-bounds AND drift (symbol moved to different line)
359
359
  * Returns { healed: number, unhealable: array }
360
360
  */
361
+ // The boot map routes; atris/refs/MAP-NOTES.md holds the deep file:line refs.
362
+ // Heal both. Entries from the notes file carry `map` so reports can say where.
363
+ const MAP_NOTES_FILE = path.join('refs', 'MAP-NOTES.md');
364
+
361
365
  function healBrokenMapRefs(cwd, atrisDir, dryRun = false, homeDir = os.homedir()) {
362
366
  const mapFile = path.join(atrisDir, 'MAP.md');
363
367
  if (!fs.existsSync(mapFile)) return { healed: 0, unhealable: [] };
368
+ const result = healMapFileRefs(cwd, mapFile, dryRun, homeDir);
369
+ const notesFile = path.join(atrisDir, MAP_NOTES_FILE);
370
+ if (!fs.existsSync(notesFile)) return result;
371
+ const notes = healMapFileRefs(cwd, notesFile, dryRun, homeDir);
372
+ const map = path.relative(cwd, notesFile).split(path.sep).join('/');
373
+ return {
374
+ healed: result.healed + notes.healed,
375
+ unhealable: [...result.unhealable, ...notes.unhealable.map((entry) => ({ ...entry, map }))],
376
+ replacements: [...result.replacements, ...notes.replacements.map((entry) => ({ ...entry, map }))],
377
+ };
378
+ }
379
+
380
+ function healMapFileRefs(cwd, mapFile, dryRun, homeDir) {
364
381
 
365
382
  let mapContent = fs.readFileSync(mapFile, 'utf8');
366
383
  const unhealable = [];
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 { checkMapDocs, fixMapDocs } = 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,18 @@ 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
+ // Line refs are checked in the boot map and in the notes file it points to.
80
+ const { refs: all, ...refs } = checkMapDocs(root, text);
81
+ refs.flagged = all.filter(ref => ref.status !== 'ok').map(ref => ({
82
+ doc: ref.doc, map_line: ref.map_line, ref: ref.raw, symbol: ref.symbol, status: ref.status,
83
+ ...(ref.line ? { line: ref.line } : {}), ...(ref.lines ? { lines: ref.lines } : {}),
84
+ ...(ref.reason ? { reason: ref.reason } : {}),
85
+ }));
77
86
  return {
78
87
  rows, paths: files.length, existing, files,
79
- score: files.length ? existing / files.length : 0,
88
+ // A path that exists but a line ref that points at the wrong code is still a wrong map.
89
+ path_score: pathScore, refs, score: pathScore * refs.score,
80
90
  features: folderCoverage(text, 'atris/features', featureNames),
81
91
  members: folderCoverage(text, 'atris/team', memberNames),
82
92
  };
@@ -272,6 +282,38 @@ function table(headers, rows) {
272
282
  return cells.map(row => row.map((cell, i) => cell.padEnd(widths[i])).join(' ').trimEnd());
273
283
  }
274
284
 
285
+ function refsLine(refs) {
286
+ const gone = refs.missing + refs.missing_file;
287
+ return `map refs: ${refs.ok} right, ${refs.moved} moved, ${refs.ambiguous} unclear, ${gone} gone`;
288
+ }
289
+
290
+ function docLine(ref) {
291
+ return `${ref.doc === 'atris/MAP.md' ? 'MAP' : ref.doc} line ${ref.map_line}`;
292
+ }
293
+
294
+ function describeLeft(ref) {
295
+ const where = `${docLine(ref)}: ${ref.raw}${ref.symbol ? ` (${ref.symbol})` : ''}`;
296
+ if (ref.status === 'ambiguous') return `${where} found on lines ${ref.lines.join(', ')}`;
297
+ if (ref.status === 'missing-file') return `${where} file is gone`;
298
+ return `${where} ${ref.reason}`;
299
+ }
300
+
301
+ function fixRefsCommand(root, asJson) {
302
+ const { changes, left } = fixMapDocs(root);
303
+ if (asJson) {
304
+ console.log(JSON.stringify({ ok: true, action: 'doc-health fix-refs', changes, left }, null, 2));
305
+ return 0;
306
+ }
307
+ const lines = [changes.length ? `map refs: moved ${changes.length}` : 'map refs: nothing to move.'];
308
+ for (const change of changes) lines.push(` ${docLine(change)}: ${change.from} -> ${change.to} (${change.symbol})`);
309
+ if (left.length) {
310
+ lines.push(`left for a human: ${left.length}`);
311
+ for (const ref of left) lines.push(` ${describeLeft(ref)}`);
312
+ }
313
+ console.log(lines.join('\n'));
314
+ return 0;
315
+ }
316
+
275
317
  function renderDocHealth(payload) {
276
318
  if (!payload.ok) return payload.message;
277
319
  const { boot_load: boot, map_coverage: map, lookup_hops: lookup, staleness, overall } = payload;
@@ -286,6 +328,8 @@ function renderDocHealth(payload) {
286
328
  ['routing rows', map.rows, map.rows], ['existing paths', map.existing, map.paths],
287
329
  ['features mentioned', map.features.mentioned, map.features.total], ['members mentioned', map.members.mentioned, map.members.total],
288
330
  ]),
331
+ refsLine(map.refs),
332
+ ...(map.refs.moved ? ['run atris doc-health --fix-refs to update the moved ones.'] : []),
289
333
  '', 'lookup hops',
290
334
  ];
291
335
  if (lookup.missing) {
@@ -315,9 +359,18 @@ function renderDocHealth(payload) {
315
359
 
316
360
  function docHealthCommand(args = [], options = {}) {
317
361
  if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
318
- console.log('usage: atris doc-health [--json] [--questions <path>]');
362
+ console.log('usage: atris doc-health [--json] [--questions <path>] [--fix-refs]');
363
+ console.log(' --fix-refs rewrite map line refs whose code moved; list the ones a human must fix');
319
364
  return 0;
320
365
  }
366
+ if (args.includes('--fix-refs')) {
367
+ const root = resolveWorkspaceRoot(options.cwd || process.cwd());
368
+ if (!stat(path.join(root, 'atris', 'MAP.md'))?.isFile()) {
369
+ console.log('no atris/MAP.md in this workspace.');
370
+ return 1;
371
+ }
372
+ return fixRefsCommand(root, args.includes('--json'));
373
+ }
321
374
  const index = args.indexOf('--questions');
322
375
  const questions = index >= 0 && args[index + 1] && !args[index + 1].startsWith('--') ? args[index + 1]
323
376
  : args.find(arg => arg.startsWith('--questions='))?.slice('--questions='.length);