atris 3.63.0 → 3.65.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/README.md +4 -0
- package/atris/skills/commerce/SKILL.md +24 -0
- package/atris/skills/x-search/SKILL.md +3 -1
- package/ax +4 -3
- package/bin/atris.js +28 -2
- package/cli/atris_code.py +25 -1
- package/commands/agent-spawn.js +1 -0
- 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 +3 -3
- package/commands/deck.js +2 -2
- package/commands/engine.js +292 -65
- package/commands/feedback.js +2 -2
- package/commands/fork.js +2 -2
- package/commands/gm.js +2 -1
- package/commands/hire.js +189 -0
- package/commands/improve.js +142 -8
- package/commands/integrations.js +4 -3
- package/commands/loops.js +2 -1
- package/commands/mail.js +163 -0
- package/commands/member.js +47 -9
- package/commands/mission.js +4 -3
- 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/stream.js +22 -1
- package/commands/task.js +84 -2
- package/commands/team.js +355 -67
- package/commands/transactions.js +3 -3
- package/commands/truth.js +2 -2
- package/commands/x-search.js +81 -10
- package/commands/youtube.js +2 -1
- package/lib/auto-accept-certified.js +83 -9
- package/lib/autoland.js +3 -2
- package/lib/ax-auto-lane.js +2 -2
- package/lib/brief-rules.js +11 -0
- package/lib/business-target.js +131 -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 +17 -2
- package/lib/deck-history.js +2 -2
- package/lib/deck-review.js +3 -3
- package/lib/design-api.js +2 -2
- package/lib/engine-processes.js +430 -0
- package/lib/engine-registry.js +101 -21
- 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-identity.js +56 -0
- package/lib/pack-capabilities.js +2 -1
- package/lib/permission-grants.js +2 -2
- package/lib/pulse.js +5 -5
- package/lib/roster-models.js +11 -0
- package/lib/roster-runs.js +13 -2
- package/lib/roster-stale.js +241 -0
- package/lib/runner-command.js +22 -6
- 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/team-lineup.js +10 -71
- package/lib/team-presence.js +224 -0
- package/lib/text-table.js +81 -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 +61 -15
- package/scripts/det/ytrail-bench.js +2 -1
- package/utils/auth.js +3 -3
- package/utils/update-check.js +2 -2
package/lib/engine-registry.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('./state-home');
|
|
6
7
|
const childProcess = require('child_process');
|
|
7
8
|
const crypto = require('crypto');
|
|
8
9
|
const {
|
|
@@ -155,7 +156,10 @@ const ENGINE_SEED_META = Object.freeze({
|
|
|
155
156
|
agy: Object.freeze({
|
|
156
157
|
tier: 'pro',
|
|
157
158
|
roles: Object.freeze(['executor']),
|
|
159
|
+
// The plain flash name takes its effort from --effort; the -high names
|
|
160
|
+
// bake the effort into the model.
|
|
158
161
|
models: Object.freeze([
|
|
162
|
+
'gemini-3.8-flash',
|
|
159
163
|
'gemini-3.8-flash-high',
|
|
160
164
|
'gemini-3.7-flash-high',
|
|
161
165
|
'gemini-3.1-pro-high',
|
|
@@ -402,6 +406,15 @@ const MACHINE_ROSTER_SCHEMA = 'atris.machine_roster.v1';
|
|
|
402
406
|
// The owner's own job for small, tightly specified builds. A build resolved
|
|
403
407
|
// with lowStakes checks this pick first, then the plain build pick.
|
|
404
408
|
const SMALL_BUILD_JOB = 'small-build';
|
|
409
|
+
// Jobs any engine may fill, keyed to the kind they count as when assign is
|
|
410
|
+
// not told one. YouTube notes are plain read-only text, so agy (build-only)
|
|
411
|
+
// and haiku (search and review) can both write them. Every other job keeps
|
|
412
|
+
// its kind check.
|
|
413
|
+
const ANY_ENGINE_JOBS = Object.freeze({ notes: 'search' });
|
|
414
|
+
|
|
415
|
+
function jobTakesAnyEngine(key) {
|
|
416
|
+
return Object.prototype.hasOwnProperty.call(ANY_ENGINE_JOBS, String(key || ''));
|
|
417
|
+
}
|
|
405
418
|
|
|
406
419
|
function rosterJob(name) {
|
|
407
420
|
const job = String(name || '').trim().toLowerCase();
|
|
@@ -472,10 +485,10 @@ function customRosterJobKeys(roster) {
|
|
|
472
485
|
// an engine and model that can run, and that engine (or its backup) is ready.
|
|
473
486
|
// A pick that cannot run never sets the job's kind, so it cannot block a
|
|
474
487
|
// ready all-projects pick of another kind.
|
|
475
|
-
function rosterPickLive(pick, now, registry) {
|
|
488
|
+
function rosterPickLive(pick, now, registry, key) {
|
|
476
489
|
const role = customPickRole(pick);
|
|
477
490
|
if (!role || !canonicalEngineName(pick.engine)) return false;
|
|
478
|
-
return Boolean(rosterChoiceForPick(pick, role, registry, { label: rosterJobLabel(role), now }));
|
|
491
|
+
return Boolean(rosterChoiceForPick(pick, role, registry, { label: rosterJobLabel(role), now, anyEngine: jobTakesAnyEngine(key) }));
|
|
479
492
|
}
|
|
480
493
|
|
|
481
494
|
// The role a job name resolves as: a built-in job's own role, else the kind
|
|
@@ -491,7 +504,7 @@ function rosterJobRole(name, root = process.cwd(), options = {}) {
|
|
|
491
504
|
const machine = options.machineRosterPicks || readMachineRoster(options);
|
|
492
505
|
const picks = [session && session[key], project && project[key], machine && machine[key]].map(rosterPickObject).filter(Boolean);
|
|
493
506
|
const registry = options.registry || readEngineRegistry(root, { persist: false });
|
|
494
|
-
const live = picks.find((pick) => rosterPickLive(pick, options.now, registry));
|
|
507
|
+
const live = picks.find((pick) => rosterPickLive(pick, options.now, registry, key));
|
|
495
508
|
if (live) return customPickRole(live);
|
|
496
509
|
const kind = inferJobKind(name);
|
|
497
510
|
if (kind) return ENGINE_JOBS[kind];
|
|
@@ -612,7 +625,7 @@ function routerSkipsRole(engine, role) {
|
|
|
612
625
|
function machineRosterFile(options = {}) {
|
|
613
626
|
return options.machineRosterFile
|
|
614
627
|
|| process.env.ATRIS_MACHINE_ROSTER_PATH
|
|
615
|
-
|| path.join(
|
|
628
|
+
|| path.join(stateHome(), '.atris', 'roster.json');
|
|
616
629
|
}
|
|
617
630
|
|
|
618
631
|
// The all-projects ROSTER.md. A scratch roster.json path (tests, sandboxes)
|
|
@@ -624,7 +637,7 @@ function machineRosterMarkdownFile(options = {}) {
|
|
|
624
637
|
if (options.machineRosterFile || process.env.ATRIS_MACHINE_ROSTER_PATH) {
|
|
625
638
|
return path.join(path.dirname(machineRosterFile(options)), 'ROSTER.md');
|
|
626
639
|
}
|
|
627
|
-
return path.join(
|
|
640
|
+
return path.join(stateHome(), '.atris', 'ROSTER.md');
|
|
628
641
|
}
|
|
629
642
|
|
|
630
643
|
function projectRosterFile(root = process.cwd()) {
|
|
@@ -902,7 +915,7 @@ function rosterPickFromValue(value, role, options = {}) {
|
|
|
902
915
|
extras = [inlineMax[1], ...extras];
|
|
903
916
|
}
|
|
904
917
|
const primary = resolveRosterWords(main);
|
|
905
|
-
if (!engineCanTake(primary.engine, role)) throw new Error(`${primary.engine} cannot do ${kind} work`);
|
|
918
|
+
if (!options.anyEngine && !engineCanTake(primary.engine, role)) throw new Error(`${primary.engine} cannot do ${kind} work`);
|
|
906
919
|
let backup = null;
|
|
907
920
|
let until = '';
|
|
908
921
|
let untilYearless = '';
|
|
@@ -910,16 +923,25 @@ function rosterPickFromValue(value, role, options = {}) {
|
|
|
910
923
|
let namedModel = false;
|
|
911
924
|
let namedEffort = false;
|
|
912
925
|
let prep = '';
|
|
926
|
+
let verified = '';
|
|
913
927
|
for (const raw of extras) {
|
|
914
|
-
// "max: 20 min"
|
|
915
|
-
|
|
928
|
+
// "max: 20 min", "until: 2026-10-24", and "verified: 2026-09-30" read
|
|
929
|
+
// like "max 20 min".
|
|
930
|
+
const part = raw.replace(/^(max|until|verified)\s*:\s*/i, '$1 ');
|
|
916
931
|
const backupMatch = /^backup\s+(.+)$/i.exec(part);
|
|
917
932
|
const untilMatch = /^until\s+(.+)$/i.exec(part);
|
|
918
933
|
const maxMatch = /^max\b/i.test(part) && !/^max$/i.test(part);
|
|
919
934
|
const modelMatch = /^model\s*(?::\s*|\s+)(.+)$/i.exec(part);
|
|
920
935
|
const effortMatch = /^effort\s*(?::\s*|\s+)(.+)$/i.exec(part);
|
|
921
936
|
const prepMatch = /^prep\s*(?::\s*|\s+)(.+)$/i.exec(part);
|
|
922
|
-
|
|
937
|
+
const verifiedMatch = /^verified\s+(.+)$/i.exec(part);
|
|
938
|
+
if (verifiedMatch && !verified) {
|
|
939
|
+
// "verified 2026-09-30": the day a person last saw this model answer.
|
|
940
|
+
// The roster flags a pin whose newest proof is over 30 days old.
|
|
941
|
+
const words = verifiedMatch[1].trim();
|
|
942
|
+
verified = parseRosterUntilWords(words);
|
|
943
|
+
if (!verified) throw new Error(`"verified ${words}" is not a date; use verified 2026-09-30`);
|
|
944
|
+
} else if (prepMatch && !prep) {
|
|
923
945
|
// "prep: search": that job's lead reads the task first and hands this
|
|
924
946
|
// worker a trimmed brief. Whether it names this same job is checked
|
|
925
947
|
// where the job is known.
|
|
@@ -946,7 +968,7 @@ function rosterPickFromValue(value, role, options = {}) {
|
|
|
946
968
|
throw new Error('a worker line takes no backup; put the backup on the next line');
|
|
947
969
|
} else if (backupMatch && !backup) {
|
|
948
970
|
backup = resolveRosterWords(backupMatch[1]);
|
|
949
|
-
if (!engineCanTake(backup.engine, role)) throw new Error(`the backup ${backup.engine} cannot do ${kind} work`);
|
|
971
|
+
if (!options.anyEngine && !engineCanTake(backup.engine, role)) throw new Error(`the backup ${backup.engine} cannot do ${kind} work`);
|
|
950
972
|
} else if (untilMatch && !until) {
|
|
951
973
|
const words = untilMatch[1].trim();
|
|
952
974
|
until = parseRosterUntilWords(words);
|
|
@@ -975,6 +997,7 @@ function rosterPickFromValue(value, role, options = {}) {
|
|
|
975
997
|
until,
|
|
976
998
|
...(until ? {} : { never_expires: true }),
|
|
977
999
|
...(untilYearless ? { until_needs_year: untilYearless } : {}),
|
|
1000
|
+
...(verified ? { verified } : {}),
|
|
978
1001
|
};
|
|
979
1002
|
}
|
|
980
1003
|
|
|
@@ -1017,6 +1040,7 @@ function rosterWorkerFromValue(value, role, options = {}) {
|
|
|
1017
1040
|
until: pick.until,
|
|
1018
1041
|
...(pick.never_expires ? { never_expires: true } : {}),
|
|
1019
1042
|
...(pick.until_needs_year ? { until_needs_year: pick.until_needs_year } : {}),
|
|
1043
|
+
...(pick.verified ? { verified: pick.verified } : {}),
|
|
1020
1044
|
};
|
|
1021
1045
|
}
|
|
1022
1046
|
|
|
@@ -1065,6 +1089,7 @@ function pickWorkers(pick) {
|
|
|
1065
1089
|
...(pick.prep ? { prep: pick.prep } : {}),
|
|
1066
1090
|
until: pick.until,
|
|
1067
1091
|
...(pick.never_expires ? { never_expires: true } : {}),
|
|
1092
|
+
...(pick.verified ? { verified: pick.verified } : {}),
|
|
1068
1093
|
...(pick.line ? { line: pick.line } : {}),
|
|
1069
1094
|
}];
|
|
1070
1095
|
if (pick.backup) {
|
|
@@ -1112,7 +1137,7 @@ function parseRosterMarkdown(text, file, options = {}) {
|
|
|
1112
1137
|
const workers = [];
|
|
1113
1138
|
for (const entry of lines) {
|
|
1114
1139
|
try {
|
|
1115
|
-
const worker = rosterWorkerFromValue(entry.value, ENGINE_JOBS[kind], options);
|
|
1140
|
+
const worker = rosterWorkerFromValue(entry.value, ENGINE_JOBS[kind], { ...options, anyEngine: jobTakesAnyEngine(key) });
|
|
1116
1141
|
const selfPrep = selfPrepWarning(worker, key, label);
|
|
1117
1142
|
if (selfPrep) warn(entry, selfPrep);
|
|
1118
1143
|
workers.push({ ...worker, line: entry.lineNumber, text: entry.raw });
|
|
@@ -1184,7 +1209,7 @@ function parseRosterMarkdown(text, file, options = {}) {
|
|
|
1184
1209
|
continue;
|
|
1185
1210
|
}
|
|
1186
1211
|
try {
|
|
1187
|
-
const pick = rosterPickFromValue(entry.value, ENGINE_JOBS[kind], options);
|
|
1212
|
+
const pick = rosterPickFromValue(entry.value, ENGINE_JOBS[kind], { ...options, anyEngine: jobTakesAnyEngine(key) });
|
|
1188
1213
|
const selfPrep = selfPrepWarning(pick, key, label);
|
|
1189
1214
|
if (selfPrep) warn(entry, selfPrep);
|
|
1190
1215
|
picks[key] = { ...pick, ...(builtIn ? {} : { like: kind }), file: display, line: entry.lineNumber };
|
|
@@ -1264,7 +1289,7 @@ function sessionRosterDir(options = {}) {
|
|
|
1264
1289
|
if (options.sessionRosterDir) return options.sessionRosterDir;
|
|
1265
1290
|
if (process.env.ATRIS_ROSTER_SESSIONS_DIR) return process.env.ATRIS_ROSTER_SESSIONS_DIR;
|
|
1266
1291
|
if (options.machineRosterFile || process.env.ATRIS_MACHINE_ROSTER_PATH) return path.join(path.dirname(machineRosterFile(options)), 'sessions');
|
|
1267
|
-
return path.join(
|
|
1292
|
+
return path.join(stateHome(), '.atris', 'sessions');
|
|
1268
1293
|
}
|
|
1269
1294
|
|
|
1270
1295
|
// Like the all-projects roster: off in test runs unless a test points the
|
|
@@ -1370,6 +1395,7 @@ function workerLineText(worker) {
|
|
|
1370
1395
|
if (Number(worker.max_seconds) > 0) parts.push(`max: ${rosterMaxText(worker.max_seconds).replace(/^max /, '')}`);
|
|
1371
1396
|
if (worker.prep) parts.push(`prep: ${rosterJobLabel(worker.prep)}`);
|
|
1372
1397
|
if (worker.until) parts.push(`until ${worker.until}`);
|
|
1398
|
+
if (worker.verified) parts.push(`verified ${worker.verified}`);
|
|
1373
1399
|
return `- ${parts.join(', ')}`;
|
|
1374
1400
|
}
|
|
1375
1401
|
|
|
@@ -1531,13 +1557,14 @@ function assignedWorkerLines(key, engineName, options, context) {
|
|
|
1531
1557
|
if (likeName && !likeKind) throw new Error(`unknown kind "${likeName}". use --like search, build, or review`);
|
|
1532
1558
|
if (builtIn && likeKind && ENGINE_JOBS[likeKind] !== key) throw new Error(`${label} is a built-in job; --like is only for your own jobs`);
|
|
1533
1559
|
const savedKind = builtIn ? '' : Object.keys(ENGINE_JOBS).find((kind) => ENGINE_JOBS[kind] === customPickRole(savedPick)) || '';
|
|
1534
|
-
const kind = builtIn ? label : likeKind || inferJobKind(jobName) || savedKind;
|
|
1560
|
+
const kind = builtIn ? label : likeKind || inferJobKind(jobName) || savedKind || ANY_ENGINE_JOBS[key] || '';
|
|
1535
1561
|
if (!kind) throw new Error(`say what kind of job "${label}" is: add --like search, --like build, or --like review`);
|
|
1536
1562
|
const role = ENGINE_JOBS[kind];
|
|
1537
1563
|
const engineId = rosterToolName(engineName);
|
|
1538
1564
|
const engine = registry.engines.find((entry) => entry.id === engineId);
|
|
1539
1565
|
if (!engine) throw new Error(`unknown engine "${engineName}"`);
|
|
1540
|
-
|
|
1566
|
+
const anyEngine = jobTakesAnyEngine(key);
|
|
1567
|
+
if (!anyEngine && !engineHasRole(engine, role)) throw new Error(`${engine.id} cannot do ${builtIn ? label : `${kind} work, so it cannot take ${label}`}`);
|
|
1541
1568
|
// --backup takes engine words with an optional model and effort, the
|
|
1542
1569
|
// same as a written line: --backup "claude opus 5.5".
|
|
1543
1570
|
const backupName = String(options.backup || '').trim();
|
|
@@ -1552,7 +1579,7 @@ function assignedWorkerLines(key, engineName, options, context) {
|
|
|
1552
1579
|
}
|
|
1553
1580
|
const backup = backupRead ? registry.engines.find((entry) => entry.id === backupRead.engine) : null;
|
|
1554
1581
|
if (backupName && !backup) throw new Error(`unknown backup engine "${backupName}"`);
|
|
1555
|
-
if (backup && !engineHasRole(backup, role)) throw new Error(`${backup.id} cannot do ${builtIn ? label : `${kind} work, so it cannot back up ${label}`}`);
|
|
1582
|
+
if (backup && !anyEngine && !engineHasRole(backup, role)) throw new Error(`${backup.id} cannot do ${builtIn ? label : `${kind} work, so it cannot back up ${label}`}`);
|
|
1556
1583
|
// No --days means the worker holds until someone changes the line.
|
|
1557
1584
|
const days = options.days === undefined ? null : Number(options.days);
|
|
1558
1585
|
if (days !== null && (!Number.isSafeInteger(days) || days < 1)) throw new Error('days must be a positive whole number');
|
|
@@ -1745,11 +1772,12 @@ const ROSTER_LAYERS = Object.freeze(['session', 'project', 'machine']);
|
|
|
1745
1772
|
|
|
1746
1773
|
// A pick's workers, each with the engine it would run (model, effort, and
|
|
1747
1774
|
// time cap pinned) or the reason it is skipped: bad line, expired, bad model,
|
|
1748
|
-
// or not ready (down, or it cannot do this kind of work).
|
|
1749
|
-
|
|
1775
|
+
// or not ready (down, or it cannot do this kind of work). anyEngine drops
|
|
1776
|
+
// the kind check for a job any engine may fill (see ANY_ENGINE_JOBS).
|
|
1777
|
+
function rosterWorkerWalk(pick, role, registry, now, { anyEngine = false } = {}) {
|
|
1750
1778
|
const ready = registry.engines
|
|
1751
1779
|
.filter((engine) => engineReadyAt(engine, now))
|
|
1752
|
-
.filter((engine) => engineHasRole(engine, role));
|
|
1780
|
+
.filter((engine) => anyEngine || engineHasRole(engine, role));
|
|
1753
1781
|
return pickWorkers(pick).map((worker) => {
|
|
1754
1782
|
if (worker.error) return { worker, skip: 'bad line', engine: null, raw: null };
|
|
1755
1783
|
if (rosterPickExpired(worker, now)) return { worker, skip: 'expired', engine: null, raw: null };
|
|
@@ -1784,10 +1812,10 @@ function rosterWorkerWalk(pick, role, registry, now) {
|
|
|
1784
1812
|
// leads; null means the next pick in line decides. A custom pick only counts
|
|
1785
1813
|
// when its kind matches the role being resolved. team is every worker that
|
|
1786
1814
|
// can run, lead first.
|
|
1787
|
-
function rosterChoiceForPick(pick, role, registry, { key, label, source, scopeText = '', now } = {}) {
|
|
1815
|
+
function rosterChoiceForPick(pick, role, registry, { key, label, source, scopeText = '', now, anyEngine } = {}) {
|
|
1788
1816
|
if (!pick || !pick.engine) return null;
|
|
1789
1817
|
if (key !== undefined && pickRoleForKey(key, pick) !== role) return null;
|
|
1790
|
-
const walk = rosterWorkerWalk(pick, role, registry, now);
|
|
1818
|
+
const walk = rosterWorkerWalk(pick, role, registry, now, { anyEngine: anyEngine === undefined ? jobTakesAnyEngine(key) : anyEngine });
|
|
1791
1819
|
const leadIndex = walk.findIndex((step) => !step.skip);
|
|
1792
1820
|
if (leadIndex === -1) return null;
|
|
1793
1821
|
const lead = walk[leadIndex];
|
|
@@ -1942,6 +1970,55 @@ function resolveJobTeam(job, root = process.cwd(), options = {}) {
|
|
|
1942
1970
|
return { job: label, key, role, source: resolved.source, lead: resolved.engine || null, team, reason: resolved.reason };
|
|
1943
1971
|
}
|
|
1944
1972
|
|
|
1973
|
+
// Who writes YouTube notes when no roster layer names a notes job: agy on
|
|
1974
|
+
// its plain flash model at low effort, then haiku. Low effort is what keeps a
|
|
1975
|
+
// notes run near 8 seconds of writer time. Each model is a name from that
|
|
1976
|
+
// engine's seed catalog above. Change the default here and only here.
|
|
1977
|
+
const NOTES_DEFAULT_WORKERS = Object.freeze([
|
|
1978
|
+
Object.freeze({ engine: 'agy', model: 'gemini-3.8-flash', effort: 'low' }),
|
|
1979
|
+
Object.freeze({ engine: 'haiku', model: 'haiku', effort: null }),
|
|
1980
|
+
]);
|
|
1981
|
+
|
|
1982
|
+
// The notes job answered from saved state only: the roster layers in order
|
|
1983
|
+
// (this session, this project, all projects), each job's workers in order
|
|
1984
|
+
// with the roster's own skip rules (expired, cooling, not ready), else the
|
|
1985
|
+
// built-in default. installed comes from the registry's saved policy, so
|
|
1986
|
+
// this never probes the machine on a settled registry. A roster pick with
|
|
1987
|
+
// no usable worker falls through to the next layer, then to the default.
|
|
1988
|
+
function resolveNotesJob(root = process.cwd(), options = {}) {
|
|
1989
|
+
const key = 'notes';
|
|
1990
|
+
const registry = readEngineRegistry(root, { now: options.now });
|
|
1991
|
+
const state = options.rosterState || readRosterState(root, { now: options.now, registry });
|
|
1992
|
+
const installedOf = (id) => {
|
|
1993
|
+
const engine = registry.engines.find((entry) => entry.id === id);
|
|
1994
|
+
return Boolean(engine && engine.installed === true);
|
|
1995
|
+
};
|
|
1996
|
+
const named = [];
|
|
1997
|
+
for (const scope of ROSTER_LAYERS) {
|
|
1998
|
+
const pick = rosterPickObject(state[scope] && state[scope].picks && state[scope].picks[key]);
|
|
1999
|
+
if (!pick) continue;
|
|
2000
|
+
named.push(scope);
|
|
2001
|
+
const role = customPickRole(pick) || ENGINE_JOBS[ANY_ENGINE_JOBS[key]];
|
|
2002
|
+
const usable = rosterWorkerWalk(pick, role, registry, options.now, { anyEngine: true }).filter((step) => !step.skip);
|
|
2003
|
+
if (!usable.length) continue;
|
|
2004
|
+
const [lead, ...rest] = usable.map((step) => ({
|
|
2005
|
+
engine: step.engine.id,
|
|
2006
|
+
model: step.engine.roster_model || null,
|
|
2007
|
+
effort: step.engine.roster_effort || null,
|
|
2008
|
+
installed: step.raw.installed === true,
|
|
2009
|
+
}));
|
|
2010
|
+
return { job: key, ...lead, backup: rest, source: 'roster', from: ROSTER_SOURCES[scope] };
|
|
2011
|
+
}
|
|
2012
|
+
const [lead, ...rest] = NOTES_DEFAULT_WORKERS.map((worker) => ({ ...worker, installed: installedOf(worker.engine) }));
|
|
2013
|
+
return {
|
|
2014
|
+
job: key,
|
|
2015
|
+
...lead,
|
|
2016
|
+
backup: rest,
|
|
2017
|
+
source: 'default',
|
|
2018
|
+
...(named.length ? { skipped: `the notes roster pick (${named.map((scope) => ROSTER_SOURCES[scope]).join(', ')}) has no worker ready now` } : {}),
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
|
|
1945
2022
|
// True when a roster line (not the router) decided.
|
|
1946
2023
|
function rosterDecided(source) {
|
|
1947
2024
|
return ROSTER_LAYERS.includes(source);
|
|
@@ -2136,6 +2213,9 @@ module.exports = {
|
|
|
2136
2213
|
rosterToolLabel,
|
|
2137
2214
|
rosterWorkerWalk,
|
|
2138
2215
|
resolveJobTeam,
|
|
2216
|
+
resolveNotesJob,
|
|
2217
|
+
NOTES_DEFAULT_WORKERS,
|
|
2218
|
+
jobTakesAnyEngine,
|
|
2139
2219
|
rosterSessionKey,
|
|
2140
2220
|
clearSessionRoster,
|
|
2141
2221
|
NO_SESSION_MESSAGE,
|
package/lib/first-minute.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('./state-home');
|
|
6
7
|
const { spawnSync } = require('child_process');
|
|
7
8
|
const { isGenericScratchRoot } = require('./scratch-root');
|
|
8
9
|
|
|
@@ -57,8 +58,7 @@ function unixUser(env = process.env) {
|
|
|
57
58
|
|
|
58
59
|
function loadSavedAccount() {
|
|
59
60
|
try {
|
|
60
|
-
const
|
|
61
|
-
const file = path.join(home, '.atris', 'credentials.json');
|
|
61
|
+
const file = path.join(stateHome(), '.atris', 'credentials.json');
|
|
62
62
|
if (!fs.existsSync(file)) return null;
|
|
63
63
|
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
64
64
|
return parsed && typeof parsed === 'object' ? parsed : null;
|
package/lib/fleet.js
CHANGED
|
@@ -36,11 +36,13 @@ const { listWorktrees, defaultMainlineBase } = require('../commands/worktree');
|
|
|
36
36
|
const { isConductorStatusLine } = require('./conductor-artifacts');
|
|
37
37
|
const { matchLessons } = require('./lesson-preflight');
|
|
38
38
|
const { sleepSync } = require('./sleep-sync');
|
|
39
|
+
const { trackedSandboxPidsSync } = require('./sandbox-pids');
|
|
39
40
|
const { ownerIdentity } = require('../utils/owner-identity');
|
|
40
41
|
const { matchTaste } = require('./taste-lessons');
|
|
41
42
|
const { buildVerifiedScoutPack, appendVerifiedScoutPack } = require('./dispatch-scout');
|
|
42
43
|
const { runPrepPass, withPrepBrief, prepRecordFields } = require('./roster-prep');
|
|
43
44
|
const { mapNotesPath } = require('./map-refs');
|
|
45
|
+
const { SHARED_BRIEF_RULES } = require('./brief-rules');
|
|
44
46
|
const {
|
|
45
47
|
applySecretGrantEnvironment,
|
|
46
48
|
buildGatewaySupervisorScript,
|
|
@@ -117,7 +119,7 @@ function buildFleetPrompt(task, { worktreePath, yolo = false } = {}) {
|
|
|
117
119
|
mapNotesPath(worktreePath || process.cwd())
|
|
118
120
|
? '- Read atris/MAP.md first to locate the code, then atris/refs/MAP-NOTES.md for exact file:line refs; never guess file locations.'
|
|
119
121
|
: '- Read atris/MAP.md first to locate the code; never guess file locations.',
|
|
120
|
-
|
|
122
|
+
...SHARED_BRIEF_RULES,
|
|
121
123
|
'- Run git status first. Stage ONLY files you changed. Never revert or touch files another agent modified.',
|
|
122
124
|
'- Include or update a focused regression test; run it with npm run test:fast && node --test <focused files> before committing.',
|
|
123
125
|
`- ${commitRule}`,
|
|
@@ -284,31 +286,11 @@ function buildEngineCommand(engineName, promptFile, { yolo = false, sealed = fal
|
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
function trackedSandboxPids(stateFile, leaseFile) {
|
|
287
|
-
const pids = new Set();
|
|
288
289
|
let state = {};
|
|
289
290
|
try { state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); } catch {}
|
|
290
291
|
const pgid = Number(state.pgid);
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
for (const value of String(grouped.stdout || '').trim().split(/\s+/)) {
|
|
294
|
-
const pid = Number(value);
|
|
295
|
-
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
const leased = spawnSync('/usr/sbin/lsof', ['-t', leaseFile], { encoding: 'utf8' });
|
|
299
|
-
for (const value of String(leased.stdout || '').trim().split(/\s+/)) {
|
|
300
|
-
const pid = Number(value);
|
|
301
|
-
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
302
|
-
}
|
|
303
|
-
if (state.cwd) {
|
|
304
|
-
const rooted = spawnSync('/usr/sbin/lsof', ['-a', '-d', 'cwd', '+D', String(state.cwd), '-t'], { encoding: 'utf8' });
|
|
305
|
-
for (const value of String(rooted.stdout || '').trim().split(/\s+/)) {
|
|
306
|
-
const pid = Number(value);
|
|
307
|
-
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
pids.delete(process.pid);
|
|
311
|
-
return { pgid, pids: [...pids] };
|
|
292
|
+
const pids = trackedSandboxPidsSync({ pgid, leaseFile, cwd: state.cwd ? String(state.cwd) : '' });
|
|
293
|
+
return { pgid, pids };
|
|
312
294
|
}
|
|
313
295
|
|
|
314
296
|
function terminateTrackedSandbox(stateFile, leaseFile) {
|
|
@@ -480,6 +462,7 @@ function runInReapedProcessGroup(executable, args, options, controlDir, statusFi
|
|
|
480
462
|
"'use strict';",
|
|
481
463
|
"const fs = require('node:fs');",
|
|
482
464
|
"const { spawn, spawnSync } = require('node:child_process');",
|
|
465
|
+
`const sandboxPids = require(${JSON.stringify(require.resolve('./sandbox-pids'))});`,
|
|
483
466
|
"const [stateFile, leaseFile, statusFile, executable, ...args] = process.argv.slice(2);",
|
|
484
467
|
"const leaseFd = fs.openSync(leaseFile, 'w', 0o600);",
|
|
485
468
|
"const statusFd = fs.openSync(statusFile, 'w', 0o600);",
|
|
@@ -489,18 +472,7 @@ function runInReapedProcessGroup(executable, args, options, controlDir, statusFi
|
|
|
489
472
|
"fs.writeFileSync(stateFile, JSON.stringify({ pgid: child.pid, cwd: process.cwd() }) + '\\n', { mode: 0o600 });",
|
|
490
473
|
"let stopping = false;",
|
|
491
474
|
"const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));",
|
|
492
|
-
"
|
|
493
|
-
" const pids = new Set();",
|
|
494
|
-
" for (const [bin, argv] of [['/usr/bin/pgrep', ['-g', String(child.pid)]], ['/usr/sbin/lsof', ['-t', leaseFile]], ['/usr/sbin/lsof', ['-a', '-d', 'cwd', '+D', process.cwd(), '-t']]]) {",
|
|
495
|
-
" const found = spawnSync(bin, argv, { encoding: 'utf8' });",
|
|
496
|
-
" for (const value of String(found.stdout || '').trim().split(/\\s+/)) {",
|
|
497
|
-
" const pid = Number(value);",
|
|
498
|
-
" if (Number.isInteger(pid) && pid > 0) pids.add(pid);",
|
|
499
|
-
" }",
|
|
500
|
-
" }",
|
|
501
|
-
" pids.delete(process.pid);",
|
|
502
|
-
" return [...pids];",
|
|
503
|
-
"}",
|
|
475
|
+
"const trackedPids = () => sandboxPids.trackedSandboxPids({ pgid: child.pid, leaseFile, cwd: process.cwd() });",
|
|
504
476
|
"async function stop(code) {",
|
|
505
477
|
" if (stopping) return;",
|
|
506
478
|
" stopping = true;",
|
|
@@ -509,7 +481,7 @@ function runInReapedProcessGroup(executable, args, options, controlDir, statusFi
|
|
|
509
481
|
" }",
|
|
510
482
|
" for (const [signal, delay] of [['SIGTERM', 100], ['SIGKILL', 100], ['SIGKILL', 100]]) {",
|
|
511
483
|
" try { process.kill(-child.pid, signal); } catch {}",
|
|
512
|
-
" for (const pid of trackedPids()) { try { process.kill(pid, signal); } catch {} }",
|
|
484
|
+
" for (const pid of await trackedPids()) { try { process.kill(pid, signal); } catch {} }",
|
|
513
485
|
" await wait(delay);",
|
|
514
486
|
" }",
|
|
515
487
|
" let exitCode = Number.isInteger(code) ? code : 128;",
|
package/lib/git-spawn.js
CHANGED
|
@@ -4,8 +4,11 @@ const { spawnSync } = require('child_process');
|
|
|
4
4
|
|
|
5
5
|
// One synchronous git runner for commands that shell out to git.
|
|
6
6
|
// check: true throws on a non-zero exit; check: false hands back the raw result.
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// input, when given, is written to git's stdin (for --stdin batch reads).
|
|
8
|
+
function runGit(args, { cwd = process.cwd(), check = true, maxBuffer, input } = {}) {
|
|
9
|
+
const result = spawnSync('git', args, {
|
|
10
|
+
cwd, encoding: 'utf8', ...(maxBuffer ? { maxBuffer } : {}), ...(input !== undefined ? { input } : {}),
|
|
11
|
+
});
|
|
9
12
|
if (check && result.status !== 0) {
|
|
10
13
|
throw new Error(`git ${args.join(' ')} failed: ${(result.stderr || result.stdout || '').trim()}`);
|
|
11
14
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Speed caches that must never be committed. They live in .atris/cache, a
|
|
4
|
+
// folder that carries its own .gitignore of `*`, so a project with no ignore
|
|
5
|
+
// rule for .atris still never stages them with `git add -A`. Caches are only
|
|
6
|
+
// kept in folders that already have .atris; elsewhere nothing is written.
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const CACHE_DIR = path.join('.atris', 'cache');
|
|
12
|
+
|
|
13
|
+
function cacheEnabled(root) {
|
|
14
|
+
try {
|
|
15
|
+
return fs.statSync(path.join(root, '.atris')).isDirectory();
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function cachePath(root, name) {
|
|
22
|
+
return path.join(root, CACHE_DIR, name);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The parsed cache file, or null when it is missing or unreadable.
|
|
26
|
+
function readCacheJson(root, name) {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(fs.readFileSync(cachePath(root, name), 'utf8'));
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Returns false when the write failed; a cache is only a speedup.
|
|
35
|
+
function writeCacheJson(root, name, value) {
|
|
36
|
+
const dir = path.join(root, CACHE_DIR);
|
|
37
|
+
try {
|
|
38
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
39
|
+
const ignore = path.join(dir, '.gitignore');
|
|
40
|
+
if (!fs.existsSync(ignore)) fs.writeFileSync(ignore, '*\n');
|
|
41
|
+
const file = cachePath(root, name);
|
|
42
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
43
|
+
fs.writeFileSync(tmp, `${JSON.stringify(value)}\n`);
|
|
44
|
+
fs.renameSync(tmp, file);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { CACHE_DIR, cacheEnabled, cachePath, readCacheJson, writeCacheJson };
|
package/lib/known-commands.js
CHANGED
|
@@ -4,7 +4,7 @@ const knownCommands = ['init', 'guide', 'doctor', 'doc-health', 'install', 'work
|
|
|
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', 'buy', 'transactions', 'design', 'mcp', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
7
|
+
'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'api-key', 'topup', 'buy', 'transactions', 'hire', 'hires', 'mail', 'design', 'mcp', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
8
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', 'host', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join', 'rsi'];
|
|
9
9
|
|
|
10
10
|
// Damerau-Levenshtein edit distance between two short strings. Counts an
|
package/lib/manifest.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const
|
|
4
|
+
const { stateHome } = require('./state-home');
|
|
5
5
|
const { computeContentHash } = require('./journal');
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -9,7 +9,7 @@ const { computeContentHash } = require('./journal');
|
|
|
9
9
|
* Stored at ~/.atris/businesses/{slug}/manifest.json
|
|
10
10
|
*/
|
|
11
11
|
function getManifestPath(slug) {
|
|
12
|
-
return path.join(
|
|
12
|
+
return path.join(stateHome(), '.atris', 'businesses', slug, 'manifest.json');
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
package/lib/map-refs.js
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
// A name elsewhere in the sentence never binds: map sentences often name a helper
|
|
7
7
|
// the ref does not point at, and binding it would move the ref to the wrong place.
|
|
8
8
|
|
|
9
|
+
const crypto = require('crypto');
|
|
9
10
|
const fs = require('fs');
|
|
10
11
|
const path = require('path');
|
|
12
|
+
const ignoredCache = require('./ignored-cache');
|
|
11
13
|
|
|
12
14
|
const WINDOW = 3;
|
|
13
15
|
const IDENT = /^[A-Za-z_$][\w$]*$/;
|
|
@@ -123,6 +125,14 @@ function classify(ref, file) {
|
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
function checkMapRefs(root, mapText) {
|
|
128
|
+
const parsed = parseRefs(mapText);
|
|
129
|
+
const results = classifyRefs(root, parsed);
|
|
130
|
+
return summarize(parsed.map((ref, i) => ({ ...ref, ...results[i] })));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// What the code says about each parsed ref: its status, plus the line it moved
|
|
134
|
+
// to or why it is missing. Each file a ref points at is read once.
|
|
135
|
+
function classifyRefs(root, parsed) {
|
|
126
136
|
const cache = new Map();
|
|
127
137
|
const linesOf = file => {
|
|
128
138
|
if (!cache.has(file)) {
|
|
@@ -144,11 +154,10 @@ function checkMapRefs(root, mapText) {
|
|
|
144
154
|
}
|
|
145
155
|
return cache.get(file);
|
|
146
156
|
};
|
|
147
|
-
|
|
157
|
+
return parsed.map(ref => {
|
|
148
158
|
const file = linesOf(ref.path);
|
|
149
|
-
return
|
|
159
|
+
return file ? classify(ref, file) : { status: 'missing-file' };
|
|
150
160
|
});
|
|
151
|
-
return summarize(refs);
|
|
152
161
|
}
|
|
153
162
|
|
|
154
163
|
function summarize(refs) {
|
|
@@ -197,13 +206,70 @@ function existingMapDocs(root) {
|
|
|
197
206
|
});
|
|
198
207
|
}
|
|
199
208
|
|
|
209
|
+
// Boot scores the docs every session, and checking every ref means reading
|
|
210
|
+
// every file the map points at (megabytes on this repo). The result for each
|
|
211
|
+
// map file is saved in the ignored .atris/cache and reused only while the map
|
|
212
|
+
// text and the stat stamp of every file it points at are unchanged.
|
|
213
|
+
const REFS_CACHE_NAME = 'map-refs.json';
|
|
214
|
+
|
|
215
|
+
// Size, modified time, change time, and inode of a file a ref points at; null
|
|
216
|
+
// when it is not there. Change time moves on every write and cannot be set
|
|
217
|
+
// back by hand, so an edit that keeps size and modified time still misses.
|
|
218
|
+
function fileStamp(full) {
|
|
219
|
+
try {
|
|
220
|
+
const stat = fs.statSync(full);
|
|
221
|
+
return `${stat.isFile() ? 'f' : 'o'}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}:${stat.ino}`;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return null;
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function refsCacheKey(root, text, parsed) {
|
|
229
|
+
const files = [...new Set(parsed.map(ref => ref.path))];
|
|
230
|
+
const stamps = files.map(file => [file, fileStamp(path.resolve(root, file))]);
|
|
231
|
+
return crypto.createHash('sha1').update(JSON.stringify([text, stamps])).digest('hex');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function loadRefsCache(root) {
|
|
235
|
+
const cache = { root, docs: {}, dirty: false, writable: ignoredCache.cacheEnabled(root) };
|
|
236
|
+
if (!cache.writable) return cache;
|
|
237
|
+
const parsed = ignoredCache.readCacheJson(root, REFS_CACHE_NAME);
|
|
238
|
+
if (parsed && parsed.version === 1 && parsed.docs && typeof parsed.docs === 'object') cache.docs = parsed.docs;
|
|
239
|
+
return cache;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function saveRefsCache(cache) {
|
|
243
|
+
if (!cache.writable || !cache.dirty) return;
|
|
244
|
+
ignoredCache.writeCacheJson(cache.root, REFS_CACHE_NAME, { version: 1, docs: cache.docs });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function cachedMapRefs(root, doc, text, cache) {
|
|
248
|
+
const parsed = parseRefs(text);
|
|
249
|
+
const key = cache.writable ? refsCacheKey(root, text, parsed) : '';
|
|
250
|
+
const hit = key && cache.docs[doc];
|
|
251
|
+
let results;
|
|
252
|
+
if (hit && hit.key === key && Array.isArray(hit.results) && hit.results.length === parsed.length) {
|
|
253
|
+
results = hit.results;
|
|
254
|
+
} else {
|
|
255
|
+
results = classifyRefs(root, parsed);
|
|
256
|
+
if (key) {
|
|
257
|
+
cache.docs[doc] = { key, results };
|
|
258
|
+
cache.dirty = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return parsed.map((ref, i) => ({ ...ref, ...results[i] }));
|
|
262
|
+
}
|
|
263
|
+
|
|
200
264
|
// mapText is the already-read atris/MAP.md, so boot does not read it twice.
|
|
201
265
|
function checkMapDocs(root, mapText) {
|
|
266
|
+
const cache = loadRefsCache(root);
|
|
202
267
|
const refs = existingMapDocs(root).flatMap(doc => {
|
|
203
268
|
const text = doc === MAP_REF_DOCS[0] && typeof mapText === 'string'
|
|
204
269
|
? mapText : fs.readFileSync(path.resolve(root, doc), 'utf8');
|
|
205
|
-
return
|
|
270
|
+
return cachedMapRefs(root, doc, text, cache).map(ref => ({ doc, ...ref }));
|
|
206
271
|
});
|
|
272
|
+
saveRefsCache(cache);
|
|
207
273
|
return summarize(refs);
|
|
208
274
|
}
|
|
209
275
|
|