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/commands/study.js CHANGED
@@ -15,6 +15,7 @@ const fs = require('fs');
15
15
  const path = require('path');
16
16
  const http = require('http');
17
17
  const { spawn, spawnSync } = require('child_process');
18
+ const { resolveBackendRoot } = require('../utils/backend-root');
18
19
 
19
20
  const FEED_URL = 'http://localhost:8777';
20
21
  const FEED_STATS_URL = `${FEED_URL}/api/stats`;
@@ -91,15 +92,19 @@ function parseArgs(argv = []) {
91
92
  }
92
93
 
93
94
  function resolveStudyRoot(cwd = process.cwd()) {
94
- const candidates = [
95
- process.env.ATRIS_STUDY_ROOT,
96
- process.env.ATRIS_BACKEND_ROOT,
97
- path.join(path.resolve(cwd), 'atris', 'study'),
98
- ].filter(Boolean);
99
- for (const root of candidates) {
100
- const resolved = path.resolve(String(root));
101
- if (fs.existsSync(resolved)) return resolved;
102
- }
95
+ if (process.env.ATRIS_STUDY_ROOT) {
96
+ const explicit = path.resolve(process.env.ATRIS_STUDY_ROOT);
97
+ if (fs.existsSync(explicit) && fs.statSync(explicit).isDirectory()) return explicit;
98
+ }
99
+ // Same order as before: an explicitly set backend root wins over the local folder.
100
+ if (process.env.ATRIS_BACKEND_ROOT) {
101
+ const envBackend = path.resolve(process.env.ATRIS_BACKEND_ROOT);
102
+ if (fs.existsSync(envBackend)) return envBackend;
103
+ }
104
+ const local = path.join(path.resolve(cwd), 'atris', 'study');
105
+ if (fs.existsSync(local) && fs.statSync(local).isDirectory()) return local;
106
+ const backend = resolveBackendRoot(cwd);
107
+ if (backend) return backend;
103
108
  return null;
104
109
  }
105
110
 
