atris 3.58.6 → 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 +9 -1
- package/atris/GEMINI.md +7 -6
- package/atris/PERSONA.md +4 -4
- package/atris/features/README.md +77 -6
- package/atris/policies/engineering-principles.md +129 -0
- package/atris/policies/genesis.md +112 -0
- package/atris/policies/product-design-principles.md +100 -0
- 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/design/SKILL.md +3 -1
- package/atris/skills/engines/SKILL.md +42 -50
- package/atris/skills/meta/SKILL.md +4 -4
- package/atris/skills/youtube/SKILL.md +11 -11
- package/bin/atris.js +60 -8
- package/commands/aeo.js +9 -12
- package/commands/analytics.js +8 -11
- package/commands/auth.js +8 -2
- package/commands/autopilot.js +4 -3
- package/commands/avail.js +1 -1
- package/commands/brain.js +4 -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/design.js +362 -0
- package/commands/doc-health.js +377 -0
- package/commands/drive.js +32 -0
- package/commands/experiments.js +13 -4
- package/commands/improve.js +68 -2
- package/commands/integrations.js +2 -5
- package/commands/land.js +144 -4
- package/commands/learn.js +7 -2
- package/commands/member.js +239 -58
- package/commands/mission.js +94 -18
- package/commands/pack.js +2 -2
- package/commands/probe.js +11 -5
- package/commands/pull.js +1 -1
- package/commands/pulse.js +38 -0
- package/commands/push.js +1 -1
- package/commands/rainmaker.js +3 -16
- package/commands/rsi.js +156 -0
- 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 +54 -3
- package/commands/workflow.js +17 -7
- package/commands/youtube.js +76 -8
- package/lib/apply-gate.js +22 -4
- package/lib/codex-flight.js +28 -5
- package/lib/daily-log.js +88 -0
- package/lib/design-api.js +130 -0
- package/lib/engine-ask.js +1 -1
- package/lib/first-minute.js +1 -6
- package/lib/fleet.js +4 -1
- package/lib/improve-vitals-html.js +3 -2
- package/lib/known-commands.js +3 -3
- package/lib/lesson-contradiction.js +2 -2
- package/lib/map-refs.js +183 -0
- package/lib/member-context.js +42 -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/rsi-record.js +335 -0
- 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 +36 -17
- package/lib/task-db.js +74 -53
- package/lib/task-list-keeper.js +192 -0
- package/lib/todo-fallback.js +10 -4
- package/lib/todo.js +22 -10
- package/lib/wish-delegate.js +19 -5
- package/mcp/atris-mcp/index.mjs +227 -0
- package/package.json +4 -2
- package/scripts/det/det.js +2 -2
- package/scripts/det/pr-description.js +2 -2
- package/scripts/det/test.js +2 -2
- package/scripts/det/ytnotes +43 -8
- package/utils/api.js +1 -1
- package/utils/auth.js +79 -14
- 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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Shared client for the Atris design API. Used by `atris design` and the
|
|
4
|
+
// `atris mcp` stdio server so both resolve keys and bill the same way.
|
|
5
|
+
//
|
|
6
|
+
// Key resolution order:
|
|
7
|
+
// 1. ATRIS_API_KEY env var
|
|
8
|
+
// 2. login credentials token (atris login)
|
|
9
|
+
// 3. ~/.atris/design-api-key file (raw key text, saved by hand)
|
|
10
|
+
//
|
|
11
|
+
// Endpoints (see https://api.atris.ai/llms.txt):
|
|
12
|
+
// POST /design/extractions {"url"} -> job; poll GET /design/extractions/{id}
|
|
13
|
+
// POST /design/adherence {source, reference} -> job; poll GET /design/adherence/{id}
|
|
14
|
+
// POST /design/search {"query", "limit"} -> sync result
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { getApiBaseUrl, httpRequest } = require('../utils/api');
|
|
20
|
+
const { loadCredentials } = require('../utils/auth');
|
|
21
|
+
|
|
22
|
+
const DESIGN_KEY_FILE = path.join(os.homedir(), '.atris', 'design-api-key');
|
|
23
|
+
|
|
24
|
+
const POLL_INTERVAL_MS = 3000;
|
|
25
|
+
const POLL_TIMEOUT_MS = 3 * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
function readKeyFile(file = DESIGN_KEY_FILE) {
|
|
28
|
+
try {
|
|
29
|
+
const text = fs.readFileSync(file, 'utf8').trim();
|
|
30
|
+
return text || null;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Returns the bearer token for design calls, or null when nothing is set up.
|
|
37
|
+
function resolveDesignKey(env = process.env, deps = {}) {
|
|
38
|
+
const fromEnv = env.ATRIS_API_KEY && env.ATRIS_API_KEY.trim();
|
|
39
|
+
if (fromEnv) return fromEnv;
|
|
40
|
+
const load = deps.loadCredentials || loadCredentials;
|
|
41
|
+
const creds = load();
|
|
42
|
+
const token = creds && typeof creds.token === 'string' ? creds.token.trim() : '';
|
|
43
|
+
if (token) return token;
|
|
44
|
+
return (deps.readKeyFile || readKeyFile)();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// One JSON call against the design API. Returns { ok, status, data, error }.
|
|
48
|
+
async function designRequest(pathname, options = {}) {
|
|
49
|
+
const key = options.key;
|
|
50
|
+
const url = `${getApiBaseUrl()}${pathname.startsWith('/') ? pathname : `/${pathname}`}`;
|
|
51
|
+
const headers = {
|
|
52
|
+
'Authorization': `Bearer ${key}`,
|
|
53
|
+
'Accept': 'application/json',
|
|
54
|
+
...(options.headers || {}),
|
|
55
|
+
};
|
|
56
|
+
let body;
|
|
57
|
+
if (options.body !== undefined && options.body !== null) {
|
|
58
|
+
body = JSON.stringify(options.body);
|
|
59
|
+
headers['Content-Type'] = 'application/json';
|
|
60
|
+
}
|
|
61
|
+
const res = await httpRequest(url, {
|
|
62
|
+
method: options.method || 'GET',
|
|
63
|
+
headers,
|
|
64
|
+
body,
|
|
65
|
+
timeoutMs: options.timeoutMs != null ? options.timeoutMs : 30000,
|
|
66
|
+
});
|
|
67
|
+
const text = res.body.toString('utf8');
|
|
68
|
+
let data = null;
|
|
69
|
+
try { data = text ? JSON.parse(text) : null; } catch { data = null; }
|
|
70
|
+
const ok = res.status >= 200 && res.status < 300;
|
|
71
|
+
const error = !ok
|
|
72
|
+
? (data && typeof data === 'object' && (data.detail || data.error || data.message)) || text.slice(0, 200) || `http ${res.status}`
|
|
73
|
+
: undefined;
|
|
74
|
+
return { ok, status: res.status, data, error };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
78
|
+
|
|
79
|
+
// Poll a design job until it finishes. `fetchJob` returns the parsed job
|
|
80
|
+
// object; `onTick` fires once per wait so callers can draw a spinner.
|
|
81
|
+
// Resolves { ok, job, timedOut } and never throws on a bad status.
|
|
82
|
+
async function pollDesignJob(fetchJob, options = {}) {
|
|
83
|
+
const intervalMs = options.intervalMs != null ? options.intervalMs : POLL_INTERVAL_MS;
|
|
84
|
+
const timeoutMs = options.timeoutMs != null ? options.timeoutMs : POLL_TIMEOUT_MS;
|
|
85
|
+
const wait = options.sleep || sleep;
|
|
86
|
+
const onTick = typeof options.onTick === 'function' ? options.onTick : null;
|
|
87
|
+
const start = Date.now();
|
|
88
|
+
let job = await fetchJob();
|
|
89
|
+
while (job && job.status && !isTerminalStatus(job.status)) {
|
|
90
|
+
if (Date.now() - start >= timeoutMs) return { ok: false, job, timedOut: true };
|
|
91
|
+
if (onTick) onTick(Date.now() - start, job);
|
|
92
|
+
await wait(intervalMs);
|
|
93
|
+
job = await fetchJob();
|
|
94
|
+
}
|
|
95
|
+
const ok = Boolean(job) && job.status === 'completed' && !job.error;
|
|
96
|
+
return { ok, job, timedOut: false };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isTerminalStatus(status) {
|
|
100
|
+
const s = String(status || '').toLowerCase();
|
|
101
|
+
return s === 'completed' || s === 'failed' || s === 'error' || s === 'succeeded';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Pull the billing block off any design response. Top-level credits_charged on
|
|
105
|
+
// jobs is the job's price; atris.credits_charged is what this call billed.
|
|
106
|
+
function billingOf(data = {}) {
|
|
107
|
+
const raw = data && typeof data === 'object' ? data : {};
|
|
108
|
+
const atris = raw.atris && typeof raw.atris === 'object' ? raw.atris : {};
|
|
109
|
+
const charged = Number(atris.credits_charged != null ? atris.credits_charged : raw.credits_charged);
|
|
110
|
+
const balance = Number(atris.balance_remaining_usd);
|
|
111
|
+
return {
|
|
112
|
+
credits: Number.isFinite(charged) ? charged : null,
|
|
113
|
+
balanceUsd: Number.isFinite(balance) ? balance : null,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function creditLine(data) {
|
|
118
|
+
const bill = billingOf(data);
|
|
119
|
+
const charged = bill.credits == null ? '?' : `${bill.credits} credit${bill.credits === 1 ? '' : 's'}`;
|
|
120
|
+
const left = bill.balanceUsd == null ? '' : ` $${bill.balanceUsd.toFixed(2)} left.`;
|
|
121
|
+
return `${charged} charged.${left}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
resolveDesignKey,
|
|
126
|
+
designRequest,
|
|
127
|
+
pollDesignJob,
|
|
128
|
+
billingOf,
|
|
129
|
+
creditLine,
|
|
130
|
+
};
|
package/lib/engine-ask.js
CHANGED
|
@@ -283,7 +283,7 @@ function buildReadOnlyEngineInvocation(engineName, prompt, modelName = '', {
|
|
|
283
283
|
if (engine === 'opencode') {
|
|
284
284
|
// `run` with a message is headless print mode; the built-in plan agent
|
|
285
285
|
// keeps the ask read-only (verified live 2026-08-21, ~7s per lookup).
|
|
286
|
-
return { engine, bin: profile.bin, args: ['--agent', 'plan', ...(model ? ['-m', model] : []), request] };
|
|
286
|
+
return { engine, bin: profile.bin, args: ['run', '--agent', 'plan', ...(model ? ['-m', model] : []), request] };
|
|
287
287
|
}
|
|
288
288
|
throw new Error(`engine ask has no read-only command for ${engine}`);
|
|
289
289
|
}
|
package/lib/first-minute.js
CHANGED
|
@@ -168,12 +168,7 @@ function titlesFromTodoSection(root, section) {
|
|
|
168
168
|
if (!match) return null;
|
|
169
169
|
const body = String(match[1] || '').trim();
|
|
170
170
|
if (!body || /\(empty|\(see /i.test(body)) return [];
|
|
171
|
-
return
|
|
172
|
-
.split('\n')
|
|
173
|
-
.map((line) => line.trim())
|
|
174
|
-
.filter((line) => /^-\s+/.test(line) && !/\(empty/i.test(line))
|
|
175
|
-
.map((line) => line.replace(/^-+\s*/, '').trim())
|
|
176
|
-
.filter(Boolean);
|
|
171
|
+
return require('./todo-fallback').parseSection(text, section).map(task => task.title);
|
|
177
172
|
} catch {
|
|
178
173
|
return null;
|
|
179
174
|
}
|
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: [] };
|
package/lib/known-commands.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const knownCommands = ['init', 'guide', 'doctor', 'install', 'workspace', 'log', 'logs', 'later', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'revisions', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
|
|
3
|
+
const knownCommands = ['init', 'guide', 'doctor', 'doc-health', 'install', 'workspace', 'log', 'logs', 'later', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'revisions', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
|
|
4
4
|
'activate', '_activate', 'agent', 'team', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
5
5
|
'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'taste', 'teach', 'plugin', 'experiments', 'bench', 'router', 'tree', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube', 'x-search',
|
|
6
6
|
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'sync-checkout',
|
|
7
|
-
'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'api-key', 'topup', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
8
|
-
'fork', 'browse', 'publish', 'pack', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'fleet-report', 'loops', 'self-improve', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines', 'playbook', 'feed', 'brief', 'social', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join'];
|
|
7
|
+
'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'api-key', 'topup', 'design', 'mcp', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
8
|
+
'fork', 'browse', 'publish', 'pack', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'fleet-report', 'loops', 'self-improve', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines', 'playbook', 'feed', 'brief', 'social', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join', 'rsi'];
|
|
9
9
|
|
|
10
10
|
// Damerau-Levenshtein edit distance between two short strings. Counts an
|
|
11
11
|
// adjacent transposition (e.g. "taks" -> "task") as a single edit, since
|
|
@@ -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 };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const MEMBER_PROCESS_PATH = 'atris/team/MEMBER_PROCESS.md';
|
|
7
|
+
const MAX_MEMBER_PROCESS_BYTES = 32 * 1024;
|
|
8
|
+
|
|
9
|
+
// Read from the executing workspace only. Never fall back to the launcher,
|
|
10
|
+
// an ancestor workspace, or a bundled policy belonging to another owner.
|
|
11
|
+
function memberProcessPrompt(cwd = process.cwd()) {
|
|
12
|
+
const file = path.join(cwd, MEMBER_PROCESS_PATH);
|
|
13
|
+
let fd;
|
|
14
|
+
try {
|
|
15
|
+
const stat = fs.statSync(file);
|
|
16
|
+
if (!stat.isFile()) throw new Error('expected a regular file');
|
|
17
|
+
fd = fs.openSync(file, 'r');
|
|
18
|
+
const buffer = Buffer.alloc(MAX_MEMBER_PROCESS_BYTES + 1);
|
|
19
|
+
const count = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
20
|
+
if (count > MAX_MEMBER_PROCESS_BYTES) {
|
|
21
|
+
throw new Error(`keep the shared process within ${MAX_MEMBER_PROCESS_BYTES} bytes`);
|
|
22
|
+
}
|
|
23
|
+
const body = buffer.subarray(0, count).toString('utf8').trim();
|
|
24
|
+
if (!body) return '';
|
|
25
|
+
return [
|
|
26
|
+
'## Shared member process',
|
|
27
|
+
`Source: ${MEMBER_PROCESS_PATH} in this execution workspace.`,
|
|
28
|
+
'Apply these defaults alongside the member identity. They do not expand role permissions or override operator direction, task scope, output format, or frozen mission constraints.',
|
|
29
|
+
'',
|
|
30
|
+
body,
|
|
31
|
+
'',
|
|
32
|
+
'End of shared member process. The current task and its constraints follow.',
|
|
33
|
+
].join('\n');
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error.code === 'ENOENT') return '';
|
|
36
|
+
throw new Error(`Cannot load ${MEMBER_PROCESS_PATH}: ${error.message}`);
|
|
37
|
+
} finally {
|
|
38
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { memberProcessPrompt, MEMBER_PROCESS_PATH, MAX_MEMBER_PROCESS_BYTES };
|
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
|
|