atris 3.58.7 → 3.59.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/AGENTS.md +3 -2
- package/GETTING_STARTED.md +2 -2
- package/PERSONA.md +11 -11
- package/README.md +1 -1
- package/atris/GEMINI.md +7 -6
- package/atris/PERSONA.md +4 -4
- package/atris/features/README.md +77 -6
- package/atris/skills/README.md +44 -15
- package/atris/skills/aeo/SKILL.md +10 -10
- package/atris/skills/atris/SKILL.md +1 -1
- package/atris/skills/engines/SKILL.md +69 -50
- package/atris/skills/meta/SKILL.md +4 -4
- package/bin/atris.js +8 -6
- package/commands/aeo.js +9 -12
- package/commands/analytics.js +8 -11
- package/commands/autopilot.js +79 -8
- package/commands/avail.js +1 -1
- package/commands/brain.js +3 -1
- package/commands/business.js +13 -8
- package/commands/clean.js +17 -0
- package/commands/close.js +2 -1
- package/commands/computer.js +15 -10
- package/commands/context-sync.js +3 -3
- package/commands/doc-health.js +55 -2
- package/commands/engine.js +384 -4
- package/commands/experiments.js +13 -4
- package/commands/host.js +53 -0
- package/commands/improve.js +1 -1
- package/commands/integrations.js +2 -5
- package/commands/learn.js +1 -1
- package/commands/member.js +231 -53
- package/commands/mission.js +129 -19
- package/commands/one-lap.js +58 -3
- package/commands/pack.js +2 -2
- package/commands/probe.js +11 -5
- package/commands/pull.js +1 -1
- package/commands/push.js +1 -1
- package/commands/rainmaker.js +3 -16
- package/commands/run.js +4 -1
- package/commands/scout.js +1 -1
- package/commands/search.js +5 -3
- package/commands/spaceship.js +1 -1
- package/commands/stream.js +27 -2
- package/commands/study.js +15 -9
- package/commands/sync.js +1 -1
- package/commands/task.js +13 -2
- package/commands/unknowns.js +5 -0
- package/commands/verify.js +12 -4
- package/commands/visualize.js +20 -3
- package/commands/workflow.js +23 -7
- package/lib/codex-flight.js +28 -5
- package/lib/dispatch-scout.js +31 -22
- package/lib/engine-registry.js +1482 -13
- package/lib/feedback-triage.js +2 -0
- package/lib/fleet.js +111 -12
- package/lib/host.js +336 -0
- package/lib/improve-vitals-html.js +3 -2
- package/lib/known-commands.js +1 -1
- package/lib/lesson-contradiction.js +2 -2
- package/lib/map-refs.js +226 -0
- package/lib/member-engine.js +253 -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/roster-markdown.js +206 -0
- package/lib/roster-models.js +179 -0
- package/lib/runner-command.js +95 -15
- package/lib/scorecard.js +4 -4
- package/lib/self-drive.js +9 -1
- package/lib/sleep-sync.js +11 -0
- package/lib/state-detection.js +28 -9
- package/lib/task-db.js +3 -2
- package/lib/todo-fallback.js +1 -1
- package/lib/wish-audit.js +10 -6
- package/lib/wish-delegate.js +29 -5
- package/mcp/atris-mcp/index.mjs +77 -24
- package/package.json +2 -5
- package/scripts/det/det.js +2 -2
- package/scripts/det/pr-description.js +2 -2
- package/scripts/det/test.js +2 -2
- package/templates/members/host/MEMBER.md +23 -0
- package/templates/members/host/MISSION.md +11 -0
- package/templates/members/host/SOUL.md +7 -0
- package/templates/members/host/context/why.md +3 -0
- package/templates/members/host/goals.json +5 -0
- package/templates/members/host/goals.md +3 -0
- package/templates/members/host/skills/host/SKILL.md +13 -0
- package/utils/api.js +1 -1
- package/utils/auth.js +17 -7
- package/utils/backend-root.js +31 -0
- package/utils/config.js +5 -4
- package/utils/owner-identity.js +61 -0
- package/atris/policies/LESSONS.md +0 -13
- package/atris/skills/autopilot/atris-autopilot.md +0 -71
package/commands/mission.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
printCliJson,
|
|
14
14
|
} = require('../lib/cli-json');
|
|
15
15
|
const escapeRegExp = require('../lib/escape-regexp');
|
|
16
|
+
const { sleepSync } = require('../lib/sleep-sync');
|
|
16
17
|
const {
|
|
17
18
|
appendBriefRecord,
|
|
18
19
|
stampBriefOutcome,
|
|
@@ -20,6 +21,7 @@ const {
|
|
|
20
21
|
} = require('../lib/brief-ledger');
|
|
21
22
|
const {
|
|
22
23
|
resolveClaudeRunnerModel,
|
|
24
|
+
resolvePinnedRunnerModel,
|
|
23
25
|
resolveClaudeRunnerBin,
|
|
24
26
|
resolveClaudeRunnerCommandTemplate,
|
|
25
27
|
buildRunnerCommand,
|
|
@@ -384,13 +386,59 @@ function resolveMissionRunnerSelection(value, options = {}) {
|
|
|
384
386
|
exitMissionError(`Unknown ${noun} "${raw}". ${knownMissionRunnerText()}.`, 2, asJson);
|
|
385
387
|
}
|
|
386
388
|
|
|
387
|
-
|
|
389
|
+
// The roster line's effort and time cap for this tick, when it sets them.
|
|
390
|
+
function rosterTickPins(engine) {
|
|
391
|
+
return {
|
|
392
|
+
...(engine && engine.roster_effort ? { roster_effort: engine.roster_effort } : {}),
|
|
393
|
+
...(engine && engine.roster_max_seconds ? { roster_max_seconds: engine.roster_max_seconds } : {}),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// A tick's time limit: the time left on the mission, capped by the roster
|
|
398
|
+
// line's "max 20 min" when it sets one, else by the default tick cap.
|
|
399
|
+
function missionTickTimeoutMs(mission, remainingMs) {
|
|
400
|
+
const cap = Number(mission && mission.roster_max_seconds) > 0
|
|
401
|
+
? Number(mission.roster_max_seconds) * 1000
|
|
402
|
+
: MISSION_RUN_DEFAULTS.claudeTimeoutMs;
|
|
403
|
+
return clampTimeoutMs(remainingMs, cap);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function resolveMissionTickRunner(mission, root = process.cwd(), options = {}) {
|
|
388
407
|
if (String(mission && mission.runner || '').trim().toLowerCase() !== MISSION_AUTO_RUNNER) {
|
|
389
408
|
return { mission, engine_id: null, requested_engine: null, engine_fallback_reason: null };
|
|
390
409
|
}
|
|
391
|
-
|
|
410
|
+
// A mission owned by a team member runs on that member's roster pick. An
|
|
411
|
+
// explicit preferred engine (or a run's --engine) still wins, and with no
|
|
412
|
+
// roster line behind the member, routing stays exactly as it was.
|
|
413
|
+
if (!String(mission.preferred_engine || '').trim() && mission.owner) {
|
|
414
|
+
const { memberRosterEngine } = require('../lib/member-engine');
|
|
415
|
+
const member = memberRosterEngine(mission.owner, root, options);
|
|
416
|
+
if (member) {
|
|
417
|
+
if (process.env.ATRIS_ROUTER_EXPLAIN !== '0') console.error(member.reason);
|
|
418
|
+
return {
|
|
419
|
+
mission: {
|
|
420
|
+
...mission,
|
|
421
|
+
runner: member.engine.id,
|
|
422
|
+
runner_kind: 'engine',
|
|
423
|
+
...(!mission.model && member.model ? { model: member.model } : {}),
|
|
424
|
+
...rosterTickPins(member.engine),
|
|
425
|
+
},
|
|
426
|
+
engine_id: member.engine.id,
|
|
427
|
+
requested_engine: null,
|
|
428
|
+
engine_fallback_reason: null,
|
|
429
|
+
member_engine_reason: member.reason,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const resolved = resolveEngineForRoleWithPreference('executor', root, mission.preferred_engine, options);
|
|
392
434
|
return {
|
|
393
|
-
mission: resolved.engine ? {
|
|
435
|
+
mission: resolved.engine ? {
|
|
436
|
+
...mission,
|
|
437
|
+
runner: resolved.engine.id,
|
|
438
|
+
runner_kind: 'engine',
|
|
439
|
+
...(!mission.model && resolved.engine.roster_model ? { model: resolved.engine.roster_model } : {}),
|
|
440
|
+
...rosterTickPins(resolved.engine),
|
|
441
|
+
} : mission,
|
|
394
442
|
engine_id: resolved.engine ? resolved.engine.id : null,
|
|
395
443
|
requested_engine: resolved.requested_engine,
|
|
396
444
|
engine_fallback_reason: resolved.engine_fallback_reason,
|
|
@@ -443,6 +491,20 @@ function applyMissionRunnerProfile(runner) {
|
|
|
443
491
|
};
|
|
444
492
|
}
|
|
445
493
|
|
|
494
|
+
// Claude-family engines share the claude CLI, so they keep the resolved
|
|
495
|
+
// default model pin. Every other engine's CLI rejects a claude model name, so
|
|
496
|
+
// it only ever takes an explicit pin (the mission's model or the roster
|
|
497
|
+
// line's); '' lets the engine ride its own default.
|
|
498
|
+
const CLAUDE_FAMILY_RUNNER_ENGINES = new Set(['claude', 'fable', 'haiku']);
|
|
499
|
+
|
|
500
|
+
function resolveMissionTickRunnerModel(mission) {
|
|
501
|
+
const engine = canonicalEngineName(mission && mission.runner);
|
|
502
|
+
if (!engine || CLAUDE_FAMILY_RUNNER_ENGINES.has(engine)) {
|
|
503
|
+
return resolveClaudeRunnerModel(mission);
|
|
504
|
+
}
|
|
505
|
+
return resolvePinnedRunnerModel(mission);
|
|
506
|
+
}
|
|
507
|
+
|
|
446
508
|
function exitMissionError(message, code = 1, asJson = false) {
|
|
447
509
|
if (asJson) {
|
|
448
510
|
printCliJson(
|
|
@@ -3568,8 +3630,8 @@ function readTasteReviewHistory(root, limit = 4) {
|
|
|
3568
3630
|
|
|
3569
3631
|
function missionTasteMemorySignals(text) {
|
|
3570
3632
|
const definitions = [
|
|
3571
|
-
['plain_language', '
|
|
3572
|
-
['proof_gate', '
|
|
3633
|
+
['plain_language', 'the operator asks for plain English', /\b(plain|jargon|feynman|understand|simple|clarify|clarity)\b/i],
|
|
3634
|
+
['proof_gate', 'the operator wants proof before accept', /\b(proof|receipt|verify|verified|checked|tested|accept|approval)\b/i],
|
|
3573
3635
|
['runway_revenue', 'runway pushes user or revenue work', /\b(runway|revenue|cash|user|users|customer|demo|adoption|retention)\b/i],
|
|
3574
3636
|
['technical_ambition', 'technical advancement still matters', /\b(technical|homerun|research|runtime|architecture|model|benchmark|agent)\b/i],
|
|
3575
3637
|
['trust_boundary', 'trust and approval boundaries matter', /\b(security|safe|safety|trust|privacy|approval|permission|gmail|email|connector)\b/i],
|
|
@@ -3668,9 +3730,9 @@ function missionPlainTaskPreview(title) {
|
|
|
3668
3730
|
function missionTasteMemoryReason(tasteMemory, signals) {
|
|
3669
3731
|
const candidateIds = new Set((signals || []).map((signal) => signal.id));
|
|
3670
3732
|
const memoryIds = new Set((tasteMemory?.signals || []).map((signal) => signal.id));
|
|
3671
|
-
if (memoryIds.has('runway_revenue') && candidateIds.has('users_revenue')) return '
|
|
3672
|
-
if (memoryIds.has('plain_language') && candidateIds.has('usability')) return '
|
|
3673
|
-
if (memoryIds.has('proof_gate') && (candidateIds.has('trust') || candidateIds.has('technical'))) return '
|
|
3733
|
+
if (memoryIds.has('runway_revenue') && candidateIds.has('users_revenue')) return 'the operator has been steering toward user/revenue proof because runway is tight.';
|
|
3734
|
+
if (memoryIds.has('plain_language') && candidateIds.has('usability')) return 'the operator keeps asking for plain-English, understandable work.';
|
|
3735
|
+
if (memoryIds.has('proof_gate') && (candidateIds.has('trust') || candidateIds.has('technical'))) return 'the operator wants proof and receipts before serious work is accepted.';
|
|
3674
3736
|
if (memoryIds.has('working_memory')) return 'Recent logs are being used as working memory for what matters now.';
|
|
3675
3737
|
if (memoryIds.has('technical_ambition') && candidateIds.has('technical')) return 'The member memory still values technical advancement when it has proof.';
|
|
3676
3738
|
return '';
|
|
@@ -4160,14 +4222,61 @@ function startMission(args, options = {}) {
|
|
|
4160
4222
|
// checkout's dirt never reaches the mission baseline.
|
|
4161
4223
|
if (hasFlag(args, '--worktree')) {
|
|
4162
4224
|
let created;
|
|
4225
|
+
const sourceTop = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd: process.cwd(), encoding: 'utf8' });
|
|
4226
|
+
const sourceCheckout = sourceTop.status === 0 ? String(sourceTop.stdout).trim() : process.cwd();
|
|
4227
|
+
let nodeModulesSource = null;
|
|
4163
4228
|
try {
|
|
4164
4229
|
const { createAgentWorktree } = require('./worktree');
|
|
4165
4230
|
const base = readFlag(args, '--base', '') || inheritedWorktreeBase(process.cwd());
|
|
4166
4231
|
created = createAgentWorktree({ member: mission.owner, task: mission.objective, ...(base ? { base } : {}) });
|
|
4232
|
+
const packageFile = path.join(sourceCheckout, 'package.json');
|
|
4233
|
+
const sourceModules = path.join(sourceCheckout, 'node_modules');
|
|
4234
|
+
const targetModules = path.join(created.path, 'node_modules');
|
|
4235
|
+
try {
|
|
4236
|
+
let targetExists = false;
|
|
4237
|
+
try {
|
|
4238
|
+
fs.lstatSync(targetModules);
|
|
4239
|
+
targetExists = true;
|
|
4240
|
+
} catch (error) {
|
|
4241
|
+
if (error.code !== 'ENOENT') throw error;
|
|
4242
|
+
}
|
|
4243
|
+
if (fs.existsSync(packageFile) && fs.existsSync(sourceModules) && !targetExists) {
|
|
4244
|
+
const manifest = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
|
|
4245
|
+
const hasDependencies = ['dependencies', 'devDependencies', 'optionalDependencies']
|
|
4246
|
+
.some((field) => manifest[field] && Object.keys(manifest[field]).length > 0);
|
|
4247
|
+
if (hasDependencies) {
|
|
4248
|
+
const checkIgnored = () => {
|
|
4249
|
+
const result = spawnSync('git', ['check-ignore', '-q', 'node_modules'], { cwd: created.path, encoding: 'utf8' });
|
|
4250
|
+
if (result.error || (result.status !== 0 && result.status !== 1)) {
|
|
4251
|
+
throw new Error(result.error?.message || String(result.stderr || 'git check-ignore failed').trim());
|
|
4252
|
+
}
|
|
4253
|
+
return result.status === 0;
|
|
4254
|
+
};
|
|
4255
|
+
if (!checkIgnored()) {
|
|
4256
|
+
const commonDir = spawnSync('git', ['rev-parse', '--git-common-dir'], { cwd: created.path, encoding: 'utf8' });
|
|
4257
|
+
if (commonDir.error || commonDir.status !== 0) {
|
|
4258
|
+
throw new Error(commonDir.error?.message || String(commonDir.stderr || 'could not find shared git directory').trim());
|
|
4259
|
+
}
|
|
4260
|
+
const excludeFile = path.resolve(created.path, String(commonDir.stdout).trim(), 'info', 'exclude');
|
|
4261
|
+
fs.mkdirSync(path.dirname(excludeFile), { recursive: true });
|
|
4262
|
+
const exclude = fs.existsSync(excludeFile) ? fs.readFileSync(excludeFile, 'utf8') : '';
|
|
4263
|
+
if (!exclude.split(/\r?\n/).includes('/node_modules')) {
|
|
4264
|
+
fs.appendFileSync(excludeFile, `${exclude && !exclude.endsWith('\n') ? '\n' : ''}/node_modules\n`);
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
if (!checkIgnored()) throw new Error('git still tracks node_modules after updating the shared exclude file');
|
|
4268
|
+
fs.symlinkSync(sourceModules, targetModules, 'dir');
|
|
4269
|
+
nodeModulesSource = sourceModules;
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
} catch (error) {
|
|
4273
|
+
console.warn(`warning: could not link node_modules: ${String(error.message).split(/\r?\n/)[0]}`);
|
|
4274
|
+
}
|
|
4167
4275
|
} catch (e) {
|
|
4168
4276
|
exitMissionError(`[mission start] worktree creation failed: ${e.message}`, 2, asJson);
|
|
4169
4277
|
}
|
|
4170
4278
|
mission.worktree = { path: created.path, branch: created.branch, base: created.base };
|
|
4279
|
+
if (nodeModulesSource) mission.node_modules_source = nodeModulesSource;
|
|
4171
4280
|
process.chdir(created.path);
|
|
4172
4281
|
}
|
|
4173
4282
|
if (mission.xp_task_enabled) {
|
|
@@ -4225,6 +4334,8 @@ function startMission(args, options = {}) {
|
|
|
4225
4334
|
dirty_count: worktreeBaseline.dirty_count,
|
|
4226
4335
|
dirty_hash: worktreeBaseline.dirty_hash,
|
|
4227
4336
|
} : null,
|
|
4337
|
+
node_modules_linked: Boolean(saved.node_modules_source),
|
|
4338
|
+
node_modules_source: saved.node_modules_source || null,
|
|
4228
4339
|
...(saved.remap_reason ? { remap_reason: saved.remap_reason } : {}),
|
|
4229
4340
|
};
|
|
4230
4341
|
if (!options.silent) {
|
|
@@ -4236,6 +4347,7 @@ function startMission(args, options = {}) {
|
|
|
4236
4347
|
`State: ${saved.status}`,
|
|
4237
4348
|
...(saved.remap_reason ? [`Remap: ${saved.remap_reason}`] : []),
|
|
4238
4349
|
...(saved.worktree ? [`Worktree: ${saved.worktree.path}`, `Branch: ${saved.worktree.branch}`] : []),
|
|
4350
|
+
...(saved.node_modules_source ? [`linked node_modules from ${saved.node_modules_source}`] : []),
|
|
4239
4351
|
...warnings.map((warning) => `Warning: ${warning.message}`),
|
|
4240
4352
|
...(saved.xp_task ? [`AgentXP task: ${saved.xp_task.ref}`] : []),
|
|
4241
4353
|
...(saved.worktree ? [`Next: cd ${saved.worktree.path} && ${nextTickCommand}`] : [`Next: ${nextTickCommand}`]),
|
|
@@ -8049,12 +8161,6 @@ function isWithinActiveHours(activeHours, now = new Date()) {
|
|
|
8049
8161
|
return cur >= start || cur < end;
|
|
8050
8162
|
}
|
|
8051
8163
|
|
|
8052
|
-
function sleepSync(ms) {
|
|
8053
|
-
const waitMs = Math.max(0, Number(ms) || 0);
|
|
8054
|
-
if (!waitMs) return;
|
|
8055
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, waitMs);
|
|
8056
|
-
}
|
|
8057
|
-
|
|
8058
8164
|
function missionLockOwnerIsAlive(pid) {
|
|
8059
8165
|
const ownerPid = Number(pid);
|
|
8060
8166
|
if (!Number.isInteger(ownerPid) || ownerPid <= 0) return false;
|
|
@@ -8700,7 +8806,7 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
8700
8806
|
let briefId = null;
|
|
8701
8807
|
try {
|
|
8702
8808
|
promptFile = writeRunnerPromptFile(cwd, mission.id, prompt);
|
|
8703
|
-
cmd = buildRunnerCommand({ promptFile, model });
|
|
8809
|
+
cmd = buildRunnerCommand({ promptFile, model, ...(mission.roster_effort ? { effort: mission.roster_effort } : {}) });
|
|
8704
8810
|
const engine = canonicalEngineName(mission.runner);
|
|
8705
8811
|
if (engine) {
|
|
8706
8812
|
const record = appendBriefRecord(cwd, {
|
|
@@ -8877,6 +8983,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
8877
8983
|
'--include-partial-messages',
|
|
8878
8984
|
];
|
|
8879
8985
|
if (model) args.push('--model', model);
|
|
8986
|
+
if (mission.roster_effort) args.push('--effort', mission.roster_effort);
|
|
8880
8987
|
if (sessionMode === 'set') args.push('--session-id', sessionId);
|
|
8881
8988
|
else if (sessionMode === 'resume') args.push('--resume', sessionId);
|
|
8882
8989
|
|
|
@@ -9043,7 +9150,7 @@ async function runEngineVerifier(mission, options = {}) {
|
|
|
9043
9150
|
signal: options.signal,
|
|
9044
9151
|
timeoutMs: ENGINE_VERIFY_TIMEOUT_MS,
|
|
9045
9152
|
prompt: buildEngineVerifyPrompt(mission, options.tickIndex || 1),
|
|
9046
|
-
model:
|
|
9153
|
+
model: resolveMissionTickRunnerModel(verifyMission),
|
|
9047
9154
|
});
|
|
9048
9155
|
} catch (error) {
|
|
9049
9156
|
engineResult = { ok: false, error: error.message };
|
|
@@ -9623,12 +9730,12 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
9623
9730
|
const runClaudeSession = () => spawnClaudeTick(tickRuntimeMission, {
|
|
9624
9731
|
sessionMode, sessionId: useId, cwd, signal: controller.signal,
|
|
9625
9732
|
missionLock: lock,
|
|
9626
|
-
timeoutMs:
|
|
9733
|
+
timeoutMs: missionTickTimeoutMs(
|
|
9734
|
+
tickRuntimeMission,
|
|
9627
9735
|
(maxWallSeconds - ((Date.now() - startedAt) / 1000)) * 1000,
|
|
9628
|
-
MISSION_RUN_DEFAULTS.claudeTimeoutMs,
|
|
9629
9736
|
),
|
|
9630
9737
|
prompt,
|
|
9631
|
-
model:
|
|
9738
|
+
model: resolveMissionTickRunnerModel(tickRuntimeMission),
|
|
9632
9739
|
});
|
|
9633
9740
|
try {
|
|
9634
9741
|
claudeResult = await runClaudeSession();
|
|
@@ -11629,6 +11736,7 @@ module.exports = {
|
|
|
11629
11736
|
missionProtectedLaneHold,
|
|
11630
11737
|
missionCommand,
|
|
11631
11738
|
startMission,
|
|
11739
|
+
resolveMission,
|
|
11632
11740
|
spawnMissionDriver,
|
|
11633
11741
|
missionRunnerNeedsLiveSession,
|
|
11634
11742
|
completeMission,
|
|
@@ -11680,6 +11788,8 @@ module.exports = {
|
|
|
11680
11788
|
missionHumanStatusText,
|
|
11681
11789
|
resolveMissionRunnerSelection,
|
|
11682
11790
|
resolveMissionTickRunner,
|
|
11791
|
+
resolveMissionTickRunnerModel,
|
|
11792
|
+
missionTickTimeoutMs,
|
|
11683
11793
|
engineFailureHealthStatus,
|
|
11684
11794
|
recordMissionEngineTickOutcome,
|
|
11685
11795
|
tickMadeProgress,
|
package/commands/one-lap.js
CHANGED
|
@@ -9,7 +9,10 @@ const { readWishes } = require('../lib/wish-store');
|
|
|
9
9
|
const {
|
|
10
10
|
engineRegistryView,
|
|
11
11
|
resolveEngineForRole,
|
|
12
|
+
resolveEngineForRoleRanked,
|
|
12
13
|
resolveRegisteredEngine,
|
|
14
|
+
rosterPinFor,
|
|
15
|
+
rosterDecided,
|
|
13
16
|
} = require('../lib/engine-registry');
|
|
14
17
|
const { parseVerifyCommand } = require('../lib/auto-accept-certified');
|
|
15
18
|
const fleet = require('../lib/fleet');
|
|
@@ -155,7 +158,8 @@ function readyExecutor(root, preferred = '') {
|
|
|
155
158
|
if (!selected.roles.includes('executor')) throw new Error(`engine ${selected.id} is not an executor`);
|
|
156
159
|
if (!selected.health || selected.health.status !== 'ready') throw new Error(`engine ${selected.id} is not ready`);
|
|
157
160
|
if (!fleet.FLEET_CAPABLE.includes(selected.id)) throw new Error(`engine ${selected.id} cannot build headlessly`);
|
|
158
|
-
|
|
161
|
+
const pin = rosterPinFor('executor', selected.id, root);
|
|
162
|
+
return Object.keys(pin).length ? { ...selected, ...pin } : selected;
|
|
159
163
|
}
|
|
160
164
|
const routed = resolveEngineForRole('executor', root);
|
|
161
165
|
if (routed && fleet.FLEET_CAPABLE.includes(routed.id)) return routed;
|
|
@@ -168,16 +172,30 @@ function readyExecutor(root, preferred = '') {
|
|
|
168
172
|
|
|
169
173
|
function readyValidators(root, preferred = '', exclude = '') {
|
|
170
174
|
const blocked = String(exclude || '').trim();
|
|
175
|
+
const rosterRanked = resolveEngineForRoleRanked('validator', root);
|
|
176
|
+
const fromRoster = rosterDecided(rosterRanked.source);
|
|
177
|
+
const rosterOrder = !preferred && fromRoster
|
|
178
|
+
? rosterRanked.ranked.map((engine) => engine.id)
|
|
179
|
+
: [];
|
|
180
|
+
const pinned = fromRoster && rosterRanked.engine
|
|
181
|
+
&& (rosterRanked.engine.roster_model || rosterRanked.engine.roster_effort || rosterRanked.engine.roster_max_seconds)
|
|
182
|
+
? rosterRanked.engine
|
|
183
|
+
: null;
|
|
184
|
+
// A roster pick can name an engine that reviews only when picked (codex),
|
|
185
|
+
// so the roster's list joins even when an engine lacks the role.
|
|
186
|
+
const rosterPicked = new Set(fromRoster ? rosterRanked.ranked.map((engine) => engine.id) : []);
|
|
171
187
|
const candidates = engineRegistryView(root)
|
|
172
|
-
.filter((engine) => engine.roles.includes('validator'))
|
|
188
|
+
.filter((engine) => engine.roles.includes('validator') || rosterPicked.has(engine.id))
|
|
173
189
|
.filter((engine) => engine.id !== blocked)
|
|
174
190
|
.sort((a, b) => {
|
|
175
191
|
const aPreferred = preferred && a.id === preferred ? 0 : 1;
|
|
176
192
|
const bPreferred = preferred && b.id === preferred ? 0 : 1;
|
|
177
193
|
return aPreferred - bPreferred
|
|
194
|
+
|| (rosterOrder.length ? (rosterOrder.indexOf(a.id) - rosterOrder.indexOf(b.id)) : 0)
|
|
178
195
|
|| Number(a.fallback_order) - Number(b.fallback_order)
|
|
179
196
|
|| String(a.id).localeCompare(String(b.id));
|
|
180
|
-
})
|
|
197
|
+
})
|
|
198
|
+
.map((engine) => (pinned && engine.id === pinned.id ? { ...engine, ...pinFields(pinned) } : engine));
|
|
181
199
|
const ready = candidates.filter((engine) => engine.health && engine.health.status === 'ready');
|
|
182
200
|
// Route-time determinism: which validator binaries exist on this machine
|
|
183
201
|
// must not change the lap's route. When none are ready, dispatch with the
|
|
@@ -187,6 +205,39 @@ function readyValidators(root, preferred = '', exclude = '') {
|
|
|
187
205
|
return ready.length ? ready : candidates;
|
|
188
206
|
}
|
|
189
207
|
|
|
208
|
+
function pinFields(engine) {
|
|
209
|
+
const pin = {};
|
|
210
|
+
for (const field of ['roster_model', 'roster_effort', 'roster_max_seconds']) {
|
|
211
|
+
if (engine && engine[field]) pin[field] = engine[field];
|
|
212
|
+
}
|
|
213
|
+
return pin;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// The roster's pins ride into the flight: the builder's model, effort, and
|
|
217
|
+
// time cap for the builder, and each reviewer's for that reviewer only. A
|
|
218
|
+
// reviewer with only a model keeps the plain model string.
|
|
219
|
+
function lapModelPins(executor, validators = []) {
|
|
220
|
+
const validatorModels = {};
|
|
221
|
+
for (const engine of validators) {
|
|
222
|
+
if (!engine) continue;
|
|
223
|
+
if (engine.roster_effort || engine.roster_max_seconds) {
|
|
224
|
+
validatorModels[engine.id] = {
|
|
225
|
+
model: engine.roster_model || '',
|
|
226
|
+
effort: engine.roster_effort || '',
|
|
227
|
+
max_seconds: engine.roster_max_seconds || 0,
|
|
228
|
+
};
|
|
229
|
+
} else if (engine.roster_model) {
|
|
230
|
+
validatorModels[engine.id] = engine.roster_model;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
model: executor && executor.roster_model || '',
|
|
235
|
+
...(executor && executor.roster_effort ? { effort: executor.roster_effort } : {}),
|
|
236
|
+
...(executor && executor.roster_max_seconds ? { maxSeconds: executor.roster_max_seconds } : {}),
|
|
237
|
+
validatorModels: Object.keys(validatorModels).length ? validatorModels : null,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
190
241
|
function taskById(runCli, taskId) {
|
|
191
242
|
const result = runCli(['task', 'show', taskId, '--json']);
|
|
192
243
|
if (!result || result.status !== 0) return null;
|
|
@@ -656,6 +707,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
656
707
|
reviewOnly: true,
|
|
657
708
|
verifierCommand: verifier,
|
|
658
709
|
validatorEngines: validators.map((entry) => entry.id),
|
|
710
|
+
...lapModelPins(executor, validators),
|
|
659
711
|
actor: mission.owner || 'mission-lead',
|
|
660
712
|
installedEngines: engineRegistryView(root)
|
|
661
713
|
.filter((entry) => entry.health && entry.health.status === 'ready')
|
|
@@ -773,4 +825,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
773
825
|
module.exports = {
|
|
774
826
|
oneLapSafetyIssue,
|
|
775
827
|
runOneLap,
|
|
828
|
+
readyExecutor,
|
|
829
|
+
readyValidators,
|
|
830
|
+
lapModelPins,
|
|
776
831
|
};
|
package/commands/pack.js
CHANGED
|
@@ -1622,7 +1622,7 @@ async function publishPack(rawArgs, cwd = process.cwd(), options = {}) {
|
|
|
1622
1622
|
}
|
|
1623
1623
|
|
|
1624
1624
|
// pack.json belongs to the pack, not to whatever repo happens to contain it
|
|
1625
|
-
// (2026-07-27: publishing
|
|
1625
|
+
// (2026-07-27: publishing a backend workspace's atris folder wrote a pack.json
|
|
1626
1626
|
// into atrisos-backend/ and polluted the source repo).
|
|
1627
1627
|
const manifestPath = path.join(sourceDir, 'pack.json');
|
|
1628
1628
|
const existing = readJson(manifestPath);
|
|
@@ -2453,7 +2453,7 @@ function printCapabilityTrustCard(policy, trust, receiptPath, userDenyRuleCount
|
|
|
2453
2453
|
if (options.operatorInput) {
|
|
2454
2454
|
console.log(` operator input: ${options.operatorInput.bytes} bytes injected; source path withheld from the pack and receipt`);
|
|
2455
2455
|
}
|
|
2456
|
-
console.log(` host shell: ${policy.grantedCapabilities.includes('host.shell') ? '
|
|
2456
|
+
console.log(` host shell: ${policy.grantedCapabilities.includes('host.shell') ? 'granted, bash can reach host files and network' : 'denied'}`);
|
|
2457
2457
|
console.log(' receipt coverage: Atris hook tool events; direct slash-skill invocations and later Claude or policy denials may not appear');
|
|
2458
2458
|
console.log(` receipt: ${receiptPath}`);
|
|
2459
2459
|
}
|
package/commands/probe.js
CHANGED
|
@@ -454,18 +454,24 @@ async function probeCommand(argv) {
|
|
|
454
454
|
const detail = ok
|
|
455
455
|
? `answer: ${resultText.slice(0, 80).replace(/\n/g, ' ')}`
|
|
456
456
|
: `dead-end: ${deadEnd}`;
|
|
457
|
-
const line =
|
|
457
|
+
const line = buildProbeLine({ stamp, where, model: a.model, member, engine, ok, secs, toolsRun, calendar: a.calendar, cliCalendarOps, detail });
|
|
458
|
+
console.log(line);
|
|
459
|
+
return ok ? 0 : 1;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function buildProbeLine({ stamp, where, model, member, engine, ok, secs, toolsRun, calendar, cliCalendarOps, detail }) {
|
|
463
|
+
const line = `- **atris-probe** \`${stamp}\` - ${where} · ${model}`
|
|
458
464
|
+ (member ? ` · member=${member}` : '')
|
|
459
465
|
+ (engine ? ` · engine=${engine}` : '')
|
|
460
|
-
+ ` · ${ok ? '
|
|
461
|
-
+ (
|
|
466
|
+
+ ` · ${ok ? 'pass' : 'fail'} · ${secs}s · tools=${toolsRun}`
|
|
467
|
+
+ (calendar ? ` cal_cli=${cliCalendarOps}` : '')
|
|
462
468
|
+ ` · ${detail}`;
|
|
463
|
-
|
|
464
|
-
return ok ? 0 : 1;
|
|
469
|
+
return line.replace(/\s*\u2014\s*/g, ', ');
|
|
465
470
|
}
|
|
466
471
|
|
|
467
472
|
module.exports = {
|
|
468
473
|
probeCommand,
|
|
474
|
+
buildProbeLine,
|
|
469
475
|
runAtris2Turn,
|
|
470
476
|
// exported for tests
|
|
471
477
|
normalizePath,
|
package/commands/pull.js
CHANGED
|
@@ -722,7 +722,7 @@ async function pullBusiness(slug) {
|
|
|
722
722
|
fs.mkdirSync(path.dirname(localPath), { recursive: true });
|
|
723
723
|
fs.writeFileSync(localPath, content);
|
|
724
724
|
}
|
|
725
|
-
console.log(` \u26A0 ${p.replace(/^\//, '')}
|
|
725
|
+
console.log(` \u26A0 ${p.replace(/^\//, '')}: conflict, both you and the computer changed this`);
|
|
726
726
|
console.log(` \u2192 Remote version saved as ${p.replace(/^\//, '')}.remote`);
|
|
727
727
|
conflictCount++;
|
|
728
728
|
}
|
package/commands/push.js
CHANGED
|
@@ -985,7 +985,7 @@ async function pushAtris() {
|
|
|
985
985
|
console.log(` ... +${failedToLand.length - shown.length} more`);
|
|
986
986
|
}
|
|
987
987
|
console.log('');
|
|
988
|
-
console.log('
|
|
988
|
+
console.log(' common causes: path is outside the workspace (for example, an absolute home path),');
|
|
989
989
|
console.log(' your role lacks write permission for that folder, or warm runner returned an error.');
|
|
990
990
|
console.log(' These files will appear as drift on your next push so you can retry.');
|
|
991
991
|
}
|
package/commands/rainmaker.js
CHANGED
|
@@ -9,22 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
|
-
const os = require('os');
|
|
13
12
|
const path = require('path');
|
|
14
13
|
const { spawnSync } = require('child_process');
|
|
15
|
-
|
|
16
|
-
function resolveBackendRoot() {
|
|
17
|
-
const candidates = [
|
|
18
|
-
process.env.ATRIS_BACKEND_DIR,
|
|
19
|
-
path.resolve(__dirname, '..', '..', 'atrisos-backend'),
|
|
20
|
-
path.resolve(process.cwd(), '..', 'atrisos-backend'),
|
|
21
|
-
path.join(os.homedir(), 'arena', 'atrisos-backend'),
|
|
22
|
-
].filter(Boolean);
|
|
23
|
-
for (const root of candidates) {
|
|
24
|
-
if (fs.existsSync(path.join(root, 'scripts', 'rainmaker.py'))) return root;
|
|
25
|
-
}
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
14
|
+
const { BACKEND_ROOT_HINT, resolveBackendRoot } = require('../utils/backend-root');
|
|
28
15
|
|
|
29
16
|
function rainmakerCommand(args = []) {
|
|
30
17
|
if (args[0] === '--help' || args[0] === '-h') {
|
|
@@ -32,8 +19,8 @@ function rainmakerCommand(args = []) {
|
|
|
32
19
|
return 0;
|
|
33
20
|
}
|
|
34
21
|
const root = resolveBackendRoot();
|
|
35
|
-
if (!root) {
|
|
36
|
-
console.error(
|
|
22
|
+
if (!root || !fs.existsSync(path.join(root, 'scripts', 'rainmaker.py'))) {
|
|
23
|
+
console.error(BACKEND_ROOT_HINT);
|
|
37
24
|
return 1;
|
|
38
25
|
}
|
|
39
26
|
const script = path.join(root, 'scripts', 'rainmaker.py');
|
package/commands/run.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
|
+
const { mapNotesPath } = require('../lib/map-refs');
|
|
12
13
|
const { execSync, spawnSync } = require('child_process');
|
|
13
14
|
const { getLogPath, ensureLogDirectory, createLogFile } = require('../lib/journal');
|
|
14
15
|
const { parseTodo } = require('../lib/todo');
|
|
@@ -118,11 +119,12 @@ function execPhaseCommandSync(cmd, opts = {}) {
|
|
|
118
119
|
* what the validator found - closing the try → notice → adjust loop.
|
|
119
120
|
*/
|
|
120
121
|
function buildRunPrompt(phase, context, priorCycleReview) {
|
|
121
|
-
const { mapPath, todoPath, personaPath, lessonsPath, journalPath } = context;
|
|
122
|
+
const { mapPath, notesPath, todoPath, personaPath, lessonsPath, journalPath } = context;
|
|
122
123
|
|
|
123
124
|
const readFiles = [
|
|
124
125
|
personaPath && `- ${personaPath}`,
|
|
125
126
|
mapPath && `- ${mapPath}`,
|
|
127
|
+
notesPath && `- ${notesPath}`,
|
|
126
128
|
todoPath && `- ${todoPath}`,
|
|
127
129
|
lessonsPath && `- ${lessonsPath}`,
|
|
128
130
|
journalPath && `- ${journalPath}`,
|
|
@@ -413,6 +415,7 @@ async function runAtris(options = {}) {
|
|
|
413
415
|
// Build context paths
|
|
414
416
|
const context = {
|
|
415
417
|
mapPath: fs.existsSync(path.join(atrisDir, 'MAP.md')) ? 'atris/MAP.md' : null,
|
|
418
|
+
notesPath: mapNotesPath(path.dirname(atrisDir)),
|
|
416
419
|
todoPath: fs.existsSync(path.join(atrisDir, 'TODO.md')) ? 'atris/TODO.md' : null,
|
|
417
420
|
personaPath: fs.existsSync(path.join(atrisDir, 'PERSONA.md')) ? 'atris/PERSONA.md' : null,
|
|
418
421
|
lessonsPath: fs.existsSync(path.join(atrisDir, 'lessons.md')) ? 'atris/lessons.md' : null,
|
package/commands/scout.js
CHANGED
|
@@ -53,7 +53,7 @@ const ENGINES = {
|
|
|
53
53
|
function scoutPrompt(question, dir) {
|
|
54
54
|
return [
|
|
55
55
|
`You are a read-only scout in ${dir}. You NEVER write, edit, or run mutating commands.`,
|
|
56
|
-
`Search order: index files first (atris/MAP.md, atris/TODO.md, README) via targeted reads, then grep. Cheap reads only.`,
|
|
56
|
+
`Search order: index files first (atris/MAP.md, atris/refs/MAP-NOTES.md if present for exact file:line refs, atris/TODO.md, README) via targeted reads, then grep. Cheap reads only.`,
|
|
57
57
|
``,
|
|
58
58
|
`Question: ${question}`,
|
|
59
59
|
``,
|
package/commands/search.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { existingMapDocs } = require('../lib/map-refs');
|
|
5
6
|
|
|
6
7
|
const MEMORY_LAYERS = [
|
|
7
8
|
{ key: 'features', dir: path.join('atris', 'features') },
|
|
@@ -226,10 +227,11 @@ function scanLayer(root, layer, terms) {
|
|
|
226
227
|
return scanFiles(root, layer.key, layer.dir.split(path.sep).join('/'), listMarkdownFiles(dir), terms);
|
|
227
228
|
}
|
|
228
229
|
|
|
230
|
+
// The map layer covers the boot map and the notes file that holds its deep refs.
|
|
229
231
|
function scanMap(root, terms) {
|
|
230
|
-
const
|
|
231
|
-
if (!
|
|
232
|
-
return scanFiles(root, 'map', 'atris/MAP.md',
|
|
232
|
+
const files = existingMapDocs(root).map((doc) => path.join(root, doc));
|
|
233
|
+
if (!files.length) return emptyLayerResult('map', 'atris/MAP.md');
|
|
234
|
+
return scanFiles(root, 'map', 'atris/MAP.md', files, terms);
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
function scanSource(root, terms) {
|
package/commands/spaceship.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* atris spaceship, bounded, self-reporting overnight runner.
|
|
5
5
|
*
|
|
6
6
|
* Thin wrapper over scripts/spaceship.sh. The script is the implementation
|
|
7
|
-
* (a supervised loop that survives bad ticks and emails
|
|
7
|
+
* (a supervised loop that survives bad ticks and emails the operator on every
|
|
8
8
|
* meaningful state change); this module just makes it reachable as
|
|
9
9
|
* `atris spaceship ...` and integrates with the CLI's command dispatch.
|
|
10
10
|
*
|
package/commands/stream.js
CHANGED
|
@@ -293,6 +293,31 @@ function pushEvent(events, input) {
|
|
|
293
293
|
if (event) events.push({ ...event, key: eventKey(event) });
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
// When timestamps match, state snapshots precede receipts, then landing summaries.
|
|
297
|
+
const STREAM_SOURCE_RANK = {
|
|
298
|
+
task: 20,
|
|
299
|
+
task_episode: 30,
|
|
300
|
+
scorecard: 40,
|
|
301
|
+
mission_event: 50,
|
|
302
|
+
mission_state: 60,
|
|
303
|
+
mission_receipt: 70,
|
|
304
|
+
mission_landing: 80,
|
|
305
|
+
worktree: 85,
|
|
306
|
+
landing: 90,
|
|
307
|
+
state: 10,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
function compareStreamEvents(a, b) {
|
|
311
|
+
const byMs = a.ms - b.ms;
|
|
312
|
+
if (byMs) return byMs;
|
|
313
|
+
const rankA = STREAM_SOURCE_RANK[a.source] ?? 55;
|
|
314
|
+
const rankB = STREAM_SOURCE_RANK[b.source] ?? 55;
|
|
315
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
316
|
+
const byKey = String(a.key || '').localeCompare(String(b.key || ''));
|
|
317
|
+
if (byKey) return byKey;
|
|
318
|
+
return String(a.event || '').localeCompare(String(b.event || ''));
|
|
319
|
+
}
|
|
320
|
+
|
|
296
321
|
function statePaths(root) {
|
|
297
322
|
const stateDir = path.join(root, '.atris', 'state');
|
|
298
323
|
return {
|
|
@@ -455,7 +480,6 @@ function collectRunReceiptEvents(root, deps, events) {
|
|
|
455
480
|
|
|
456
481
|
function candidateWorktreeDirs(root, deps) {
|
|
457
482
|
const dirs = new Set([
|
|
458
|
-
path.resolve(root, '..', '.agent-worktrees'),
|
|
459
483
|
path.join(path.dirname(root), '.agent-worktrees', path.basename(root)),
|
|
460
484
|
]);
|
|
461
485
|
const sidecar = readJson(statePaths(root).sidecar, deps, null);
|
|
@@ -606,7 +630,7 @@ function collectStreamEvents({ root = process.cwd(), sinceMs = 0, agent = '', no
|
|
|
606
630
|
if (wantedAgent && event.agent.toLowerCase() !== wantedAgent) continue;
|
|
607
631
|
byKey.set(event.key, event);
|
|
608
632
|
}
|
|
609
|
-
return [...byKey.values()].sort(
|
|
633
|
+
return [...byKey.values()].sort(compareStreamEvents);
|
|
610
634
|
}
|
|
611
635
|
|
|
612
636
|
function waitingOnOperator(tasks) {
|
|
@@ -841,6 +865,7 @@ function streamCommand(args = [], deps = defaultDeps()) {
|
|
|
841
865
|
module.exports = {
|
|
842
866
|
collectSnapshot,
|
|
843
867
|
collectStreamEvents,
|
|
868
|
+
compareStreamEvents,
|
|
844
869
|
createStreamState,
|
|
845
870
|
parseArgs,
|
|
846
871
|
parseSince,
|
package/commands/study.js
CHANGED
|
@@ -15,6 +15,7 @@ const fs = require('fs');
|
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const http = require('http');
|
|
17
17
|
const { spawn, spawnSync } = require('child_process');
|
|
18
|
+
const { resolveBackendRoot } = require('../utils/backend-root');
|
|
18
19
|
|
|
19
20
|
const FEED_URL = 'http://localhost:8777';
|
|
20
21
|
const FEED_STATS_URL = `${FEED_URL}/api/stats`;
|
|
@@ -91,15 +92,19 @@ function parseArgs(argv = []) {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
function resolveStudyRoot(cwd = process.cwd()) {
|
|
94
|
-
|
|
95
|
-
process.env.ATRIS_STUDY_ROOT
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
if (fs.existsSync(
|
|
102
|
-
}
|
|
95
|
+
if (process.env.ATRIS_STUDY_ROOT) {
|
|
96
|
+
const explicit = path.resolve(process.env.ATRIS_STUDY_ROOT);
|
|
97
|
+
if (fs.existsSync(explicit) && fs.statSync(explicit).isDirectory()) return explicit;
|
|
98
|
+
}
|
|
99
|
+
// Same order as before: an explicitly set backend root wins over the local folder.
|
|
100
|
+
if (process.env.ATRIS_BACKEND_ROOT) {
|
|
101
|
+
const envBackend = path.resolve(process.env.ATRIS_BACKEND_ROOT);
|
|
102
|
+
if (fs.existsSync(envBackend)) return envBackend;
|
|
103
|
+
}
|
|
104
|
+
const local = path.join(path.resolve(cwd), 'atris', 'study');
|
|
105
|
+
if (fs.existsSync(local) && fs.statSync(local).isDirectory()) return local;
|
|
106
|
+
const backend = resolveBackendRoot(cwd);
|
|
107
|
+
if (backend) return backend;
|
|
103
108
|
return null;
|
|
104
109
|
}
|
|
105
110
|
|
|
@@ -697,6 +702,7 @@ async function run(argv = []) {
|
|
|
697
702
|
|
|
698
703
|
module.exports = {
|
|
699
704
|
run,
|
|
705
|
+
resolveStudyRoot,
|
|
700
706
|
parseArgs,
|
|
701
707
|
showHelp,
|
|
702
708
|
wrapText,
|