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.
- package/GETTING_STARTED.md +2 -2
- package/PERSONA.md +11 -11
- package/README.md +1 -1
- package/atris/GEMINI.md +7 -6
- package/atris/PERSONA.md +4 -4
- package/atris/features/README.md +77 -6
- package/atris/skills/README.md +44 -15
- package/atris/skills/aeo/SKILL.md +10 -10
- package/atris/skills/atris/SKILL.md +1 -1
- package/atris/skills/engines/SKILL.md +42 -50
- package/atris/skills/meta/SKILL.md +4 -4
- package/bin/atris.js +5 -6
- package/commands/aeo.js +9 -12
- package/commands/analytics.js +8 -11
- package/commands/autopilot.js +4 -3
- package/commands/avail.js +1 -1
- package/commands/brain.js +3 -1
- package/commands/business.js +13 -8
- package/commands/close.js +2 -1
- package/commands/computer.js +15 -10
- package/commands/context-sync.js +3 -3
- package/commands/doc-health.js +50 -2
- package/commands/experiments.js +13 -4
- package/commands/improve.js +1 -1
- package/commands/integrations.js +2 -5
- package/commands/learn.js +1 -1
- package/commands/member.js +174 -47
- package/commands/mission.js +57 -11
- package/commands/pack.js +2 -2
- package/commands/probe.js +11 -5
- package/commands/pull.js +1 -1
- package/commands/push.js +1 -1
- package/commands/rainmaker.js +3 -16
- package/commands/spaceship.js +1 -1
- package/commands/stream.js +27 -2
- package/commands/study.js +15 -9
- package/commands/sync.js +1 -1
- package/commands/task.js +13 -2
- package/commands/workflow.js +6 -1
- package/lib/codex-flight.js +28 -5
- package/lib/fleet.js +4 -1
- package/lib/improve-vitals-html.js +3 -2
- package/lib/lesson-contradiction.js +2 -2
- package/lib/map-refs.js +183 -0
- package/lib/mission-room.js +4 -4
- package/lib/noninteractive.js +15 -4
- package/lib/policy-lessons.js +1 -1
- package/lib/receipt-block.js +137 -28
- package/lib/runner-command.js +1 -1
- package/lib/scorecard.js +4 -4
- package/lib/sleep-sync.js +11 -0
- package/lib/state-detection.js +28 -9
- package/lib/task-db.js +3 -2
- package/lib/todo-fallback.js +1 -1
- package/lib/wish-delegate.js +19 -5
- package/mcp/atris-mcp/index.mjs +77 -24
- package/package.json +2 -5
- package/scripts/det/det.js +2 -2
- package/scripts/det/pr-description.js +2 -2
- package/scripts/det/test.js +2 -2
- package/utils/api.js +1 -1
- package/utils/auth.js +17 -7
- package/utils/backend-root.js +31 -0
- package/utils/config.js +5 -4
- package/utils/owner-identity.js +61 -0
- package/atris/policies/LESSONS.md +0 -13
- 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
|
-
|
|
95
|
-
process.env.ATRIS_STUDY_ROOT
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
if (fs.existsSync(
|
|
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 (
|
|
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 {
|
|
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;
|
package/commands/workflow.js
CHANGED
|
@@ -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 =
|
|
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,
|
package/lib/codex-flight.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 = [
|
|
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:
|
|
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
|
-
|
|
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) =>
|
|
55
|
-
const theirs = flags.filter((f) =>
|
|
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
|
|
26
|
-
const lessonRegex = /^-\s+\*\*\[(\d{4}-\d{2}-\d{2})\]\s+([a-z0-9-]+)\*\*\s
|
|
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 }]
|
package/lib/map-refs.js
ADDED
|
@@ -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 };
|
package/lib/mission-room.js
CHANGED
|
@@ -400,9 +400,9 @@ function defaultThinkingBody() {
|
|
|
400
400
|
'',
|
|
401
401
|
'Team logs say what happened.',
|
|
402
402
|
'',
|
|
403
|
-
'This file says how
|
|
403
|
+
'This file says how the operator thinks.',
|
|
404
404
|
'',
|
|
405
|
-
'## How
|
|
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
|
-
'-
|
|
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
|
|
460
|
+
purpose: 'how the operator thinks, decides, approves, rejects, prioritizes, and explains.',
|
|
461
461
|
};
|
|
462
462
|
}
|
|
463
463
|
|
package/lib/noninteractive.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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,
|
package/lib/policy-lessons.js
CHANGED
|
@@ -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}**
|
|
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
|
|