atris 3.62.0 → 3.64.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.
- package/atris/features/README.md +8 -0
- package/atris/skills/README.md +1 -0
- package/atris/skills/commerce/SKILL.md +120 -0
- package/atris/skills/x-search/SKILL.md +3 -1
- package/ax +4 -3
- package/bin/atris.js +22 -2
- package/cli/atris_code.py +25 -1
- package/commands/auth.js +79 -10
- package/commands/autoland.js +11 -1
- package/commands/browse.js +2 -2
- package/commands/business.js +3 -3
- package/commands/buy.js +242 -0
- package/commands/deck.js +2 -2
- package/commands/engine.js +122 -13
- package/commands/feedback.js +2 -2
- package/commands/fork.js +2 -2
- package/commands/gm.js +2 -1
- package/commands/improve.js +142 -8
- package/commands/integrations.js +4 -3
- package/commands/loops.js +2 -1
- package/commands/member.js +82 -15
- package/commands/mission.js +4 -1
- package/commands/pack-craft.js +2 -2
- package/commands/publish.js +2 -2
- package/commands/radar.js +3 -2
- package/commands/router.js +2 -2
- package/commands/task.js +91 -2
- package/commands/transactions.js +115 -0
- package/commands/truth.js +2 -2
- package/commands/x-search.js +81 -10
- package/commands/youtube.js +24 -1
- package/lib/autoland.js +3 -2
- package/lib/ax-auto-lane.js +2 -2
- package/lib/brief-rules.js +11 -0
- package/lib/chat-log-scan.js +2 -1
- package/lib/ci-runner.js +3 -2
- package/lib/codex-flight.js +2 -1
- package/lib/commerce.js +90 -0
- package/lib/deck-history.js +2 -2
- package/lib/deck-review.js +3 -3
- package/lib/design-api.js +2 -2
- package/lib/engine-registry.js +102 -18
- package/lib/first-minute.js +2 -2
- package/lib/fleet.js +8 -36
- package/lib/git-spawn.js +5 -2
- package/lib/ignored-cache.js +51 -0
- package/lib/known-commands.js +1 -1
- package/lib/manifest.js +2 -2
- package/lib/map-refs.js +70 -4
- package/lib/member-engine.js +15 -1
- package/lib/member-scaffold.js +2 -0
- package/lib/member-type.js +27 -0
- package/lib/pack-capabilities.js +2 -1
- package/lib/permission-grants.js +2 -2
- package/lib/pulse.js +5 -5
- package/lib/roster-runs.js +13 -2
- package/lib/runner-command.js +54 -3
- package/lib/sandbox-pids.js +113 -0
- package/lib/secret-gateway.js +3 -13
- package/lib/state-home.js +106 -0
- package/lib/task-db.js +4 -3
- package/lib/task-prune.js +462 -0
- package/lib/x-post-check.js +497 -0
- package/package.json +1 -1
- package/scripts/det/data/xsearch-answer.json +24 -0
- package/scripts/det/data/xsearch-live-2026-09-29.json +93 -0
- package/scripts/det/xsearch-bench.js +307 -0
- package/scripts/det/ytnotes +122 -16
- package/scripts/det/ytrail-bench.js +215 -0
- package/scripts/det/ytrail-eval.js +128 -56
- package/utils/auth.js +3 -3
- package/utils/update-check.js +2 -2
package/commands/improve.js
CHANGED
|
@@ -18,10 +18,13 @@
|
|
|
18
18
|
* fallback, and scorecard writes can all be faked in tests.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
const crypto = require('crypto');
|
|
21
22
|
const fs = require('fs');
|
|
22
23
|
const os = require('os');
|
|
23
24
|
const path = require('path');
|
|
24
25
|
const { spawnSync } = require('child_process');
|
|
26
|
+
const gitSpawn = require('../lib/git-spawn');
|
|
27
|
+
const ignoredCache = require('../lib/ignored-cache');
|
|
25
28
|
const { apiRequestJson, getApiBaseUrl } = require('../utils/api');
|
|
26
29
|
const { loadCredentials } = require('../utils/auth');
|
|
27
30
|
const pulse = require('../lib/pulse');
|
|
@@ -1085,8 +1088,9 @@ function parseRevisionsArgs(argv = []) {
|
|
|
1085
1088
|
return opts;
|
|
1086
1089
|
}
|
|
1087
1090
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1091
|
+
// Through the module object so tests can count git spawns with a spy.
|
|
1092
|
+
function gitLines(cwdRoot, gitArgs, { input, maxBuffer } = {}) {
|
|
1093
|
+
const r = gitSpawn.runGit(gitArgs, { cwd: cwdRoot, check: false, input, maxBuffer });
|
|
1090
1094
|
if (r.status !== 0) {
|
|
1091
1095
|
const err = new Error(String(r.stderr || `git ${gitArgs[0]} exited ${r.status}`).trim());
|
|
1092
1096
|
err.gitFailed = true;
|
|
@@ -1095,17 +1099,129 @@ function gitLines(cwdRoot, gitArgs) {
|
|
|
1095
1099
|
return String(r.stdout || '');
|
|
1096
1100
|
}
|
|
1097
1101
|
|
|
1102
|
+
// File names come back NUL-separated and never quoted, whatever core.quotePath
|
|
1103
|
+
// says, so a name reads the same fresh from git and from the saved cache.
|
|
1104
|
+
const NAME_ARGS = ['-c', 'core.quotePath=false'];
|
|
1105
|
+
|
|
1106
|
+
function splitNames(out) {
|
|
1107
|
+
return String(out || '').split('\0').filter(Boolean);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1098
1110
|
/**
|
|
1099
1111
|
* Files changed by one commit. Merge commits are attributed by their
|
|
1100
1112
|
* first-parent diff (what the merge actually brought onto the mainline);
|
|
1101
1113
|
* plain commits use diff-tree. Root commits list their initial files.
|
|
1114
|
+
* The rename choice is spelled out so a user's diff.renames setting cannot
|
|
1115
|
+
* change the lists: merges find renames (git diff's default), plain commits
|
|
1116
|
+
* do not (diff-tree's default).
|
|
1102
1117
|
*/
|
|
1103
1118
|
function commitFiles(cwdRoot, commit) {
|
|
1104
1119
|
const parents = commit.parents;
|
|
1105
1120
|
const out = parents.length >= 2
|
|
1106
|
-
? gitLines(cwdRoot, ['diff', '--name-only', `${commit.hash}^1`, commit.hash])
|
|
1107
|
-
: gitLines(cwdRoot, ['diff-tree', '--no-commit-id', '--name-only', '-r', '--root', commit.hash]);
|
|
1108
|
-
return out
|
|
1121
|
+
? gitLines(cwdRoot, [...NAME_ARGS, 'diff', '--name-only', '--find-renames', '-z', `${commit.hash}^1`, commit.hash])
|
|
1122
|
+
: gitLines(cwdRoot, [...NAME_ARGS, 'diff-tree', '--no-commit-id', '--name-only', '--no-renames', '-z', '-r', '--root', commit.hash]);
|
|
1123
|
+
return splitNames(out);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Files changed by many plain commits in one git process: the same diff-tree
|
|
1128
|
+
* per-commit call above, fed every hash on stdin. --always prints the hash
|
|
1129
|
+
* even for an empty commit. Raw output frames every file as a ':'-led status
|
|
1130
|
+
* field followed by its path, so a path is never mistaken for a commit hash,
|
|
1131
|
+
* even a path spelled exactly like the next one.
|
|
1132
|
+
*/
|
|
1133
|
+
function plainCommitFiles(cwdRoot, commits) {
|
|
1134
|
+
const found = new Map(commits.map((commit) => [commit.hash, []]));
|
|
1135
|
+
if (!commits.length) return found;
|
|
1136
|
+
const hashes = commits.map((commit) => commit.hash);
|
|
1137
|
+
const out = gitLines(cwdRoot, [...NAME_ARGS, 'diff-tree', '--stdin', '--always', '--raw', '--no-renames', '-z', '-r', '--root'], {
|
|
1138
|
+
input: `${hashes.join('\n')}\n`,
|
|
1139
|
+
// Many commits' lists in one reply; the per-commit calls each had 1MB.
|
|
1140
|
+
maxBuffer: Math.max(1, hashes.length) * 1024 * 1024,
|
|
1141
|
+
});
|
|
1142
|
+
const fields = splitNames(out);
|
|
1143
|
+
let current = null;
|
|
1144
|
+
for (let i = 0; i < fields.length; i++) {
|
|
1145
|
+
const field = fields[i];
|
|
1146
|
+
if (field.startsWith(':')) {
|
|
1147
|
+
// Status field; with --no-renames exactly one path follows it.
|
|
1148
|
+
if (current && i + 1 < fields.length) current.push(fields[i + 1]);
|
|
1149
|
+
i += 1;
|
|
1150
|
+
} else {
|
|
1151
|
+
current = found.get(field) || null;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return found;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Where git keeps state shared by every checkout of this repo (a linked
|
|
1158
|
+
// worktree points at its main repo). null when it cannot be found on disk.
|
|
1159
|
+
function gitCommonDir(root) {
|
|
1160
|
+
if (process.env.GIT_DIR || process.env.GIT_COMMON_DIR) return null;
|
|
1161
|
+
let dir = path.resolve(root);
|
|
1162
|
+
for (;;) {
|
|
1163
|
+
const dotGit = path.join(dir, '.git');
|
|
1164
|
+
let stat = null;
|
|
1165
|
+
try { stat = fs.statSync(dotGit); } catch { stat = null; }
|
|
1166
|
+
if (stat && stat.isDirectory()) return dotGit;
|
|
1167
|
+
if (stat && stat.isFile()) {
|
|
1168
|
+
const match = fs.readFileSync(dotGit, 'utf8').match(/^gitdir:\s*(.+)$/m);
|
|
1169
|
+
if (!match) return null;
|
|
1170
|
+
const gitDir = path.resolve(dir, match[1].trim());
|
|
1171
|
+
try {
|
|
1172
|
+
return path.resolve(gitDir, fs.readFileSync(path.join(gitDir, 'commondir'), 'utf8').trim());
|
|
1173
|
+
} catch {
|
|
1174
|
+
return gitDir;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
const parent = path.dirname(dir);
|
|
1178
|
+
if (parent === dir) return null;
|
|
1179
|
+
dir = parent;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// A shallow clone cuts history at boundary commits, and those commits list
|
|
1184
|
+
// different files once the history is fetched. The saved lists are only good
|
|
1185
|
+
// for the shallow state they were read under; null means do not cache.
|
|
1186
|
+
function shallowStamp(root) {
|
|
1187
|
+
const common = gitCommonDir(root);
|
|
1188
|
+
if (!common) return null;
|
|
1189
|
+
try {
|
|
1190
|
+
return crypto.createHash('sha1').update(fs.readFileSync(path.join(common, 'shallow'))).digest('hex');
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
return error.code === 'ENOENT' ? 'full' : null;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// A commit's file list never changes, so it is saved by hash in the ignored
|
|
1197
|
+
// .atris/cache and the gauge (shown on every boot) only asks git about commits
|
|
1198
|
+
// it has not seen. Hashes this read used come first; older ones stay until the
|
|
1199
|
+
// cap, so a 7-day boot read and a 14-day report read do not evict each other.
|
|
1200
|
+
const REVISION_FILES_CACHE = 'revision-files.json';
|
|
1201
|
+
const REVISION_FILES_CACHE_MAX = 5000;
|
|
1202
|
+
|
|
1203
|
+
function loadRevisionFilesCache(root) {
|
|
1204
|
+
const shallow = ignoredCache.cacheEnabled(root) ? shallowStamp(root) : null;
|
|
1205
|
+
const cache = { root, shallow, entries: {}, used: {}, dirty: false, writable: Boolean(shallow) };
|
|
1206
|
+
if (!cache.writable) return cache;
|
|
1207
|
+
const parsed = ignoredCache.readCacheJson(root, REVISION_FILES_CACHE);
|
|
1208
|
+
if (parsed && parsed.version === 1 && parsed.shallow === shallow && parsed.entries && typeof parsed.entries === 'object') {
|
|
1209
|
+
cache.entries = parsed.entries;
|
|
1210
|
+
}
|
|
1211
|
+
return cache;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function saveRevisionFilesCache(cache) {
|
|
1215
|
+
if (!cache.writable || !cache.dirty) return;
|
|
1216
|
+
const entries = { ...cache.used };
|
|
1217
|
+
let count = Object.keys(entries).length;
|
|
1218
|
+
for (const [hash, files] of Object.entries(cache.entries)) {
|
|
1219
|
+
if (count >= REVISION_FILES_CACHE_MAX) break;
|
|
1220
|
+
if (entries[hash] || !Array.isArray(files)) continue;
|
|
1221
|
+
entries[hash] = files;
|
|
1222
|
+
count += 1;
|
|
1223
|
+
}
|
|
1224
|
+
ignoredCache.writeCacheJson(cache.root, REVISION_FILES_CACHE, { version: 1, shallow: cache.shallow, entries });
|
|
1109
1225
|
}
|
|
1110
1226
|
|
|
1111
1227
|
/**
|
|
@@ -1150,10 +1266,27 @@ function collectRevisionSignals(root, options = {}) {
|
|
|
1150
1266
|
// commits count as revision signals.
|
|
1151
1267
|
const humans = commits.filter((c) => !c.isAgent && c.parents.length < 2);
|
|
1152
1268
|
|
|
1153
|
-
|
|
1269
|
+
// Every landing's files are needed, and a human commit's only when it falls
|
|
1270
|
+
// inside some landing's window. Plain commits missing from the cache are
|
|
1271
|
+
// read in one batch; merges keep their own first-parent diff.
|
|
1272
|
+
const cache = loadRevisionFilesCache(root);
|
|
1273
|
+
const needed = [...landings, ...humans.filter((human) => landings.some((landing) => human.ms > landing.ms && human.ms <= landing.ms + REVISION_WINDOW_MS))];
|
|
1274
|
+
const missing = [];
|
|
1275
|
+
for (const commit of needed) {
|
|
1276
|
+
const hit = cache.entries[commit.hash];
|
|
1277
|
+
if (Array.isArray(hit)) cache.used[commit.hash] = hit;
|
|
1278
|
+
else if (commit.parents.length < 2) missing.push(commit);
|
|
1279
|
+
}
|
|
1280
|
+
for (const [hash, files] of plainCommitFiles(root, missing)) {
|
|
1281
|
+
cache.used[hash] = files;
|
|
1282
|
+
cache.dirty = true;
|
|
1283
|
+
}
|
|
1154
1284
|
const filesOf = (commit) => {
|
|
1155
|
-
if (!
|
|
1156
|
-
|
|
1285
|
+
if (!cache.used[commit.hash]) {
|
|
1286
|
+
cache.used[commit.hash] = commitFiles(root, commit);
|
|
1287
|
+
cache.dirty = true;
|
|
1288
|
+
}
|
|
1289
|
+
return cache.used[commit.hash];
|
|
1157
1290
|
};
|
|
1158
1291
|
|
|
1159
1292
|
const revisions = [];
|
|
@@ -1177,6 +1310,7 @@ function collectRevisionSignals(root, options = {}) {
|
|
|
1177
1310
|
});
|
|
1178
1311
|
}
|
|
1179
1312
|
}
|
|
1313
|
+
saveRevisionFilesCache(cache);
|
|
1180
1314
|
|
|
1181
1315
|
return {
|
|
1182
1316
|
schema: REVISIONS_SCHEMA,
|
package/commands/integrations.js
CHANGED
|
@@ -33,11 +33,12 @@ const {
|
|
|
33
33
|
const fs = require('fs');
|
|
34
34
|
const os = require('os');
|
|
35
35
|
const path = require('path');
|
|
36
|
+
const { stateHome } = require('../lib/state-home');
|
|
36
37
|
const readline = require('readline');
|
|
37
38
|
const { spawnSync } = require('child_process');
|
|
38
39
|
const { sleepSync } = require('../lib/sleep-sync');
|
|
39
40
|
|
|
40
|
-
const CALENDAR_CACHE_PATH = path.join(
|
|
41
|
+
const CALENDAR_CACHE_PATH = path.join(stateHome(), '.atris', 'calendar-events-cache.json');
|
|
41
42
|
const GMAIL_CONNECT_POLL_MS = 3000;
|
|
42
43
|
const GMAIL_CONNECT_TIMEOUT_MS = 3 * 60 * 1000;
|
|
43
44
|
const GMAIL_SEND_USAGE = 'usage: atris gmail send <to> <subject> <body...> [--body-file <path>] [--account <id>]';
|
|
@@ -182,7 +183,7 @@ function parseGmailVerdictsArgs(args = []) {
|
|
|
182
183
|
|
|
183
184
|
function gmailAccountStatePath() {
|
|
184
185
|
return process.env.ATRIS_GMAIL_ACCOUNT_FILE
|
|
185
|
-
|| path.join(
|
|
186
|
+
|| path.join(stateHome(), '.atris', 'gmail-account.json');
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
function readGmailStickyAccount() {
|
|
@@ -1794,7 +1795,7 @@ function normalizeContactLabel(value) {
|
|
|
1794
1795
|
}
|
|
1795
1796
|
|
|
1796
1797
|
function imessageLookupCachePath() {
|
|
1797
|
-
return path.join(
|
|
1798
|
+
return path.join(stateHome(), '.atris', 'cache', 'imessage-contacts.json');
|
|
1798
1799
|
}
|
|
1799
1800
|
|
|
1800
1801
|
function readImessageLookupCache() {
|
package/commands/loops.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const os = require('os');
|
|
15
15
|
const path = require('path');
|
|
16
|
+
const { stateHome } = require('../lib/state-home');
|
|
16
17
|
const { execSync, spawnSync } = require('child_process');
|
|
17
18
|
|
|
18
19
|
const MAX_LOG_AGE_DAYS = 14;
|
|
@@ -26,7 +27,7 @@ const SCAFFOLD_FILES = [
|
|
|
26
27
|
];
|
|
27
28
|
|
|
28
29
|
function heartbeatDir() {
|
|
29
|
-
return process.env.ATRIS_HEARTBEAT_DIR || path.join(
|
|
30
|
+
return process.env.ATRIS_HEARTBEAT_DIR || path.join(stateHome(), '.atris', 'heartbeat');
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
function readJson(filePath, fallback) {
|
package/commands/member.js
CHANGED
|
@@ -7,6 +7,7 @@ const { loadCredentials } = require('../utils/auth');
|
|
|
7
7
|
const { resolveBackendRoot } = require('../utils/backend-root');
|
|
8
8
|
const { apiRequestJson } = require('../utils/api');
|
|
9
9
|
const { runAliveTick } = require('../lib/member-alive');
|
|
10
|
+
const { memberRefusalReason } = require('../lib/member-type');
|
|
10
11
|
const { defaultObjectiveRunner } = require('../lib/default-runner');
|
|
11
12
|
const { readJson, writeJson } = require('../lib/json-file');
|
|
12
13
|
const { sleepSync } = require('../lib/sleep-sync');
|
|
@@ -306,6 +307,28 @@ function nearestMemberSlug(name, slugs) {
|
|
|
306
307
|
return bestScore <= Math.max(2, Math.floor(needle.length / 3)) ? best : null;
|
|
307
308
|
}
|
|
308
309
|
|
|
310
|
+
// A person is never run as an agent. Reads the member's own type and returns
|
|
311
|
+
// the plain refusal sentence, or '' when the member can run.
|
|
312
|
+
function memberRefusalFor(name, memberFile) {
|
|
313
|
+
try {
|
|
314
|
+
return memberRefusalReason(name, parseFrontmatter(fs.readFileSync(memberFile, 'utf8')) || {});
|
|
315
|
+
} catch {
|
|
316
|
+
return '';
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Same as requireMemberDir, then stops with a non-zero exit for a human or
|
|
321
|
+
// odd-typed member. Every path that runs a member as an agent starts here.
|
|
322
|
+
function requireRunnableMember(name) {
|
|
323
|
+
const paths = requireMemberDir(name);
|
|
324
|
+
const reason = memberRefusalFor(name, paths.memberFile);
|
|
325
|
+
if (reason) {
|
|
326
|
+
console.error(reason);
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
return paths;
|
|
330
|
+
}
|
|
331
|
+
|
|
309
332
|
function requireMemberDir(name) {
|
|
310
333
|
if (!name) {
|
|
311
334
|
console.error('Usage: atris member <goal|tick|review> <name> ...');
|
|
@@ -1077,7 +1100,7 @@ function memberRun(name, ...args) {
|
|
|
1077
1100
|
}
|
|
1078
1101
|
|
|
1079
1102
|
const asJson = hasFlag(args, '--json');
|
|
1080
|
-
const paths =
|
|
1103
|
+
const paths = requireRunnableMember(name);
|
|
1081
1104
|
// Parking only hides a member from the team views; a run by name still goes.
|
|
1082
1105
|
const { isParkedText } = require('../lib/member-park');
|
|
1083
1106
|
try {
|
|
@@ -4221,6 +4244,8 @@ name: ${name}
|
|
|
4221
4244
|
role: ${role}
|
|
4222
4245
|
description: ${description || `Handles ${role.toLowerCase()} tasks`}
|
|
4223
4246
|
version: 1.0.0
|
|
4247
|
+
# ai or human; a human member is a person and Atris never acts as them
|
|
4248
|
+
type: ai
|
|
4224
4249
|
|
|
4225
4250
|
skills: []
|
|
4226
4251
|
|
|
@@ -5418,7 +5443,12 @@ function isWikiMinerNoiseName(value) {
|
|
|
5418
5443
|
const name = compactSentence(value || '', 120);
|
|
5419
5444
|
if (!name) return true;
|
|
5420
5445
|
if (/^[A-Z]{2,}$/.test(name)) return false;
|
|
5421
|
-
return /^(?:i|me|my|mine|myself|we|us|our|ours|ourselves|you|your|yours|yourself|yourselves|he|him|his|himself|she|her|hers|herself|it|its|itself|they|them|their|theirs|themselves|this|that|these|those|the|wiki|mission)$/i.test(name);
|
|
5446
|
+
return /^(?:i|me|my|mine|myself|we|us|our|ours|ourselves|you|your|yours|yourself|yourselves|he|him|his|himself|she|her|hers|herself|it|its|itself|they|them|their|theirs|themselves|this|that|these|those|here|there|where|when|what|who|whom|which|why|how|everything|nothing|something|anything|everyone|anyone|someone|nobody|everybody|anybody|somebody|none|the|wiki|mission)$/i.test(name);
|
|
5447
|
+
}
|
|
5448
|
+
|
|
5449
|
+
function isWikiMinerSelfLoop(relationship) {
|
|
5450
|
+
const from = lowerCompact(relationship?.from);
|
|
5451
|
+
return from !== '' && from === lowerCompact(relationship?.to);
|
|
5422
5452
|
}
|
|
5423
5453
|
|
|
5424
5454
|
function normalizeWikiMinerExtraction(raw) {
|
|
@@ -5437,7 +5467,7 @@ function normalizeWikiMinerExtraction(raw) {
|
|
|
5437
5467
|
to: compactSentence(relationship?.to || '', 120),
|
|
5438
5468
|
type: allowedRelationTypes.has(String(relationship?.type || '').toLowerCase()) ? String(relationship.type).toLowerCase() : 'uses',
|
|
5439
5469
|
}))
|
|
5440
|
-
.filter((relationship) => !isWikiMinerNoiseName(relationship.from) && !isWikiMinerNoiseName(relationship.to));
|
|
5470
|
+
.filter((relationship) => !isWikiMinerNoiseName(relationship.from) && !isWikiMinerNoiseName(relationship.to) && !isWikiMinerSelfLoop(relationship));
|
|
5441
5471
|
return { entities, relationships };
|
|
5442
5472
|
}
|
|
5443
5473
|
|
|
@@ -5459,7 +5489,9 @@ function heuristicWikiMinerExtraction(pagePath, pageContent) {
|
|
|
5459
5489
|
}
|
|
5460
5490
|
const entities = [{ type: 'concept', name: title }];
|
|
5461
5491
|
for (const name of [...candidates].slice(0, 12)) entities.push({ type: /agent|api|sdk|cli|hub|system/i.test(name) ? 'system' : 'concept', name });
|
|
5462
|
-
const relationships = [...candidates].slice(0, 12)
|
|
5492
|
+
const relationships = [...candidates].slice(0, 12)
|
|
5493
|
+
.map((name) => ({ from: title, to: name, type: 'uses' }))
|
|
5494
|
+
.filter((relationship) => !isWikiMinerSelfLoop(relationship));
|
|
5463
5495
|
return { entities, relationships };
|
|
5464
5496
|
}
|
|
5465
5497
|
|
|
@@ -5513,11 +5545,12 @@ function mergeWikiGraph(graph, extraction, sourcePath) {
|
|
|
5513
5545
|
|
|
5514
5546
|
const relationshipMap = new Map();
|
|
5515
5547
|
for (const relationship of graph.relationships || []) {
|
|
5516
|
-
if (isWikiMinerNoiseName(relationship.from) || isWikiMinerNoiseName(relationship.to)) continue;
|
|
5548
|
+
if (isWikiMinerNoiseName(relationship.from) || isWikiMinerNoiseName(relationship.to) || isWikiMinerSelfLoop(relationship)) continue;
|
|
5517
5549
|
const key = `${lowerCompact(relationship.from)}|${String(relationship.type || 'uses').toLowerCase()}|${lowerCompact(relationship.to)}`;
|
|
5518
5550
|
relationshipMap.set(key, { ...relationship, sources: Array.isArray(relationship.sources) ? relationship.sources : [] });
|
|
5519
5551
|
}
|
|
5520
5552
|
for (const relationship of extraction.relationships || []) {
|
|
5553
|
+
if (isWikiMinerNoiseName(relationship.from) || isWikiMinerNoiseName(relationship.to) || isWikiMinerSelfLoop(relationship)) continue;
|
|
5521
5554
|
const key = `${lowerCompact(relationship.from)}|${relationship.type}|${lowerCompact(relationship.to)}`;
|
|
5522
5555
|
const existing = relationshipMap.get(key) || { from: relationship.from, to: relationship.to, type: relationship.type, sources: [] };
|
|
5523
5556
|
if (sourcePath && !existing.sources.includes(sourcePath)) existing.sources.push(sourcePath);
|
|
@@ -5794,7 +5827,6 @@ async function callSupervisorLlm(receipts, logs) {
|
|
|
5794
5827
|
const output = { isTTY: false, write() { return true; } };
|
|
5795
5828
|
const result = await postTurn(supervisorPrompt(receipts, logs), {
|
|
5796
5829
|
mode: process.env.ATRIS_SUPERVISOR_LLM_MODE || 'fast',
|
|
5797
|
-
route: 'local',
|
|
5798
5830
|
cwd: process.cwd(),
|
|
5799
5831
|
output,
|
|
5800
5832
|
color: false,
|
|
@@ -5880,11 +5912,21 @@ async function runSupervisorWake(name, paths, { execute = false } = {}) {
|
|
|
5880
5912
|
analysis = normalizeSupervisorAnalysis(llm.analysis, { status: 'ok', llm_source: llm.source });
|
|
5881
5913
|
if (!analysis) {
|
|
5882
5914
|
reason = 'llm_json_parse_failed';
|
|
5883
|
-
analysis =
|
|
5915
|
+
analysis = {
|
|
5916
|
+
...fallbackSupervisorAnalysis(receipts),
|
|
5917
|
+
status: 'parse_error',
|
|
5918
|
+
llm_source: llm.source,
|
|
5919
|
+
llm_error: 'invalid_analysis_shape',
|
|
5920
|
+
};
|
|
5884
5921
|
}
|
|
5885
5922
|
} else if (llm?.error) {
|
|
5886
5923
|
reason = llm.error === 'invalid_json' ? 'llm_json_parse_failed' : 'llm_analysis_failed';
|
|
5887
|
-
analysis =
|
|
5924
|
+
analysis = {
|
|
5925
|
+
...fallbackSupervisorAnalysis(receipts),
|
|
5926
|
+
status: llm.error === 'invalid_json' ? 'parse_error' : 'llm_error',
|
|
5927
|
+
llm_source: llm.source,
|
|
5928
|
+
llm_error: llm.error,
|
|
5929
|
+
};
|
|
5888
5930
|
} else {
|
|
5889
5931
|
reason = 'llm_not_configured';
|
|
5890
5932
|
analysis = fallbackSupervisorAnalysis(receipts);
|
|
@@ -7326,7 +7368,7 @@ async function runGeneralistWake(name, paths, { execute = false, domainInput = {
|
|
|
7326
7368
|
record: domain.ok && proof.status === 'ok',
|
|
7327
7369
|
libraryBefore: crossDomainLibraryBefore,
|
|
7328
7370
|
receiptPath,
|
|
7329
|
-
proofPath,
|
|
7371
|
+
proofPath: execute ? proofPath : null,
|
|
7330
7372
|
});
|
|
7331
7373
|
proof.learning = {
|
|
7332
7374
|
library_path: path.relative(root, learning.library_path),
|
|
@@ -7757,6 +7799,24 @@ function wakeRuleDirtyMemberScope(ctx) {
|
|
|
7757
7799
|
};
|
|
7758
7800
|
}
|
|
7759
7801
|
|
|
7802
|
+
// Rule: a goal pinned to a terminal mission must not mint another doomed tick.
|
|
7803
|
+
// The goal stays active until a human retires it or revives the mission, so the
|
|
7804
|
+
// wake surfaces that decision instead of proposing `member tick --goal` on a
|
|
7805
|
+
// dead spine. Unknown mission ids pass through: they may live in a worktree.
|
|
7806
|
+
function wakeRuleTerminalMissionGoal(ctx) {
|
|
7807
|
+
const mission = ctx.goalTerminalMission;
|
|
7808
|
+
if (!ctx.goal || !mission) return null;
|
|
7809
|
+
return {
|
|
7810
|
+
decision: 'ask',
|
|
7811
|
+
reason: `goal_mission_terminal:${mission.status}`,
|
|
7812
|
+
needs_user: true,
|
|
7813
|
+
ask: `Goal "${ctx.goal.title}" is pinned to mission ${mission.id} (${mission.status}). Retire the goal or revive the mission, then wake again.`,
|
|
7814
|
+
next_command: `atris member goal-from-mission ${ctx.name}`,
|
|
7815
|
+
goal: ctx.goal,
|
|
7816
|
+
current_experiment: ctx.current || null,
|
|
7817
|
+
};
|
|
7818
|
+
}
|
|
7819
|
+
|
|
7760
7820
|
// Rule: nothing blocks the member, so take the safe next bounded step.
|
|
7761
7821
|
function wakeRuleSafeBoundedTick(ctx) {
|
|
7762
7822
|
return {
|
|
@@ -7777,6 +7837,7 @@ const WAKE_DECISION_RULES = [
|
|
|
7777
7837
|
wakeRuleOpenReviewableProposal,
|
|
7778
7838
|
wakeRuleScoredNonTickCandidate,
|
|
7779
7839
|
wakeRuleDirtyMemberScope,
|
|
7840
|
+
wakeRuleTerminalMissionGoal,
|
|
7780
7841
|
wakeRuleSafeBoundedTick,
|
|
7781
7842
|
];
|
|
7782
7843
|
|
|
@@ -7790,6 +7851,8 @@ function wakeDecision(name, paths, { force = false, runtimeKind = memberRuntimeK
|
|
|
7790
7851
|
const steering = readSteeringMemory(paths, name);
|
|
7791
7852
|
const state = loadMemberGoals(name, paths);
|
|
7792
7853
|
const goal = activeGoal(state);
|
|
7854
|
+
const pinnedMission = goal?.mission_id ? memberRunMissionMap().get(String(goal.mission_id).trim()) || null : null;
|
|
7855
|
+
const goalTerminalMission = pinnedMission && MISSION_TERMINAL_STATUSES.has(lowerCompact(pinnedMission.status || '')) ? pinnedMission : null;
|
|
7793
7856
|
const current = memberOpenExperiment(state);
|
|
7794
7857
|
const generalistDecision = wakeRuleGeneralistDomainFile(name, { purpose, steering, state, goal, current });
|
|
7795
7858
|
if (generalistDecision) return generalistDecision;
|
|
@@ -7835,7 +7898,7 @@ function wakeDecision(name, paths, { force = false, runtimeKind = memberRuntimeK
|
|
|
7835
7898
|
next_command: wakeScores.selected.next_command,
|
|
7836
7899
|
} : null;
|
|
7837
7900
|
|
|
7838
|
-
const ctx = { name, force, purpose, nowFile, steering, state, goal, current, blocked, evidence, workspace, checks, wakeScores };
|
|
7901
|
+
const ctx = { name, force, purpose, nowFile, steering, state, goal, goalTerminalMission, current, blocked, evidence, workspace, checks, wakeScores };
|
|
7839
7902
|
for (const rule of WAKE_DECISION_RULES) {
|
|
7840
7903
|
const verdict = rule(ctx);
|
|
7841
7904
|
if (verdict) return composeWakeDecision(ctx, verdict);
|
|
@@ -7863,6 +7926,8 @@ function wakeMemberEngine(name, root = process.cwd()) {
|
|
|
7863
7926
|
|
|
7864
7927
|
async function runMemberWake(name, { execute = false, confirmed = false, force = false, domainInput = {} } = {}) {
|
|
7865
7928
|
const paths = requireMemberDir(name);
|
|
7929
|
+
const refusal = memberRefusalFor(name, paths.memberFile);
|
|
7930
|
+
if (refusal) throw new Error(refusal);
|
|
7866
7931
|
const runtimeKind = paths.runtimeKind || memberRuntimeKind(name);
|
|
7867
7932
|
if (runtimeKind === 'auto-improver') {
|
|
7868
7933
|
return runAutoImproverWake(name, paths, { execute, confirmed });
|
|
@@ -8176,6 +8241,7 @@ function clipText(value, max = 70) {
|
|
|
8176
8241
|
}
|
|
8177
8242
|
|
|
8178
8243
|
async function memberWake(name, ...args) {
|
|
8244
|
+
if (name) requireRunnableMember(name);
|
|
8179
8245
|
const asJson = hasFlag(args, '--json');
|
|
8180
8246
|
const execute = hasFlag(args, '--execute') && !hasFlag(args, '--dry-run');
|
|
8181
8247
|
const confirmed = hasFlag(args, '--confirm-autonomy-policy');
|
|
@@ -8223,14 +8289,15 @@ function collectFleetActivationRows(root = process.cwd()) {
|
|
|
8223
8289
|
const goalPlane = memberGoalPlaneStatus(slug, paths);
|
|
8224
8290
|
const activeMission = memberLiveMission(slug, missionMap);
|
|
8225
8291
|
const latestMission = memberLatestMission(slug, missionMap);
|
|
8226
|
-
const
|
|
8292
|
+
const refusal = memberRefusalFor(slug, paths.memberFile);
|
|
8293
|
+
const skipped = Boolean(refusal) || (!goalPlane.goal && !activeMission);
|
|
8227
8294
|
return {
|
|
8228
8295
|
member: slug,
|
|
8229
8296
|
goal: goalPlane.goal || null,
|
|
8230
8297
|
active_mission: activeMission || null,
|
|
8231
8298
|
latest_mission: latestMission || null,
|
|
8232
8299
|
skipped,
|
|
8233
|
-
skip_reason: skipped ? 'no_goal_and_no_active_mission' : null,
|
|
8300
|
+
skip_reason: refusal ? 'not_ai' : skipped ? 'no_goal_and_no_active_mission' : null,
|
|
8234
8301
|
};
|
|
8235
8302
|
})
|
|
8236
8303
|
.sort((a, b) => a.member.localeCompare(b.member));
|
|
@@ -8263,7 +8330,7 @@ async function memberLoopAll(kind, ...args) {
|
|
|
8263
8330
|
for (const row of rows) {
|
|
8264
8331
|
if (row.skipped) {
|
|
8265
8332
|
skipped.push(row);
|
|
8266
|
-
if (!asJson) console.log(`Skip ${row.member}: no goal and no active mission
|
|
8333
|
+
if (!asJson) console.log(`Skip ${row.member}: ${row.skip_reason === 'not_ai' ? 'not an AI member.' : 'no goal and no active mission.'}`);
|
|
8267
8334
|
continue;
|
|
8268
8335
|
}
|
|
8269
8336
|
if (!asJson) console.log(`Activate ${row.member}: ${row.goal ? 'goal' : `mission ${row.active_mission?.id || 'active'}`}`);
|
|
@@ -8675,7 +8742,7 @@ function memberLoopRecordResult(name, paths, lease, run) {
|
|
|
8675
8742
|
// Driver: the phases above do the work; this reads as setup -> gate -> run -> record.
|
|
8676
8743
|
async function memberLoop(name, ...args) {
|
|
8677
8744
|
if (name === '--all' || name === 'all') return memberLoopAll('loop', ...args);
|
|
8678
|
-
|
|
8745
|
+
requireRunnableMember(name);
|
|
8679
8746
|
const asJson = hasFlag(args, '--json');
|
|
8680
8747
|
const aliveMode = hasFlag(args, '--alive');
|
|
8681
8748
|
const execute = hasFlag(args, '--execute');
|
|
@@ -8739,7 +8806,7 @@ async function memberLoop(name, ...args) {
|
|
|
8739
8806
|
}
|
|
8740
8807
|
|
|
8741
8808
|
async function memberTick(name, ...args) {
|
|
8742
|
-
const paths =
|
|
8809
|
+
const paths = requireRunnableMember(name);
|
|
8743
8810
|
const asJson = hasFlag(args, '--json');
|
|
8744
8811
|
const force = hasFlag(args, '--force');
|
|
8745
8812
|
const goalId = readFlag(args, '--goal', '');
|
package/commands/mission.js
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
const {
|
|
23
23
|
resolveClaudeRunnerModel,
|
|
24
24
|
resolvePinnedRunnerModel,
|
|
25
|
+
runnerModelFor,
|
|
25
26
|
resolveClaudeRunnerBin,
|
|
26
27
|
resolveClaudeRunnerCommandTemplate,
|
|
27
28
|
buildRunnerCommand,
|
|
@@ -568,6 +569,8 @@ function applyMissionRunnerProfile(runner) {
|
|
|
568
569
|
// default model pin. Every other engine's CLI rejects a claude model name, so
|
|
569
570
|
// it only ever takes an explicit pin (the mission's model or the roster
|
|
570
571
|
// line's); '' lets the engine ride its own default.
|
|
572
|
+
// A mission often still carries the claude model it was created with; an
|
|
573
|
+
// engine that runs only its own models (codex, grok) drops it.
|
|
571
574
|
const CLAUDE_FAMILY_RUNNER_ENGINES = new Set(['claude', 'fable', 'haiku']);
|
|
572
575
|
|
|
573
576
|
function resolveMissionTickRunnerModel(mission) {
|
|
@@ -575,7 +578,7 @@ function resolveMissionTickRunnerModel(mission) {
|
|
|
575
578
|
if (!engine || CLAUDE_FAMILY_RUNNER_ENGINES.has(engine)) {
|
|
576
579
|
return resolveClaudeRunnerModel(mission);
|
|
577
580
|
}
|
|
578
|
-
return resolvePinnedRunnerModel(mission);
|
|
581
|
+
return runnerModelFor(engine, resolvePinnedRunnerModel(mission));
|
|
579
582
|
}
|
|
580
583
|
|
|
581
584
|
function exitMissionError(message, code = 1, asJson = false) {
|
package/commands/pack-craft.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
const os = require('os');
|
|
5
4
|
const path = require('path');
|
|
5
|
+
const { stateHome } = require('../lib/state-home');
|
|
6
6
|
|
|
7
7
|
function slugify(value, fallback = 'atris-pack') {
|
|
8
8
|
return String(value || fallback)
|
|
@@ -69,7 +69,7 @@ function isNonEmptyTarget(targetDir) {
|
|
|
69
69
|
function defaultAuthor() {
|
|
70
70
|
try {
|
|
71
71
|
const creds = JSON.parse(
|
|
72
|
-
fs.readFileSync(path.join(
|
|
72
|
+
fs.readFileSync(path.join(stateHome(), '.atris', 'credentials.json'), 'utf8')
|
|
73
73
|
);
|
|
74
74
|
if (creds && typeof creds.email === 'string' && creds.email.trim()) return creds.email.trim();
|
|
75
75
|
} catch {
|
package/commands/publish.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const
|
|
3
|
+
const { stateHome } = require('../lib/state-home');
|
|
4
4
|
const { loadCredentials } = require('../utils/auth');
|
|
5
5
|
const { apiRequestJson } = require('../utils/api');
|
|
6
6
|
|
|
@@ -104,7 +104,7 @@ async function publishAtris() {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// Save local copy
|
|
107
|
-
const localDir = path.join(
|
|
107
|
+
const localDir = path.join(stateHome(), '.atris', 'templates', name.toLowerCase().replace(/[^a-z0-9-]/g, '-'));
|
|
108
108
|
fs.mkdirSync(localDir, { recursive: true });
|
|
109
109
|
|
|
110
110
|
for (const f of files) {
|
package/commands/radar.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { stateHome } = require('../lib/state-home');
|
|
6
7
|
const { execFileSync } = require('child_process');
|
|
7
8
|
const { parseScopeFlag, pathUnderRoot } = require('../lib/cli-scope');
|
|
8
9
|
|
|
@@ -346,7 +347,7 @@ function loadBusinessCollaboration(root, deps, team = {}, scorecardCount = count
|
|
|
346
347
|
const business = readJsonFile(path.join(root, '.atris', 'business.json'), deps, null);
|
|
347
348
|
const runtime = readJsonFile(path.join(root, '.atris', 'state', 'runtime.json'), deps, null);
|
|
348
349
|
const sync = readJsonFile(path.join(root, '.atris', 'state', '_sync.json'), deps, null);
|
|
349
|
-
const cache = readJsonFile(path.join(deps.homeDir ||
|
|
350
|
+
const cache = readJsonFile(path.join(deps.homeDir || stateHome(), '.atris', 'businesses.json'), deps, {}) || {};
|
|
350
351
|
const slug = business?.slug || sync?.workspace_slug || null;
|
|
351
352
|
const cacheEntry = slug && cache ? cache[slug] : null;
|
|
352
353
|
const hasAtris = deps.exists(path.join(root, 'atris'));
|
|
@@ -804,7 +805,7 @@ function collectRadar(options = {}) {
|
|
|
804
805
|
readlink: options.readlinkSync || fs.readlinkSync,
|
|
805
806
|
readdir: options.readdirSync || fs.readdirSync,
|
|
806
807
|
platform: options.platform || os.platform(),
|
|
807
|
-
homeDir: options.homeDir ||
|
|
808
|
+
homeDir: options.homeDir || stateHome(),
|
|
808
809
|
};
|
|
809
810
|
const nowMs = options.nowMs || Date.now();
|
|
810
811
|
const tasks = loadTasks(root, deps);
|
package/commands/router.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require('node:crypto');
|
|
4
4
|
const fs = require('node:fs');
|
|
5
|
-
const os = require('node:os');
|
|
6
5
|
const path = require('node:path');
|
|
6
|
+
const { stateHome } = require('../lib/state-home');
|
|
7
7
|
const { spawnSync } = require('node:child_process');
|
|
8
8
|
const { loadOverrides, pickLane } = require('../lib/ax-auto-lane');
|
|
9
9
|
|
|
@@ -28,7 +28,7 @@ function readFirstNamedFlag(args, names) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function resolveRouterPaths(args = [], env = process.env) {
|
|
31
|
-
const home = env
|
|
31
|
+
const home = stateHome(env);
|
|
32
32
|
return {
|
|
33
33
|
picks: path.resolve(readFirstNamedFlag(args, ['--picks', '--picks-path'])
|
|
34
34
|
|| env.ATRIS_ROUTER_PICKS_PATH
|