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
package/commands/sync.js CHANGED
@@ -615,7 +615,7 @@ Detect: Project has \`atris/\` folder with MAP.md, TODO.md, PERSONA.md
615
615
  Commands: plan → do → review
616
616
 
617
617
  Key behaviors:
618
- - Read PERSONA.md (3-4 sentences, ASCII visuals)
618
+ - Read PERSONA.md (MAP.md first, plain words, lead with the outcome)
619
619
  - Check MAP.md for file:line refs
620
620
  - Use \`atris task\` for claims, proof, ready, and accept
621
621
  - Give every task a plain first layer: what changes, why it matters, and what
package/commands/task.js CHANGED
@@ -634,6 +634,12 @@ function goldenPathMissionXpTask(task) {
634
634
  && /\b(?:golden[- ]path|zero[- ]knowledge|zero[- ]papercuts?|fresh[- ](?:laptop|environment|install|home)|self[- ]landed)\b/i.test(text);
635
635
  }
636
636
 
637
+ // A receipt that cannot be read drops out of review evidence; name it under
638
+ // ATRIS_DEBUG so the gap is findable, matching lib/todo.js.
639
+ function debugUnreadableReceipt(rel, err) {
640
+ if (process.env.ATRIS_DEBUG) console.error(`[task] receipt unreadable, skipped: ${rel}: ${err && err.message}`);
641
+ }
642
+
637
643
  function receiptTextForProof(proof, root = process.cwd()) {
638
644
  const chunks = [];
639
645
  const pattern = new RegExp(RECEIPT_PATH_PATTERN.source, 'g');
@@ -652,7 +658,9 @@ function receiptTextForProof(proof, root = process.cwd()) {
652
658
  last_landing: parsed.last_landing || null,
653
659
  summary: parsed.summary || null,
654
660
  }).slice(0, 12000));
655
- } catch {}
661
+ } catch (err) {
662
+ debugUnreadableReceipt(rel, err);
663
+ }
656
664
  }
657
665
  return chunks.join(' ');
658
666
  }
@@ -672,7 +680,10 @@ function missionReceiptResultForProof(task, proof, root = process.cwd()) {
672
680
  try {
673
681
  const parsed = JSON.parse(fs.readFileSync(path.resolve(root, rel), 'utf8'));
674
682
  landing = parsed?.result?.landing || parsed?.landing || parsed?.last_landing || null;
675
- } catch { continue; }
683
+ } catch (err) {
684
+ debugUnreadableReceipt(rel, err);
685
+ continue;
686
+ }
676
687
  const changed = String(landing?.changed || landing?.happened || '').replace(/\s+/g, ' ').trim();
677
688
  if (!changed) continue;
678
689
  if (/\brecorded tick \d+\.?$/i.test(changed) || /^recorded a proof heartbeat\b/i.test(changed)) continue;
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const { mapNotesPath } = require('../lib/map-refs');
5
6
  const { spawnSync } = require('child_process');
6
7
  const taskDb = require('../lib/task-db');
7
8
  const {
@@ -167,6 +168,8 @@ function formatTaskEvents(rows) {
167
168
 
168
169
  function gatherTerritoryContext(root, db) {
169
170
  const map = readFileBestEffort(path.join(root, 'atris', 'MAP.md'), { lines: 100 });
171
+ const notesPath = mapNotesPath(root);
172
+ const notes = notesPath ? readFileBestEffort(path.join(root, notesPath), { lines: 100 }) : '';
170
173
  const lessonsMd = readFileBestEffort(path.join(root, 'atris', 'lessons.md'), { maxChars: READ_LIMIT });
171
174
  const lessonsJson = readFileBestEffort(path.join(root, 'atris', 'lessons.json'), { maxChars: READ_LIMIT });
172
175
  const journals = readRecentJournals(root);
@@ -176,6 +179,7 @@ function gatherTerritoryContext(root, db) {
176
179
  return [
177
180
  section('workspace_root', root),
178
181
  section('atris/MAP.md first 100 lines', map),
182
+ ...(notesPath ? [section(`${notesPath} first 100 lines`, notes)] : []),
179
183
  section('atris/lessons.md', lessonsMd),
180
184
  section('atris/lessons.json', lessonsJson),
181
185
  section('last 3 daily journals', journals),
@@ -614,4 +618,5 @@ module.exports = {
614
618
  unknownsCommand,
615
619
  showHelp,
616
620
  fallbackPayload,
621
+ gatherTerritoryContext,
617
622
  };
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { spawnSync } = require('child_process');
4
4
  const escapeRegExp = require('../lib/escape-regexp');
5
+ const { existingMapDocs } = require('../lib/map-refs');
5
6
 
6
7
  /**
7
8
  * atris verify [task] - Validate work is actually done
@@ -397,13 +398,13 @@ function checkDocsVsChanges(cwd, atrisDir) {
397
398
  return result;
398
399
  }
399
400
 
400
- // Check if significant files are in MAP.md
401
+ // Check if significant files are in MAP.md or the notes file it points to
401
402
  const mapFile = path.join(atrisDir, 'MAP.md');
402
403
  if (!fs.existsSync(mapFile)) {
403
404
  return result;
404
405
  }
405
406
 
406
- const mapContent = fs.readFileSync(mapFile, 'utf8');
407
+ const mapContent = mapDocsText(atrisDir);
407
408
 
408
409
  const significantExtensions = ['.js', '.ts', '.py', '.go', '.rs', '.rb', '.java'];
409
410
  const significantChanges = changedFiles.filter(f =>
@@ -547,6 +548,12 @@ function verifyChange(cwd, change) {
547
548
  };
548
549
  }
549
550
 
551
+ // A file counts as documented when the boot map or atris/refs/MAP-NOTES.md names it.
552
+ function mapDocsText(atrisDir) {
553
+ const root = path.dirname(atrisDir);
554
+ return existingMapDocs(root).map((doc) => fs.readFileSync(path.join(root, doc), 'utf8')).join('\n');
555
+ }
556
+
550
557
  /**
551
558
  * Check if MAP.md documents the expected files
552
559
  */
@@ -556,7 +563,7 @@ function checkMapForFiles(atrisDir, changes) {
556
563
  return { documented: false };
557
564
  }
558
565
 
559
- const content = fs.readFileSync(mapFile, 'utf8');
566
+ const content = mapDocsText(atrisDir);
560
567
  const fileChanges = changes.filter(c => c.type === 'file');
561
568
 
562
569
  if (fileChanges.length === 0) {
@@ -725,5 +732,6 @@ module.exports = {
725
732
  escapeRegExp,
726
733
  mapCoverage,
727
734
  isPathCovered,
728
- normalizeMapPath
735
+ normalizeMapPath,
736
+ checkMapForFiles,
729
737
  };
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const { mapNotesPath } = require('../lib/map-refs');
3
4
  const { spawnSync } = require('child_process');
4
5
  const { getLogPath } = require('../lib/journal');
5
6
  const { ensureValidCredentials } = require('../utils/auth');
@@ -153,14 +154,16 @@ function readBusinessMeta(cwd = process.cwd()) {
153
154
  }
154
155
  }
155
156
 
157
+ function promptWords(prompt) {
158
+ return new Set(String(prompt || '').toLowerCase().split(/[^a-z0-9]+/).filter(w => w.length >= 4));
159
+ }
160
+
156
161
  function findRelevantContextFiles(cwd, prompt) {
157
162
  const roots = [
158
163
  path.join(cwd, 'atris', 'context'),
159
164
  path.join(cwd, 'atris', 'wiki'),
160
165
  ];
161
- const words = new Set(
162
- prompt.toLowerCase().split(/[^a-z0-9]+/).filter(w => w.length >= 4)
163
- );
166
+ const words = promptWords(prompt);
164
167
  const files = [];
165
168
 
166
169
  function walk(dir) {
@@ -192,6 +195,17 @@ function findRelevantContextFiles(cwd, prompt) {
192
195
  .slice(0, 4);
193
196
  }
194
197
 
198
+ // The deep map is large: keep only the lines that share a word with the prompt.
199
+ function mapNotesExcerpt(cwd, prompt, maxChars) {
200
+ const notesPath = mapNotesPath(cwd);
201
+ if (!notesPath) return '';
202
+ const words = promptWords(prompt);
203
+ if (!words.size) return '';
204
+ const lines = fs.readFileSync(path.join(cwd, notesPath), 'utf8').split(/\r?\n/)
205
+ .filter(line => [...words].some(word => line.toLowerCase().includes(word)));
206
+ return lines.join('\n').slice(0, maxChars);
207
+ }
208
+
195
209
  function collectWorkspaceContext(prompt, cwd = process.cwd()) {
196
210
  const business = readBusinessMeta(cwd);
197
211
  const chunks = [];
@@ -201,6 +215,8 @@ function collectWorkspaceContext(prompt, cwd = process.cwd()) {
201
215
 
202
216
  const mapSnippet = readTextIfExists(path.join(cwd, 'atris', 'MAP.md'), 1400);
203
217
  if (mapSnippet) chunks.push(`MAP excerpt:\n${mapSnippet}`);
218
+ const notesSnippet = mapNotesExcerpt(cwd, prompt, 1400);
219
+ if (notesSnippet) chunks.push(`MAP notes excerpt:\n${notesSnippet}`);
204
220
 
205
221
  const todoSnippet = readTextIfExists(path.join(cwd, 'atris', 'TODO.md'), 1000);
206
222
  if (todoSnippet) chunks.push(`TODO excerpt:\n${todoSnippet}`);
@@ -389,4 +405,5 @@ async function visualizeAtris(args = process.argv.slice(3)) {
389
405
 
390
406
  module.exports = {
391
407
  visualizeAtris,
408
+ collectWorkspaceContext,
392
409
  };
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const { mapNotesPath } = require('../lib/map-refs');
3
4
  const { getLogPath } = require('../lib/journal');
4
5
  const {
5
6
  buildFirstMinute,
@@ -21,6 +22,10 @@ const { loadContext } = require('../lib/state-detection');
21
22
  const { buildToolResultBody } = require('../lib/tool-result-encode');
22
23
  const { commitReviewLearning } = require('./learn');
23
24
 
25
+ function reviewLearningLine(timestamp, answer) {
26
+ return `- ${timestamp} - ${String(answer).trim().replace(/\s*\u2014\s*/g, ', ')}`;
27
+ }
28
+
24
29
  function appendReviewLearningToJournal(journalContent, learning) {
25
30
  const text = String(journalContent || '').replace(/\r\n/g, '\n');
26
31
  const note = String(learning || '');
@@ -487,6 +492,19 @@ async function runAtris2Local(userInput, atris2Mode) {
487
492
  }
488
493
  }
489
494
 
495
+ // Where the agent finds code. When the short routing map has a notes file,
496
+ // point line lookups there so the agent does not hunt for refs the map lacks.
497
+ function mapPromptSection(targetDir, cwd = process.cwd()) {
498
+ const mapFile = path.join(targetDir, 'MAP.md');
499
+ if (!fs.existsSync(mapFile)) return '';
500
+ const mapRef = path.relative(cwd, mapFile);
501
+ const notes = mapNotesPath(path.dirname(targetDir));
502
+ if (!notes) return `## MAP.md\nRead this file for file:line references: ${mapRef}\n\n`;
503
+ const notesRef = path.relative(cwd, path.join(path.dirname(targetDir), notes));
504
+ return `## MAP.md\nRead this file to find where work lives: ${mapRef}\n\n`
505
+ + `## MAP-NOTES.md\nRead this file for exact file:line references: ${notesRef}\n\n`;
506
+ }
507
+
490
508
  async function planAtris(userInput = null) {
491
509
  const { loadConfig } = require('../utils/config');
492
510
  const { loadCredentials, ensureValidCredentials } = require('../utils/auth');
@@ -769,9 +787,7 @@ async function planAtris(userInput = null) {
769
787
  systemPrompt += '## PERSONA.md\n' + fs.readFileSync(personaPath, 'utf8') + '\n\n';
770
788
  }
771
789
 
772
- if (mapFileRef) {
773
- systemPrompt += `## MAP.md\nRead this file for file:line references: ${mapFileRef}\n\n`;
774
- }
790
+ systemPrompt += mapPromptSection(targetDir);
775
791
 
776
792
  // Build user prompt with context
777
793
  let userPrompt = `You are the Navigator. Take ideas from Inbox → break them down into perfect, manageable tasks.\n\n`;
@@ -1249,9 +1265,7 @@ async function doAtris() {
1249
1265
  if (persona) {
1250
1266
  systemPrompt += '## PERSONA.md\n' + persona + '\n\n';
1251
1267
  }
1252
- if (mapPath) {
1253
- systemPrompt += `## MAP.md\nRead this file for file:line references: ${mapPath}\n\n`;
1254
- }
1268
+ systemPrompt += mapPromptSection(targetDir);
1255
1269
  if (profile) {
1256
1270
  systemPrompt += `## PROJECT CONTEXT\nType: ${context}\nProfile: ${JSON.stringify(profile, null, 2)}\n\n`;
1257
1271
  }
@@ -1782,7 +1796,7 @@ async function reviewAtris() {
1782
1796
  if (fs.existsSync(logFile)) {
1783
1797
  let journalContent = fs.readFileSync(logFile, 'utf8');
1784
1798
  const timestamp = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
1785
- const learning = `- ${timestamp} \u2014 ${answer.trim()}`;
1799
+ const learning = reviewLearningLine(timestamp, answer);
1786
1800
 
1787
1801
  journalContent = appendReviewLearningToJournal(journalContent, learning);
1788
1802
 
@@ -1894,10 +1908,12 @@ async function executeAgentSDKFast(userInput) {
1894
1908
  }
1895
1909
 
1896
1910
  module.exports = {
1911
+ mapPromptSection,
1897
1912
  planAtris,
1898
1913
  doAtris,
1899
1914
  reviewAtris,
1900
1915
  appendReviewLearningToJournal,
1916
+ reviewLearningLine,
1901
1917
  renderReviewMinute,
1902
1918
  executorAgentPrompt,
1903
1919
  executorDispatchForTask,
@@ -1,10 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
+ const os = require('os');
4
5
  const path = require('path');
5
6
  const { spawn, spawnSync } = require('child_process');
6
7
 
7
- const CODEX_COMPANION = '/Users/keshavrao/.claude/plugins/cache/openai-codex/codex/1.0.4/scripts/codex-companion.mjs';
8
+ // Worktree flights need full file access to edit and ship the requested repo.
9
+ const FLIGHT_SANDBOX = 'danger-full-access';
8
10
  const CONTRACT = "CRITICAL: complete the entire job in this single run; never end your reply on a plan. Do NOT run any atris boot/brain/state commands; the only atris commands allowed are 'atris worktree guard' and 'atris worktree ship'.";
9
11
  const COAUTHOR = 'Co-authored-by: Atris <299057014+atris-builder[bot]@users.noreply.github.com>';
10
12
 
@@ -96,12 +98,30 @@ function parseTaskId(stdout) {
96
98
  return asMatch ? asMatch[1] : '';
97
99
  }
98
100
 
99
- function spawnCodex(worktreePath, prompt) {
101
+ function resolveCodexCompanion() {
102
+ if (process.env.ATRIS_CODEX_COMPANION) {
103
+ const override = path.resolve(process.env.ATRIS_CODEX_COMPANION);
104
+ return fs.existsSync(override) && fs.statSync(override).isFile() ? override : null;
105
+ }
106
+ const cache = path.join(os.homedir(), '.claude', 'plugins', 'cache', 'openai-codex', 'codex');
107
+ if (!fs.existsSync(cache)) return null;
108
+ const versions = fs.readdirSync(cache, { withFileTypes: true })
109
+ .filter((entry) => entry.isDirectory())
110
+ .map((entry) => entry.name)
111
+ .sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
112
+ for (const version of versions) {
113
+ const script = path.join(cache, version, 'scripts', 'codex-companion.mjs');
114
+ if (fs.existsSync(script) && fs.statSync(script).isFile()) return script;
115
+ }
116
+ return null;
117
+ }
118
+
119
+ function spawnCodex(worktreePath, prompt, companion) {
100
120
  return new Promise((resolve, reject) => {
101
- const argv = [CODEX_COMPANION, 'task', '--background', '--write', prompt];
121
+ const argv = [companion, 'task', '--background', '--write', prompt];
102
122
  const child = spawn('node', argv, {
103
123
  cwd: worktreePath,
104
- env: { ...process.env, CODEX_COMPANION_SANDBOX: 'danger-full-access' },
124
+ env: { ...process.env, CODEX_COMPANION_SANDBOX: FLIGHT_SANDBOX },
105
125
  stdio: ['ignore', 'pipe', 'pipe'],
106
126
  });
107
127
  let stdout = '';
@@ -143,12 +163,15 @@ async function dispatchCodexFlight({ repoPath, slug, brief, verifyCmd, dryRun =
143
163
  return { dryRun: true, prompt };
144
164
  }
145
165
 
166
+ const companion = resolveCodexCompanion();
167
+ if (!companion) throw new Error('codex companion not found. set ATRIS_CODEX_COMPANION to its script path.');
146
168
  runWorktreeAdd(repo.repoPath, branch, worktreePath);
147
- const task = await spawnCodex(worktreePath, prompt);
169
+ const task = await spawnCodex(worktreePath, prompt, companion);
148
170
  return { taskId: task.taskId, worktreePath, branch };
149
171
  }
150
172
 
151
173
  module.exports = {
152
174
  dispatchCodexFlight,
153
175
  buildPrompt,
176
+ resolveCodexCompanion,
154
177
  };
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { spawnSync } = require('child_process');
6
+ const { MAP_REF_DOCS, existingMapDocs } = require('./map-refs');
6
7
  const {
7
8
  buildReadOnlyEngineInvocation,
8
9
  runAskProcess,
@@ -91,9 +92,8 @@ function seedScoutContext({ task, worktreePath }) {
91
92
  const trackedList = trackedFiles(worktreePath);
92
93
  const tracked = new Set(trackedList);
93
94
  const keywords = titleKeywords(task && task.title);
94
- const mapPath = 'atris/MAP.md';
95
- let mapText = '';
96
- try { mapText = fs.readFileSync(path.join(worktreePath, mapPath), 'utf8'); } catch {}
95
+ const mapPath = MAP_REF_DOCS[0];
96
+ const mapTexts = readMapTexts(worktreePath);
97
97
 
98
98
  const scores = new Map();
99
99
  const gotchaCandidates = [];
@@ -103,18 +103,21 @@ function seedScoutContext({ task, worktreePath }) {
103
103
  };
104
104
  if (tracked.has(mapPath)) addScore(mapPath, 1);
105
105
 
106
- const mapLines = mapText.split(/\r?\n/);
107
- for (let index = 0; index < mapLines.length; index += 1) {
108
- const line = mapLines[index];
109
- const lower = line.toLowerCase();
110
- const matches = keywords.filter((keyword) => lower.includes(keyword));
111
- const refs = mapPathRefs(line, tracked);
112
- for (const ref of refs) {
113
- const pathMatches = keywords.filter((keyword) => ref.toLowerCase().includes(keyword)).length;
114
- if (matches.length || pathMatches) addScore(ref, 80 + (matches.length * 5) + (pathMatches * 8));
115
- }
116
- if (matches.length && line.trim() && line.trim().length <= 300 && gotchaCandidates.length < 16) {
117
- gotchaCandidates.push({ line: index + 1, text: line.trim() });
106
+ // The boot map routes; the notes file holds the deep file:line refs. Read both.
107
+ for (const [doc, mapText] of mapTexts) {
108
+ const mapLines = mapText.split(/\r?\n/);
109
+ for (let index = 0; index < mapLines.length; index += 1) {
110
+ const line = mapLines[index];
111
+ const lower = line.toLowerCase();
112
+ const matches = keywords.filter((keyword) => lower.includes(keyword));
113
+ const refs = mapPathRefs(line, tracked);
114
+ for (const ref of refs) {
115
+ const pathMatches = keywords.filter((keyword) => ref.toLowerCase().includes(keyword)).length;
116
+ if (matches.length || pathMatches) addScore(ref, 80 + (matches.length * 5) + (pathMatches * 8));
117
+ }
118
+ if (matches.length && line.trim() && line.trim().length <= 300 && gotchaCandidates.length < 16) {
119
+ gotchaCandidates.push({ ...(doc === mapPath ? {} : { file: doc }), line: index + 1, text: line.trim() });
120
+ }
118
121
  }
119
122
  }
120
123
 
@@ -268,15 +271,22 @@ function verifyHit(hit, { worktreePath, tracked, allowed }) {
268
271
  return { path: citedPath, line: nearest.line, excerpt, why };
269
272
  }
270
273
 
271
- function verifyMapGotcha(gotcha, mapText) {
274
+ // Map text by doc, boot map first. A missing doc is simply absent.
275
+ function readMapTexts(root) {
276
+ return new Map(existingMapDocs(root).map((doc) => [doc, fs.readFileSync(path.join(root, doc), 'utf8')]));
277
+ }
278
+
279
+ function verifyMapGotcha(gotcha, mapTexts) {
272
280
  if (!gotcha || typeof gotcha !== 'object' || Array.isArray(gotcha)) return null;
273
281
  const text = String(gotcha.text || '').trim();
274
282
  const citedLine = Number(gotcha.line);
283
+ const file = gotcha.file === undefined ? MAP_REF_DOCS[0] : String(gotcha.file);
275
284
  if (!text || text.length > 300 || !Number.isInteger(citedLine) || citedLine < 1) return null;
276
- const lines = mapText.split(/\r?\n/);
285
+ if (!mapTexts.has(file)) return null;
286
+ const lines = mapTexts.get(file).split(/\r?\n/);
277
287
  const actualLine = lines.findIndex((line) => line.trim() === text) + 1;
278
288
  if (!actualLine) return null;
279
- return { line: actualLine, text };
289
+ return { ...(file === MAP_REF_DOCS[0] ? {} : { file }), line: actualLine, text };
280
290
  }
281
291
 
282
292
  function verifyScoutPack(rawPack, { task, worktreePath, allowedFiles, expectedCommit = '' }) {
@@ -301,11 +311,10 @@ function verifyScoutPack(rawPack, { task, worktreePath, allowedFiles, expectedCo
301
311
  });
302
312
  if (hits.length < 2) return null;
303
313
 
304
- let mapText = '';
305
- try { mapText = fs.readFileSync(path.join(worktreePath, 'atris', 'MAP.md'), 'utf8'); } catch {}
314
+ const mapTexts = readMapTexts(worktreePath);
306
315
  const mapGotchas = (Array.isArray(rawPack.map_gotchas) ? rawPack.map_gotchas : [])
307
316
  .slice(0, SCOUT_MAX_MAP_GOTCHAS)
308
- .map((gotcha) => verifyMapGotcha(gotcha, mapText))
317
+ .map((gotcha) => verifyMapGotcha(gotcha, mapTexts))
309
318
  .filter(Boolean);
310
319
  const notChecked = (Array.isArray(rawPack.not_checked) ? rawPack.not_checked : [])
311
320
  .map((entry) => String(entry || '').trim())
@@ -360,7 +369,7 @@ function renderVerifiedScoutBlock(pack, { worktreePath }) {
360
369
  }
361
370
  if (pack.map_gotchas.length) {
362
371
  lines.push('MAP gotchas:');
363
- lines.push(...pack.map_gotchas.map((gotcha) => `- MAP line ${gotcha.line}: ${gotcha.text}`));
372
+ lines.push(...pack.map_gotchas.map((gotcha) => `- ${gotcha.file || 'MAP'} line ${gotcha.line}: ${gotcha.text}`));
364
373
  }
365
374
  if (pack.not_checked.length) lines.push(`Not checked: ${pack.not_checked.join('; ')}`);
366
375
  return lines.join('\n');