@@ -697,6 +702,7 @@ async function run(argv = []) {
697
702
 
698
703
  module.exports = {
699
704
  run,
705
+ resolveStudyRoot,
700
706
  parseArgs,
701
707
  showHelp,
702
708
  wrapText,
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;
@@ -21,6 +21,10 @@ const { loadContext } = require('../lib/state-detection');
21
21
  const { buildToolResultBody } = require('../lib/tool-result-encode');
22
22
  const { commitReviewLearning } = require('./learn');
23
23
 
24
+ function reviewLearningLine(timestamp, answer) {
25
+ return `- ${timestamp} - ${String(answer).trim().replace(/\s*\u2014\s*/g, ', ')}`;
26
+ }
27
+
24
28
  function appendReviewLearningToJournal(journalContent, learning) {
25
29
  const text = String(journalContent || '').replace(/\r\n/g, '\n');
26
30
  const note = String(learning || '');
@@ -1782,7 +1786,7 @@ async function reviewAtris() {
1782
1786
  if (fs.existsSync(logFile)) {
1783
1787
  let journalContent = fs.readFileSync(logFile, 'utf8');
1784
1788
  const timestamp = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
1785
- const learning = `- ${timestamp} \u2014 ${answer.trim()}`;
1789
+ const learning = reviewLearningLine(timestamp, answer);
1786
1790
 
1787
1791
  journalContent = appendReviewLearningToJournal(journalContent, learning);
1788
1792
 
@@ -1898,6 +1902,7 @@ module.exports = {
1898
1902
  doAtris,
1899
1903
  reviewAtris,
1900
1904
  appendReviewLearningToJournal,
1905
+ reviewLearningLine,
1901
1906
  renderReviewMinute,
1902
1907
  executorAgentPrompt,
1903
1908
  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
  };
package/lib/fleet.js CHANGED
@@ -33,6 +33,8 @@ const {
33
33
  const { listWorktrees, defaultMainlineBase } = require('../commands/worktree');
34
34
  const { isConductorStatusLine } = require('./conductor-artifacts');
35
35
  const { matchLessons } = require('./lesson-preflight');
36
+ const { sleepSync } = require('./sleep-sync');
37
+ const { ownerIdentity } = require('../utils/owner-identity');
36
38
  const { matchTaste } = require('./taste-lessons');
37
39
  const { buildVerifiedScoutPack, appendVerifiedScoutPack } = require('./dispatch-scout');
38
40
  const {
@@ -286,7 +288,7 @@ function terminateTrackedSandbox(stateFile, leaseFile) {
286
288
  for (const pid of tracked.pids) {
287
289
  try { process.kill(pid, signal); } catch {}
288
290
  }
289
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
291
+ sleepSync(100);
290
292
  }
291
293
  }
292
294
 
@@ -1381,6 +1383,7 @@ function reviewOnlyEngineEnvironment(worktreePath, options = {}) {
1381
1383
  throw error;
1382
1384
  }
1383
1385
  const environment = {
1386
+ ATRIS_OWNER_NAMES: [...new Set(ownerIdentity().names)].join(','),
1384
1387
  PATH: `${guardDir}${path.delimiter}${process.env.PATH || ''}`,
1385
1388
  GIT_CONFIG_COUNT: '2',
1386
1389
  GIT_CONFIG_KEY_0: 'remote.origin.pushurl',
@@ -10,6 +10,7 @@
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
12
  const { spawnSync } = require('child_process');
13
+ const { isOwner } = require('../utils/owner-identity');
13
14
 
14
15
  const HTML_RELATIVE_PATH = path.join('.atris', 'state', 'improve-vitals.html');
15
16
 
@@ -51,8 +52,8 @@ function openLoops(cwd, now) {
51
52
  try {
52
53
  const close = require('../commands/close');
53
54
  const flags = close.openFlags(cwd, { now });
54
- const mine = flags.filter((f) => /^(you|me|operator|human|keshav)$/.test(f.owner));
55
- const theirs = flags.filter((f) => !/^(you|me|operator|human|keshav)$/.test(f.owner));
55
+ const mine = flags.filter((f) => isOwner(f.owner));
56
+ const theirs = flags.filter((f) => !isOwner(f.owner));
56
57
  return { mine, theirs };
57
58
  } catch {
58
59
  return { mine: [], theirs: [] };
@@ -22,8 +22,8 @@ function detectLessonContradictions(cwd) {
22
22
  ? fs.readFileSync(lessonsMdPath, 'utf8')
23
23
  : '';
24
24
 
25
- // Format: - **[YYYY-MM-DD] slug**, pass|fail, prose
26
- const lessonRegex = /^-\s+\*\*\[(\d{4}-\d{2}-\d{2})\]\s+([a-z0-9-]+)\*\*\s*\u2014\s*(pass|fail)\s*\u2014\s*(\[resolved\])?\s*(.+)$/m;
25
+ // Format: - **[YYYY-MM-DD] slug** - pass|fail - prose
26
+ const lessonRegex = /^-\s+\*\*\[(\d{4}-\d{2}-\d{2})\]\s+([a-z0-9-]+)\*\*\s*[-\u2014]\s*(pass|fail)\s*[-\u2014]\s*(\[resolved\])?\s*(.+)$/m;
27
27
  const lines = lessonsMdContent.split('\n');
28
28
 
29
29
  const lessonsBySlug = new Map(); // slug -> [{ date, outcome, lineNum, resolved }]
@@ -0,0 +1,183 @@
1
+ 'use strict';
2
+
3
+ // Checks the `path:N` line refs in a map document against the code they point at.
4
+ // A ref may carry a symbol: an identifier written right after it (`path:N` `name`,
5
+ // `path:N` (`name`), or a bare camelCase name) or right before it (`name` (`path:N`)).
6
+ // A name elsewhere in the sentence never binds: map sentences often name a helper
7
+ // the ref does not point at, and binding it would move the ref to the wrong place.
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const WINDOW = 3;
13
+ const IDENT = /^[A-Za-z_$][\w$]*$/;
14
+ const REF = /^(?:\.\/)?([^\s:`<>*|]+):(\d+)(?:-(\d+))?((?:,\d+(?:-\d+)?)*)(?:#\S*)?$/;
15
+
16
+ function escape(text) {
17
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
18
+ }
19
+
20
+ function definitionPatterns(symbol) {
21
+ const s = escape(symbol);
22
+ return [
23
+ new RegExp(`\\bfunction\\s*\\*?\\s*${s}\\s*\\(`),
24
+ new RegExp(`\\b(?:const|let|var)\\s+${s}\\b`),
25
+ new RegExp(`\\bclass\\s+${s}\\b`),
26
+ new RegExp(`\\bdef\\s+${s}\\b`),
27
+ new RegExp(`^\\s*(?:async\\s+)?(?:static\\s+)?(?:get\\s+|set\\s+)?\\*?${s}\\s*\\([^)]*\\)\\s*\\{`),
28
+ new RegExp(`^\\s*['"]?${s}['"]?\\s*:(?!:)`),
29
+ new RegExp(`\\bcase\\s+['"\`]?${s}['"\`]?\\s*:`),
30
+ new RegExp(`\\bexports\\.${s}\\s*=`),
31
+ ];
32
+ }
33
+
34
+ function tokenPattern(symbol, flags = '') {
35
+ return new RegExp(`(?<![\\w$])${escape(symbol)}(?![\\w$])`, flags);
36
+ }
37
+
38
+ function isPathLike(file) {
39
+ return !file.includes('://') && (file.includes('/') || /\.[a-z0-9]+$/i.test(file));
40
+ }
41
+
42
+ // Find every ref in the map, with its map line, column, and optional symbol.
43
+ // Code fences are scanned too: the map keeps prose notes inside its search block.
44
+ function parseRefs(mapText) {
45
+ const refs = [];
46
+ mapText.split(/\r?\n/).forEach((line, index) => {
47
+ const spans = [...line.matchAll(/`([^`\r\n]+)`/g)].map(match => ({
48
+ text: match[1], start: match.index, end: match.index + match[0].length,
49
+ }));
50
+ spans.forEach((span, i) => {
51
+ const match = span.text.trim().match(REF);
52
+ if (!match || !isPathLike(match[1])) return;
53
+ const ref = {
54
+ map_line: index + 1, column: span.start, raw: span.text, path: match[1],
55
+ start: Number(match[2]), end: match[3] ? Number(match[3]) : null,
56
+ extra: match[4] ? match[4].slice(1).split(',').map(part => part.split('-').map(Number)) : [],
57
+ symbols: [],
58
+ };
59
+ if (!ref.extra.length) ref.symbols = symbolsFor(line, spans, i);
60
+ ref.symbol = ref.symbols[0] || null;
61
+ refs.push(ref);
62
+ });
63
+ });
64
+ return refs;
65
+ }
66
+
67
+ // Symbols bind only when written right next to the ref, so a backticked word
68
+ // later in the sentence never passes for the thing the ref points at.
69
+ function symbolsFor(line, spans, i) {
70
+ const clean = text => text.trim().replace(/\(\)$/, '');
71
+ const found = [];
72
+ const after = spans[i + 1];
73
+ if (after && /^\s*\(?\s*$/.test(line.slice(spans[i].end, after.start)) && IDENT.test(clean(after.text))) {
74
+ found.push(clean(after.text));
75
+ }
76
+ // An unquoted name counts only when it is shaped like code (camelCase or snake_case).
77
+ const bare = line.slice(spans[i].end).match(/^ ([A-Za-z_$][\w$]*)(?:\(\))?(?=[\s),;:.]|$)/);
78
+ if (bare && /[a-z][A-Z]|_/.test(bare[1])) found.push(bare[1]);
79
+ const before = spans[i - 1];
80
+ if (before && /^\s*\(\s*$/.test(line.slice(before.end, spans[i].start)) && IDENT.test(clean(before.text))) {
81
+ found.push(clean(before.text));
82
+ }
83
+ return [...new Set(found)];
84
+ }
85
+
86
+ // Line numbers (1-based) of every line holding the symbol, found in one pass over the text.
87
+ function lineNumbersOf(file, symbol) {
88
+ const found = [];
89
+ for (const match of file.text.matchAll(tokenPattern(symbol, 'g'))) {
90
+ let low = 0;
91
+ let high = file.starts.length - 1;
92
+ while (low < high) {
93
+ const mid = (low + high + 1) >> 1;
94
+ if (file.starts[mid] <= match.index) low = mid; else high = mid - 1;
95
+ }
96
+ if (found[found.length - 1] !== low + 1) found.push(low + 1);
97
+ }
98
+ return found;
99
+ }
100
+
101
+ function classify(ref, file) {
102
+ const { lines } = file;
103
+ const last = ref.end || ref.start;
104
+ const numbers = [ref.start, last, ...ref.extra.flat()];
105
+ const inBounds = numbers.every(n => n >= 1 && n <= lines.length);
106
+ if (!ref.symbol) return inBounds ? { status: 'ok' } : { status: 'missing', reason: 'line out of range' };
107
+ const low = Math.max(1, ref.start - WINDOW);
108
+ const high = Math.min(lines.length, last + WINDOW);
109
+ // A range that runs past the end of the file is wrong even when its start is right.
110
+ if (inBounds) for (const symbol of ref.symbols) {
111
+ const near = tokenPattern(symbol);
112
+ for (let n = low; n <= high; n++) if (near.test(lines[n - 1])) return { status: 'ok', symbol };
113
+ }
114
+ const uses = lineNumbersOf(file, ref.symbol);
115
+ const patterns = definitionPatterns(ref.symbol);
116
+ const defs = uses.filter(n => patterns.some(pattern => pattern.test(lines[n - 1])));
117
+ const candidates = defs.length ? defs : uses;
118
+ if (!candidates.length) return { status: 'missing', reason: `${ref.symbol} is not in the file` };
119
+ if (candidates.length > 1) return { status: 'ambiguous', lines: candidates.slice(0, 5) };
120
+ const line = candidates[0];
121
+ // Keep the range width but never write an end past the last line.
122
+ return { status: 'moved', line, ...(ref.end ? { end_line: Math.min(lines.length, line + ref.end - ref.start) } : {}) };
123
+ }
124
+
125
+ function checkMapRefs(root, mapText) {
126
+ const cache = new Map();
127
+ const linesOf = file => {
128
+ if (!cache.has(file)) {
129
+ const full = path.resolve(root, file);
130
+ let text = null;
131
+ try {
132
+ if (fs.statSync(full).isFile()) text = fs.readFileSync(full, 'utf8');
133
+ } catch (error) {
134
+ if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error;
135
+ }
136
+ if (text === null) cache.set(file, null);
137
+ else {
138
+ const raw = text.split('\n');
139
+ const starts = [0];
140
+ for (let i = 0; i < raw.length - 1; i++) starts.push(starts[i] + raw[i].length + 1);
141
+ const lines = raw.map(line => (line.endsWith('\r') ? line.slice(0, -1) : line));
142
+ cache.set(file, { text, lines, starts });
143
+ }
144
+ }
145
+ return cache.get(file);
146
+ };
147
+ const refs = parseRefs(mapText).map(ref => {
148
+ const file = linesOf(ref.path);
149
+ return { ...ref, ...(file ? classify(ref, file) : { status: 'missing-file' }) };
150
+ });
151
+ const count = status => refs.filter(ref => ref.status === status).length;
152
+ const summary = {
153
+ total: refs.length, ok: count('ok'), moved: count('moved'), ambiguous: count('ambiguous'),
154
+ missing: count('missing'), missing_file: count('missing-file'),
155
+ };
156
+ return { ...summary, score: refs.length ? summary.ok / refs.length : 1, refs };
157
+ }
158
+
159
+ function newRefText(ref) {
160
+ const lineText = ref.end ? `${ref.line}-${ref.end_line}` : `${ref.line}`;
161
+ return ref.raw.replace(`:${ref.start}${ref.end ? `-${ref.end}` : ''}`, `:${lineText}`);
162
+ }
163
+
164
+ // Rewrite only refs whose symbol sits unambiguously on another line.
165
+ function fixMapRefs(root, mapFile = 'atris/MAP.md') {
166
+ const file = path.resolve(root, mapFile);
167
+ const text = fs.readFileSync(file, 'utf8');
168
+ const result = checkMapRefs(root, text);
169
+ const moved = result.refs.filter(ref => ref.status === 'moved');
170
+ const lines = text.split('\n');
171
+ const changes = [];
172
+ for (const ref of [...moved].sort((a, b) => b.map_line - a.map_line || b.column - a.column)) {
173
+ const replacement = newRefText(ref);
174
+ const line = lines[ref.map_line - 1];
175
+ lines[ref.map_line - 1] = line.slice(0, ref.column + 1) + replacement + line.slice(ref.column + 1 + ref.raw.length);
176
+ changes.unshift({ map_line: ref.map_line, from: ref.raw, to: replacement, symbol: ref.symbol });
177
+ }
178
+ if (changes.length) fs.writeFileSync(file, lines.join('\n'));
179
+ const left = result.refs.filter(ref => ref.status !== 'ok' && ref.status !== 'moved');
180
+ return { changes, left };
181
+ }
182
+
183
+ module.exports = { checkMapRefs, fixMapRefs };
@@ -400,9 +400,9 @@ function defaultThinkingBody() {
400
400
  '',
401
401
  'Team logs say what happened.',
402
402
  '',
403
- 'This file says how Keshav thinks.',
403
+ 'This file says how the operator thinks.',
404
404
  '',
405
- '## How Keshav Thinks',
405
+ '## How the Operator Thinks',
406
406
  '',
407
407
  '- Starts from messy truth, then wants it clarified into a mission he can approve or reject.',
408
408
  '- Values plain English, no jargon, and proof over motion.',
@@ -411,7 +411,7 @@ function defaultThinkingBody() {
411
411
  '## Approval Rules',
412
412
  '',
413
413
  '- Atris can clarify, propose, run bounded proof steps, and write receipts.',
414
- '- Keshav keeps taste, judgment, priority, budget, customer calls, and final accept.',
414
+ '- The operator keeps taste, judgment, priority, budget, customer calls, and final accept.',
415
415
  '- Real-world side effects wait for approval unless the mission explicitly grants permission.',
416
416
  '',
417
417
  '## Proof Standards',
@@ -457,7 +457,7 @@ function writeThinkingMemory(room, options = {}) {
457
457
  path: relativePath,
458
458
  updated: true,
459
459
  latest_signal: signal,
460
- purpose: 'how Keshav thinks, decides, approves, rejects, prioritizes, and explains.',
460
+ purpose: 'how the operator thinks, decides, approves, rejects, prioritizes, and explains.',
461
461
  };
462
462
  }
463
463
 
@@ -1,5 +1,17 @@
1
1
  'use strict';
2
2
 
3
+ /**
4
+ * The one place that reads the two env names. Both mean the same thing; any
5
+ * value other than empty, 0, or false turns prompts off.
6
+ */
7
+ const NON_INTERACTIVE_ENV_NAMES = ['ATRIS_NONINTERACTIVE', 'ATRIS_NO_INTERACTIVE'];
8
+ function envForcesNonInteractive(env = process.env) {
9
+ return NON_INTERACTIVE_ENV_NAMES.some((name) => {
10
+ const value = String(env[name] || '').trim().toLowerCase();
11
+ return value !== '' && value !== '0' && value !== 'false';
12
+ });
13
+ }
14
+
3
15
  /**
4
16
  * Shared headless/agent guard. When true, commands must not open a prompt.
5
17
  * Covers: ATRIS_NONINTERACTIVE / ATRIS_NO_INTERACTIVE, --yes/-y, --json.
@@ -9,8 +21,7 @@
9
21
  */
10
22
  function isForcedNonInteractive(args = process.argv.slice(2)) {
11
23
  const list = Array.isArray(args) ? args : [];
12
- if (process.env.ATRIS_NONINTERACTIVE === '1') return true;
13
- if (process.env.ATRIS_NO_INTERACTIVE === '1') return true;
24
+ if (envForcesNonInteractive()) return true;
14
25
  if (list.includes('--yes') || list.includes('-y')) return true;
15
26
  if (list.includes('--json')) return true;
16
27
  return false;
@@ -48,8 +59,7 @@ function hasYesFlag(args = []) {
48
59
  function refuseHeadlessUnless(args, { allowYes = true, allowOnce = false, usage = '' } = {}) {
49
60
  const list = Array.isArray(args) ? args : [];
50
61
  const headless = Boolean(
51
- process.env.ATRIS_NONINTERACTIVE === '1'
52
- || process.env.ATRIS_NO_INTERACTIVE === '1'
62
+ envForcesNonInteractive()
53
63
  || !process.stdin.isTTY
54
64
  || !process.stdout.isTTY
55
65
  );
@@ -77,6 +87,7 @@ function rejectUnsupportedJson(commandName, args = process.argv.slice(2)) {
77
87
  }
78
88
 
79
89
  module.exports = {
90
+ envForcesNonInteractive,
80
91
  isForcedNonInteractive,
81
92
  isNonInteractive,
82
93
  wantsJson,
@@ -366,7 +366,7 @@ function syncLessonsMd(root, mined) {
366
366
  const today = (mined.mined_at || new Date().toISOString()).split('T')[0];
367
367
  const lines = (mined.lessons || []).map((lesson) => ({
368
368
  id: lesson.id,
369
- line: `- **[${today}] policy-${lesson.id}** \u2014 ${lesson.status || 'pass'} \u2014 ${lesson.lesson} (mined from ${mined.sources.career_xp_receipts} receipts / ${mined.sources.task_episodes} episodes / ${mined.sources.scorecards} scorecards)`,
369
+ line: `- **[${today}] policy-${lesson.id}** - ${lesson.status || 'pass'} - ${String(lesson.lesson).replace(/\s*\u2014\s*/g, ', ')} (mined from ${mined.sources.career_xp_receipts} receipts / ${mined.sources.task_episodes} episodes / ${mined.sources.scorecards} scorecards)`,
370
370
  }));
371
371
  if (!lines.length) return { path: lessonsPath, written: [] };
372
372