atris 3.63.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/skills/commerce/SKILL.md +24 -0
- package/atris/skills/x-search/SKILL.md +3 -1
- package/ax +4 -3
- package/bin/atris.js +11 -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 +3 -3
- 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 +47 -9
- package/commands/mission.js +1 -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 +84 -2
- 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/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 +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-registry.js +85 -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/manifest.js +2 -2
- package/lib/map-refs.js +70 -4
- 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 +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/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/commands/member.js
CHANGED
|
@@ -5443,7 +5443,12 @@ function isWikiMinerNoiseName(value) {
|
|
|
5443
5443
|
const name = compactSentence(value || '', 120);
|
|
5444
5444
|
if (!name) return true;
|
|
5445
5445
|
if (/^[A-Z]{2,}$/.test(name)) return false;
|
|
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|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);
|
|
5447
5452
|
}
|
|
5448
5453
|
|
|
5449
5454
|
function normalizeWikiMinerExtraction(raw) {
|
|
@@ -5462,7 +5467,7 @@ function normalizeWikiMinerExtraction(raw) {
|
|
|
5462
5467
|
to: compactSentence(relationship?.to || '', 120),
|
|
5463
5468
|
type: allowedRelationTypes.has(String(relationship?.type || '').toLowerCase()) ? String(relationship.type).toLowerCase() : 'uses',
|
|
5464
5469
|
}))
|
|
5465
|
-
.filter((relationship) => !isWikiMinerNoiseName(relationship.from) && !isWikiMinerNoiseName(relationship.to));
|
|
5470
|
+
.filter((relationship) => !isWikiMinerNoiseName(relationship.from) && !isWikiMinerNoiseName(relationship.to) && !isWikiMinerSelfLoop(relationship));
|
|
5466
5471
|
return { entities, relationships };
|
|
5467
5472
|
}
|
|
5468
5473
|
|
|
@@ -5484,7 +5489,9 @@ function heuristicWikiMinerExtraction(pagePath, pageContent) {
|
|
|
5484
5489
|
}
|
|
5485
5490
|
const entities = [{ type: 'concept', name: title }];
|
|
5486
5491
|
for (const name of [...candidates].slice(0, 12)) entities.push({ type: /agent|api|sdk|cli|hub|system/i.test(name) ? 'system' : 'concept', name });
|
|
5487
|
-
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));
|
|
5488
5495
|
return { entities, relationships };
|
|
5489
5496
|
}
|
|
5490
5497
|
|
|
@@ -5538,11 +5545,12 @@ function mergeWikiGraph(graph, extraction, sourcePath) {
|
|
|
5538
5545
|
|
|
5539
5546
|
const relationshipMap = new Map();
|
|
5540
5547
|
for (const relationship of graph.relationships || []) {
|
|
5541
|
-
if (isWikiMinerNoiseName(relationship.from) || isWikiMinerNoiseName(relationship.to)) continue;
|
|
5548
|
+
if (isWikiMinerNoiseName(relationship.from) || isWikiMinerNoiseName(relationship.to) || isWikiMinerSelfLoop(relationship)) continue;
|
|
5542
5549
|
const key = `${lowerCompact(relationship.from)}|${String(relationship.type || 'uses').toLowerCase()}|${lowerCompact(relationship.to)}`;
|
|
5543
5550
|
relationshipMap.set(key, { ...relationship, sources: Array.isArray(relationship.sources) ? relationship.sources : [] });
|
|
5544
5551
|
}
|
|
5545
5552
|
for (const relationship of extraction.relationships || []) {
|
|
5553
|
+
if (isWikiMinerNoiseName(relationship.from) || isWikiMinerNoiseName(relationship.to) || isWikiMinerSelfLoop(relationship)) continue;
|
|
5546
5554
|
const key = `${lowerCompact(relationship.from)}|${relationship.type}|${lowerCompact(relationship.to)}`;
|
|
5547
5555
|
const existing = relationshipMap.get(key) || { from: relationship.from, to: relationship.to, type: relationship.type, sources: [] };
|
|
5548
5556
|
if (sourcePath && !existing.sources.includes(sourcePath)) existing.sources.push(sourcePath);
|
|
@@ -5819,7 +5827,6 @@ async function callSupervisorLlm(receipts, logs) {
|
|
|
5819
5827
|
const output = { isTTY: false, write() { return true; } };
|
|
5820
5828
|
const result = await postTurn(supervisorPrompt(receipts, logs), {
|
|
5821
5829
|
mode: process.env.ATRIS_SUPERVISOR_LLM_MODE || 'fast',
|
|
5822
|
-
route: 'local',
|
|
5823
5830
|
cwd: process.cwd(),
|
|
5824
5831
|
output,
|
|
5825
5832
|
color: false,
|
|
@@ -5905,11 +5912,21 @@ async function runSupervisorWake(name, paths, { execute = false } = {}) {
|
|
|
5905
5912
|
analysis = normalizeSupervisorAnalysis(llm.analysis, { status: 'ok', llm_source: llm.source });
|
|
5906
5913
|
if (!analysis) {
|
|
5907
5914
|
reason = 'llm_json_parse_failed';
|
|
5908
|
-
analysis =
|
|
5915
|
+
analysis = {
|
|
5916
|
+
...fallbackSupervisorAnalysis(receipts),
|
|
5917
|
+
status: 'parse_error',
|
|
5918
|
+
llm_source: llm.source,
|
|
5919
|
+
llm_error: 'invalid_analysis_shape',
|
|
5920
|
+
};
|
|
5909
5921
|
}
|
|
5910
5922
|
} else if (llm?.error) {
|
|
5911
5923
|
reason = llm.error === 'invalid_json' ? 'llm_json_parse_failed' : 'llm_analysis_failed';
|
|
5912
|
-
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
|
+
};
|
|
5913
5930
|
} else {
|
|
5914
5931
|
reason = 'llm_not_configured';
|
|
5915
5932
|
analysis = fallbackSupervisorAnalysis(receipts);
|
|
@@ -7351,7 +7368,7 @@ async function runGeneralistWake(name, paths, { execute = false, domainInput = {
|
|
|
7351
7368
|
record: domain.ok && proof.status === 'ok',
|
|
7352
7369
|
libraryBefore: crossDomainLibraryBefore,
|
|
7353
7370
|
receiptPath,
|
|
7354
|
-
proofPath,
|
|
7371
|
+
proofPath: execute ? proofPath : null,
|
|
7355
7372
|
});
|
|
7356
7373
|
proof.learning = {
|
|
7357
7374
|
library_path: path.relative(root, learning.library_path),
|
|
@@ -7782,6 +7799,24 @@ function wakeRuleDirtyMemberScope(ctx) {
|
|
|
7782
7799
|
};
|
|
7783
7800
|
}
|
|
7784
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
|
+
|
|
7785
7820
|
// Rule: nothing blocks the member, so take the safe next bounded step.
|
|
7786
7821
|
function wakeRuleSafeBoundedTick(ctx) {
|
|
7787
7822
|
return {
|
|
@@ -7802,6 +7837,7 @@ const WAKE_DECISION_RULES = [
|
|
|
7802
7837
|
wakeRuleOpenReviewableProposal,
|
|
7803
7838
|
wakeRuleScoredNonTickCandidate,
|
|
7804
7839
|
wakeRuleDirtyMemberScope,
|
|
7840
|
+
wakeRuleTerminalMissionGoal,
|
|
7805
7841
|
wakeRuleSafeBoundedTick,
|
|
7806
7842
|
];
|
|
7807
7843
|
|
|
@@ -7815,6 +7851,8 @@ function wakeDecision(name, paths, { force = false, runtimeKind = memberRuntimeK
|
|
|
7815
7851
|
const steering = readSteeringMemory(paths, name);
|
|
7816
7852
|
const state = loadMemberGoals(name, paths);
|
|
7817
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;
|
|
7818
7856
|
const current = memberOpenExperiment(state);
|
|
7819
7857
|
const generalistDecision = wakeRuleGeneralistDomainFile(name, { purpose, steering, state, goal, current });
|
|
7820
7858
|
if (generalistDecision) return generalistDecision;
|
|
@@ -7860,7 +7898,7 @@ function wakeDecision(name, paths, { force = false, runtimeKind = memberRuntimeK
|
|
|
7860
7898
|
next_command: wakeScores.selected.next_command,
|
|
7861
7899
|
} : null;
|
|
7862
7900
|
|
|
7863
|
-
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 };
|
|
7864
7902
|
for (const rule of WAKE_DECISION_RULES) {
|
|
7865
7903
|
const verdict = rule(ctx);
|
|
7866
7904
|
if (verdict) return composeWakeDecision(ctx, verdict);
|
package/commands/mission.js
CHANGED
|
@@ -570,7 +570,7 @@ function applyMissionRunnerProfile(runner) {
|
|
|
570
570
|
// it only ever takes an explicit pin (the mission's model or the roster
|
|
571
571
|
// line's); '' lets the engine ride its own default.
|
|
572
572
|
// A mission often still carries the claude model it was created with; an
|
|
573
|
-
// engine that runs only its own models (codex) drops it.
|
|
573
|
+
// engine that runs only its own models (codex, grok) drops it.
|
|
574
574
|
const CLAUDE_FAMILY_RUNNER_ENGINES = new Set(['claude', 'fable', 'haiku']);
|
|
575
575
|
|
|
576
576
|
function resolveMissionTickRunnerModel(mission) {
|
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
|
package/commands/task.js
CHANGED
|
@@ -253,6 +253,8 @@ atris task - durable local task state (SQLite, gitignored)
|
|
|
253
253
|
Sweep off-roadmap/duplicate work as archived (not failed);
|
|
254
254
|
--from-failed opts in to relabel a fail-closed row (never done)
|
|
255
255
|
atris task clear-done [--before <days>] [--dry-run] [--json] Archive completed rows, oldest first
|
|
256
|
+
atris task prune-test-junk [--yes] [--vacuum] [--json]
|
|
257
|
+
Count rows test runs left from temp folders; --yes backs up the db, then removes them
|
|
256
258
|
atris task keep [--json] Put away finished and untouched work, then refresh the list
|
|
257
259
|
atris task reap-mission-blockers [--json] Close blocker rows whose missions are complete or stopped
|
|
258
260
|
atris task relabel-archived [--dry-run|--apply]
|
|
@@ -1421,7 +1423,7 @@ function taskReviewSummary(task) {
|
|
|
1421
1423
|
const reviewPassCount = Number(metadata.agent_review_pass_count || payload.review_pass_count || 0);
|
|
1422
1424
|
const agentCertified = metadata.agent_certified === true
|
|
1423
1425
|
|| payload.agent_certified === true
|
|
1424
|
-
|| reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES;
|
|
1426
|
+
|| (reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES && reviewIntegrity.hasIndependentReview(task));
|
|
1425
1427
|
const reviewedEventHas = (key) => reviewed && reviewed.event_type === 'reviewed'
|
|
1426
1428
|
&& Object.prototype.hasOwnProperty.call(payload, key);
|
|
1427
1429
|
const clearedReviewFields = new Set(Array.isArray(payload.cleared_review_fields) ? payload.cleared_review_fields : []);
|
|
@@ -8841,7 +8843,7 @@ function taskAllowsReviewChat(task, { allowCertified = false } = {}) {
|
|
|
8841
8843
|
if (allowCertified) return true;
|
|
8842
8844
|
if (review.agent_certified === true || metadata.agent_certified === true) return false;
|
|
8843
8845
|
const reviewPassCount = Number(review.agent_review_pass_count || metadata.agent_review_pass_count || 0);
|
|
8844
|
-
if (reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES) return false;
|
|
8846
|
+
if (reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES && reviewIntegrity.hasIndependentReview(task)) return false;
|
|
8845
8847
|
const handoff = reviewHandoffForTask(task, { suppressExistingFollowUp: true });
|
|
8846
8848
|
return !(handoff && (handoff.next_action === 'continue_work' || handoff.next_action === 'human_accept_waiting'));
|
|
8847
8849
|
}
|
|
@@ -9814,6 +9816,78 @@ function cmdClearDone(args) {
|
|
|
9814
9816
|
console.log(`cleared ${candidates.length} completed task(s).`);
|
|
9815
9817
|
}
|
|
9816
9818
|
|
|
9819
|
+
function formatBytes(bytes) {
|
|
9820
|
+
const mb = Number(bytes || 0) / (1024 * 1024);
|
|
9821
|
+
return mb >= 1 ? `${mb.toFixed(1)} MB` : `${Math.round(Number(bytes || 0) / 1024)} KB`;
|
|
9822
|
+
}
|
|
9823
|
+
|
|
9824
|
+
// Test runs that forgot ATRIS_TASKS_DB left rows in the shared database
|
|
9825
|
+
// under temp folders. Default is a read-only count; --yes backs up, then
|
|
9826
|
+
// removes those rows and their history. Real project rows are never touched.
|
|
9827
|
+
function cmdPruneTestJunk(args) {
|
|
9828
|
+
const taskDb = getTaskDb();
|
|
9829
|
+
const prune = require('../lib/task-prune');
|
|
9830
|
+
const dbPath = taskDb.getDbPath();
|
|
9831
|
+
const apply = hasFlag(args, '--yes') && !hasFlag(args, '--dry-run');
|
|
9832
|
+
const vacuum = apply && hasFlag(args, '--vacuum');
|
|
9833
|
+
if (!apply) {
|
|
9834
|
+
const plan = prune.dryRun(dbPath);
|
|
9835
|
+
const { rows, ...summary } = plan;
|
|
9836
|
+
if (wantsJson(args)) {
|
|
9837
|
+
printJson({ ok: true, action: 'prune-test-junk', dry_run: true, ...summary });
|
|
9838
|
+
return;
|
|
9839
|
+
}
|
|
9840
|
+
if (plan.missing) {
|
|
9841
|
+
console.log(`no task database at ${dbPath}. nothing to prune.`);
|
|
9842
|
+
return;
|
|
9843
|
+
}
|
|
9844
|
+
const r = plan.remove;
|
|
9845
|
+
console.log('prune-test-junk dry run: nothing was changed.');
|
|
9846
|
+
console.log(`database: ${dbPath} (${formatBytes(plan.db_bytes)}, ${plan.total_tasks} tasks)`);
|
|
9847
|
+
console.log(`would remove ${r.tasks} test task(s) from temp folders, ${r.history_rows} history row(s), `
|
|
9848
|
+
+ `${r.stray_history_rows} stray history row(s), ${r.part_uses} part use row(s).`);
|
|
9849
|
+
for (const g of plan.groups) {
|
|
9850
|
+
console.log(` ${g.prefix.padEnd(22)} ${String(g.tasks).padStart(7)} tasks ${String(g.history_rows + g.stray_history_rows).padStart(8)} history rows`);
|
|
9851
|
+
}
|
|
9852
|
+
const k = plan.kept;
|
|
9853
|
+
console.log(`kept: ${k.folder_exists} temp task(s) whose folder still exists, ${k.recent} active in the last ${plan.grace_hours} hour(s), `
|
|
9854
|
+
+ `${k.held} with history from a real project, ${k.stray_held + k.stray_recent} leftover history set(s) that are recent or name a real project.`);
|
|
9855
|
+
if (r.tasks || r.stray_history_rows) {
|
|
9856
|
+
console.log('run with --yes to remove them. a backup copy is written next to the database first.');
|
|
9857
|
+
}
|
|
9858
|
+
return;
|
|
9859
|
+
}
|
|
9860
|
+
|
|
9861
|
+
if (vacuum && !wantsJson(args)) {
|
|
9862
|
+
console.log('note: --vacuum rewrites the whole file and needs every other atris process idle; it waits a few seconds, then gives up.');
|
|
9863
|
+
}
|
|
9864
|
+
let result;
|
|
9865
|
+
try {
|
|
9866
|
+
result = prune.applyPrune(dbPath, { vacuum });
|
|
9867
|
+
} catch (err) {
|
|
9868
|
+
failTask('atris task prune-test-junk', 'prune_failed', `prune-test-junk stopped: ${err && err.message || err}`, 1);
|
|
9869
|
+
}
|
|
9870
|
+
if (wantsJson(args)) {
|
|
9871
|
+
printJson({ ok: true, action: 'prune-test-junk', dry_run: false, ...result });
|
|
9872
|
+
return;
|
|
9873
|
+
}
|
|
9874
|
+
if (result.missing) {
|
|
9875
|
+
console.log(`no task database at ${dbPath}. nothing to prune.`);
|
|
9876
|
+
return;
|
|
9877
|
+
}
|
|
9878
|
+
const r = result.removed;
|
|
9879
|
+
console.log(`backup: ${result.backup_path} (${result.backup_tasks} tasks)`);
|
|
9880
|
+
console.log(`removed ${r.tasks} test task(s), ${r.history_rows} history row(s), `
|
|
9881
|
+
+ `${r.stray_history_rows} stray history row(s), ${r.part_uses} part use row(s).`);
|
|
9882
|
+
const sk = result.skipped;
|
|
9883
|
+
if (sk.held || sk.changed_since_backup || sk.recent || sk.folder_exists) {
|
|
9884
|
+
console.log(`left ${sk.changed_since_backup} that changed after the backup, ${sk.recent} that became active, `
|
|
9885
|
+
+ `${sk.folder_exists} whose folder came back, and ${sk.held} with real-project history.`);
|
|
9886
|
+
}
|
|
9887
|
+
console.log(`database now has ${result.total_tasks} tasks, ${formatBytes(result.bytes_after)} on disk`
|
|
9888
|
+
+ (result.vacuumed ? ' after vacuum.' : '. run again with --yes --vacuum to shrink the file.'));
|
|
9889
|
+
}
|
|
9890
|
+
|
|
9817
9891
|
function cmdReapMissionBlockers(args) {
|
|
9818
9892
|
const taskDb = getTaskDb();
|
|
9819
9893
|
const db = taskDb.open();
|
|
@@ -10468,6 +10542,13 @@ function stampReadyVerifyMetadata(taskDb, db, taskId, verify) {
|
|
|
10468
10542
|
if (!row) return;
|
|
10469
10543
|
const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
|
|
10470
10544
|
metadata.verify = verify;
|
|
10545
|
+
// A real verify stamped at ready means the creation-time degraded flag is
|
|
10546
|
+
// stale: clear it so `task show` stops warning about a missing check. A
|
|
10547
|
+
// diff-only verify keeps the flag; it still cannot fail on real breakage.
|
|
10548
|
+
if (verify.toLowerCase() !== 'git diff --check') {
|
|
10549
|
+
delete metadata.verification_status;
|
|
10550
|
+
delete metadata.verification_degraded_reason;
|
|
10551
|
+
}
|
|
10471
10552
|
db.prepare(`
|
|
10472
10553
|
UPDATE tasks
|
|
10473
10554
|
SET metadata = ?,
|
|
@@ -13374,6 +13455,7 @@ async function runTaskCommand(args) {
|
|
|
13374
13455
|
case 'fail': return cmdDone([...rest, '--failed']);
|
|
13375
13456
|
case 'archive': return cmdArchive(rest);
|
|
13376
13457
|
case 'clear-done': return cmdClearDone(rest);
|
|
13458
|
+
case 'prune-test-junk': return cmdPruneTestJunk(rest);
|
|
13377
13459
|
case 'keep': return cmdKeep(rest);
|
|
13378
13460
|
case 'reap-mission-blockers':
|
|
13379
13461
|
case 'reap-blockers':
|
package/commands/transactions.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
const { apiRequestJson } = require('../utils/api');
|
|
7
7
|
const { loadCredentials } = require('../utils/auth');
|
|
8
|
-
const { NOT_LOGGED_IN,
|
|
9
|
-
const { parseFlags, formatCents, errorFrom } = require('../lib/commerce');
|
|
8
|
+
const { NOT_LOGGED_IN, oneLine, printResult } = require('../lib/developer-api');
|
|
9
|
+
const { parseFlags, formatCents, errorFrom, commerceToken } = require('../lib/commerce');
|
|
10
10
|
|
|
11
11
|
const TX_KINDS = ['ticket', 'shop', 'flight', 'test_purchase', 'fee', 'trade', 'wallet_send'];
|
|
12
12
|
|
|
@@ -98,7 +98,7 @@ async function transactionsCommand(args = [], deps = {}) {
|
|
|
98
98
|
|
|
99
99
|
const load = deps.loadCredentials || loadCredentials;
|
|
100
100
|
const request = deps.apiRequestJson || apiRequestJson;
|
|
101
|
-
const token =
|
|
101
|
+
const token = commerceToken(load(), 'transactions:read', deps.now ? deps.now() : Date.now());
|
|
102
102
|
if (!token) return fail(NOT_LOGGED_IN);
|
|
103
103
|
|
|
104
104
|
try {
|
package/commands/truth.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
-
const
|
|
10
|
+
const { stateHome } = require('../lib/state-home');
|
|
11
11
|
const { execFileSync } = require('child_process');
|
|
12
12
|
const taskDb = require('../lib/task-db');
|
|
13
13
|
const { parseScopeFlag } = require('../lib/cli-scope');
|
|
@@ -172,7 +172,7 @@ function loadFeatures(cwd) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
function loadHeartbeats() {
|
|
175
|
-
const hbDir = path.join(
|
|
175
|
+
const hbDir = path.join(stateHome(), '.atris', 'heartbeat');
|
|
176
176
|
const registry = readJson(path.join(hbDir, 'registry.json'));
|
|
177
177
|
const state = readJson(path.join(hbDir, 'state.json')) || {};
|
|
178
178
|
if (!registry || !Array.isArray(registry.jobs)) return [];
|
package/commands/x-search.js
CHANGED
|
@@ -4,7 +4,9 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { apiRequestJson } = require('../utils/api');
|
|
6
6
|
const { ensureBilledCommandAuth } = require('./auth');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
7
8
|
const applyGate = require('../lib/apply-gate');
|
|
9
|
+
const { checkXPosts, annotateContent, formatTally } = require('../lib/x-post-check');
|
|
8
10
|
const {
|
|
9
11
|
fileTeachExperiment,
|
|
10
12
|
extractTeachNumbers,
|
|
@@ -23,9 +25,10 @@ const KEEP_RULE = 'keep only if measure.py moves 0→1. scores 1 only when the f
|
|
|
23
25
|
|
|
24
26
|
function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
|
|
25
27
|
output('');
|
|
26
|
-
output(`Usage: ${commandName} "<query>" [--limit N] [--days N] [--save] [--json]`);
|
|
27
|
-
output(` ${commandName} person --name <name> [--handle <h>] [--company <c>] [--context <text>] [--save] [--json]`);
|
|
28
|
+
output(`Usage: ${commandName} "<query>" [--limit N] [--days N] [--save] [--json] [--no-check]`);
|
|
29
|
+
output(` ${commandName} person --name <name> [--handle <h>] [--company <c>] [--context <text>] [--save] [--json] [--no-check]`);
|
|
28
30
|
output(` ${commandName} unsave <query-or-source>`);
|
|
31
|
+
output(` ${commandName} bench [--quick|--free]`);
|
|
29
32
|
output('');
|
|
30
33
|
output(`Search X/Twitter via Atris (${COST_HINT}).`);
|
|
31
34
|
output('Requires login. Same auth path as atris youtube process.');
|
|
@@ -33,13 +36,15 @@ function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
|
|
|
33
36
|
output('--save files a brief only when the result is rich.');
|
|
34
37
|
output('unsave deletes the filed brief, apply stub, and matching experiment pack (no paid calls).');
|
|
35
38
|
output('Empty or failed search prints credits refunded only when the server marks a refund.');
|
|
39
|
+
output('Each quoted post is looked up on X for free and marked checked, unverified, or unknown.');
|
|
36
40
|
output('');
|
|
37
41
|
output('Options:');
|
|
38
42
|
output(' --limit <n> Max results hint (search only)');
|
|
39
43
|
output(' --days <n> Only tweets from the last N days (search only)');
|
|
40
44
|
output(' --save File brief, journal, apply; rich results mint a keep/revert experiment');
|
|
41
45
|
output(' --unsave Delete filed brief, apply stub, and matching experiment pack (no paid calls)');
|
|
42
|
-
output(' --json Print the raw JSON response');
|
|
46
|
+
output(' --json Print the raw JSON response (plus a checks list)');
|
|
47
|
+
output(' --no-check Skip looking up the quoted posts on X');
|
|
43
48
|
output(' -h, --help This help');
|
|
44
49
|
output('');
|
|
45
50
|
output('Person options:');
|
|
@@ -55,6 +60,7 @@ function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
|
|
|
55
60
|
output(` ${commandName} person --name "Leah Bonvissuto" --handle leahbon`);
|
|
56
61
|
output(` ${commandName} unsave "MCP agents"`);
|
|
57
62
|
output(` ${commandName} --unsave "MCP agents"`);
|
|
63
|
+
output(` ${commandName} bench --free`);
|
|
58
64
|
output('');
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -81,6 +87,7 @@ function parseSearchArgs(argv = []) {
|
|
|
81
87
|
json: false,
|
|
82
88
|
save: false,
|
|
83
89
|
unsave: false,
|
|
90
|
+
check: true,
|
|
84
91
|
query: null,
|
|
85
92
|
limit: null,
|
|
86
93
|
daysBack: null,
|
|
@@ -100,6 +107,8 @@ function parseSearchArgs(argv = []) {
|
|
|
100
107
|
options.json = true;
|
|
101
108
|
} else if (arg === '--save') {
|
|
102
109
|
options.save = true;
|
|
110
|
+
} else if (arg === '--no-check') {
|
|
111
|
+
options.check = false;
|
|
103
112
|
} else if (arg === '--unsave') {
|
|
104
113
|
options.unsave = true;
|
|
105
114
|
} else if (arg === '--limit') {
|
|
@@ -153,6 +162,7 @@ function parsePersonArgs(argv = []) {
|
|
|
153
162
|
help: false,
|
|
154
163
|
json: false,
|
|
155
164
|
save: false,
|
|
165
|
+
check: true,
|
|
156
166
|
name: null,
|
|
157
167
|
handle: null,
|
|
158
168
|
company: null,
|
|
@@ -173,6 +183,8 @@ function parsePersonArgs(argv = []) {
|
|
|
173
183
|
options.json = true;
|
|
174
184
|
} else if (arg === '--save') {
|
|
175
185
|
options.save = true;
|
|
186
|
+
} else if (arg === '--no-check') {
|
|
187
|
+
options.check = false;
|
|
176
188
|
} else if (arg === '--name') {
|
|
177
189
|
options.name = readValue(args, i, arg);
|
|
178
190
|
i++;
|
|
@@ -592,23 +604,26 @@ function ensureXSearchApply({ cwd, source, packRel, now, output } = {}) {
|
|
|
592
604
|
});
|
|
593
605
|
}
|
|
594
606
|
|
|
595
|
-
|
|
607
|
+
// check is the result of checkXPosts. With it, each post carries its own
|
|
608
|
+
// link or warning, and citations no post uses are listed as other sources.
|
|
609
|
+
function formatXSearchResult(data, check = null) {
|
|
596
610
|
const lines = [];
|
|
597
611
|
const content = xSearchContent(data);
|
|
598
612
|
const citations = xSearchCitations(data);
|
|
599
613
|
|
|
600
614
|
if (content) {
|
|
601
|
-
lines.push(content);
|
|
615
|
+
lines.push(check ? annotateContent(content, check) : content);
|
|
602
616
|
} else if (data?.message) {
|
|
603
617
|
lines.push(String(data.message).trim());
|
|
604
618
|
} else {
|
|
605
619
|
lines.push('X search completed');
|
|
606
620
|
}
|
|
607
621
|
|
|
608
|
-
|
|
622
|
+
const listed = check ? check.otherSources : citations;
|
|
623
|
+
if (listed.length) {
|
|
609
624
|
lines.push('');
|
|
610
|
-
lines.push('Citations:');
|
|
611
|
-
for (const cite of
|
|
625
|
+
lines.push(check ? 'Other sources:' : 'Citations:');
|
|
626
|
+
for (const cite of listed) {
|
|
612
627
|
lines.push(` ${cite}`);
|
|
613
628
|
}
|
|
614
629
|
}
|
|
@@ -619,9 +634,54 @@ function formatXSearchResult(data) {
|
|
|
619
634
|
lines.push(...creditLines);
|
|
620
635
|
}
|
|
621
636
|
|
|
637
|
+
if (check) {
|
|
638
|
+
lines.push('');
|
|
639
|
+
lines.push(formatTally(check.tally));
|
|
640
|
+
}
|
|
641
|
+
|
|
622
642
|
return lines.join('\n');
|
|
623
643
|
}
|
|
624
644
|
|
|
645
|
+
// Look the quoted posts up on X. Free, and never fatal: any failure means no
|
|
646
|
+
// check, and the original output prints unchanged.
|
|
647
|
+
async function checkXSearchPosts(data, options, deps = {}) {
|
|
648
|
+
if (options.check === false) return null;
|
|
649
|
+
try {
|
|
650
|
+
return await (deps.checkXPosts || checkXPosts)({
|
|
651
|
+
content: xSearchContent(data),
|
|
652
|
+
citations: xSearchCitations(data),
|
|
653
|
+
fetchPost: deps.fetchPost,
|
|
654
|
+
fetch: deps.fetch,
|
|
655
|
+
});
|
|
656
|
+
} catch {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function renderXSearchResult(data, check) {
|
|
662
|
+
if (check) {
|
|
663
|
+
try {
|
|
664
|
+
return formatXSearchResult(data, check);
|
|
665
|
+
} catch {
|
|
666
|
+
// fall through to the original output
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return formatXSearchResult(data);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function runXSearchBench(argv, deps = {}) {
|
|
673
|
+
const run = (deps.spawnSync || spawnSync)(
|
|
674
|
+
process.execPath,
|
|
675
|
+
[path.join(__dirname, '..', 'scripts', 'det', 'xsearch-bench.js'), ...argv],
|
|
676
|
+
{ stdio: 'inherit' },
|
|
677
|
+
);
|
|
678
|
+
if (run.error) {
|
|
679
|
+
(deps.output || console.error)(`bench did not start: ${run.error.message}`);
|
|
680
|
+
return 1;
|
|
681
|
+
}
|
|
682
|
+
return typeof run.status === 'number' ? run.status : 1;
|
|
683
|
+
}
|
|
684
|
+
|
|
625
685
|
function formatEmptyXSearchResult(data) {
|
|
626
686
|
const lines = ['no results'];
|
|
627
687
|
const creditLines = formatCreditsLines(xSearchCredits(data));
|
|
@@ -634,6 +694,11 @@ function formatEmptyXSearchResult(data) {
|
|
|
634
694
|
|
|
635
695
|
async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
|
|
636
696
|
const output = deps.output || ((line = '') => console.log(line));
|
|
697
|
+
if (argv[0] === 'bench') {
|
|
698
|
+
const code = runXSearchBench(argv.slice(1), deps);
|
|
699
|
+
if (!deps.output && !deps.spawnSync) process.exit(code);
|
|
700
|
+
return code;
|
|
701
|
+
}
|
|
637
702
|
let options;
|
|
638
703
|
try {
|
|
639
704
|
options = parseXSearchArgs(argv);
|
|
@@ -655,10 +720,14 @@ async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
|
|
|
655
720
|
try {
|
|
656
721
|
const data = await runXSearch(options, deps);
|
|
657
722
|
const hasResults = xSearchHasResults(data);
|
|
723
|
+
const check = hasResults ? await checkXSearchPosts(data, options, deps) : null;
|
|
658
724
|
if (options.json) {
|
|
659
|
-
|
|
725
|
+
const withChecks = check && data && typeof data === 'object' && !Array.isArray(data)
|
|
726
|
+
? { ...data, checks: check.checks }
|
|
727
|
+
: data;
|
|
728
|
+
output(JSON.stringify(withChecks, null, 2));
|
|
660
729
|
} else {
|
|
661
|
-
output(hasResults ?
|
|
730
|
+
output(hasResults ? renderXSearchResult(data, check) : formatEmptyXSearchResult(data));
|
|
662
731
|
}
|
|
663
732
|
if (!hasResults) {
|
|
664
733
|
if (
|
|
@@ -734,5 +803,7 @@ module.exports = {
|
|
|
734
803
|
xSearchExperimentSlug,
|
|
735
804
|
xSearchExperimentRel,
|
|
736
805
|
unsaveXSearch,
|
|
806
|
+
formatXSearchResult,
|
|
807
|
+
runXSearchBench,
|
|
737
808
|
xSearchCommand,
|
|
738
809
|
};
|
package/commands/youtube.js
CHANGED
|
@@ -5,6 +5,7 @@ const { spawnSync } = require('child_process');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const { stateHome } = require('../lib/state-home');
|
|
8
9
|
const https = require('https');
|
|
9
10
|
|
|
10
11
|
const YTNOTES_USAGE = 'usage: ytnotes <youtube-url> [youtube-url-or-playlist...] [auto|gemini|haiku|atris-fast|grok|codex|cursor]';
|
|
@@ -2256,7 +2257,7 @@ function searchCacheTtlMs(deps = {}) {
|
|
|
2256
2257
|
|
|
2257
2258
|
function resolveSearchCachePath(deps = {}) {
|
|
2258
2259
|
if (deps.searchCachePath) return deps.searchCachePath;
|
|
2259
|
-
const homeDir = deps.homeDir ||
|
|
2260
|
+
const homeDir = deps.homeDir || stateHome();
|
|
2260
2261
|
return path.join(homeDir, '.atris', LOCAL_SEARCH_CACHE_FILE);
|
|
2261
2262
|
}
|
|
2262
2263
|
|
package/lib/autoland.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
+
const { stateHome } = require('./state-home');
|
|
10
11
|
const os = require('os');
|
|
11
12
|
const { spawnSync } = require('child_process');
|
|
12
13
|
const { computeTrustTier } = require('./trust-tiers');
|
|
@@ -189,7 +190,7 @@ function buildCronLine(root) {
|
|
|
189
190
|
const node = process.execPath;
|
|
190
191
|
const bin = path.join(root, 'bin', 'atris.js');
|
|
191
192
|
const cliBin = fs.existsSync(bin) ? bin : 'atris';
|
|
192
|
-
const logDir = path.join(
|
|
193
|
+
const logDir = path.join(stateHome(), '.atris', 'overnight');
|
|
193
194
|
const logFile = path.join(logDir, `autoland-${path.basename(root)}.log`);
|
|
194
195
|
const invoke = cliBin.endsWith('.js') ? `${node} ${cliBin}` : cliBin;
|
|
195
196
|
return `14 * * * * cd ${root} && ${invoke} autoland tick >> ${logFile} 2>&1 # ${cronMarker(root)}`;
|
|
@@ -210,7 +211,7 @@ function cronInstalled(root) {
|
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
function installCron(root) {
|
|
213
|
-
fs.mkdirSync(path.join(
|
|
214
|
+
fs.mkdirSync(path.join(stateHome(), '.atris', 'overnight'), { recursive: true });
|
|
214
215
|
const marker = cronMarker(root);
|
|
215
216
|
const kept = readCrontab().split(/\r?\n/).filter((line) => line.trim() && !line.includes(marker));
|
|
216
217
|
const next = `${kept.join('\n')}${kept.length ? '\n' : ''}${buildCronLine(root)}\n`;
|
package/lib/ax-auto-lane.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
|
-
const os = require('node:os');
|
|
5
4
|
const path = require('node:path');
|
|
5
|
+
const { stateHome } = require('./state-home');
|
|
6
6
|
|
|
7
7
|
const SHORT_LOOKUP_MAX_CHARS = 140;
|
|
8
8
|
const LONG_INPUT_MIN_CHARS = 1200;
|
|
@@ -75,7 +75,7 @@ function overrideMatches(text, override) {
|
|
|
75
75
|
function loadOverrides(filePath) {
|
|
76
76
|
const resolved = filePath
|
|
77
77
|
|| process.env.ATRIS_ROUTER_OVERRIDES_PATH
|
|
78
|
-
|| path.join(
|
|
78
|
+
|| path.join(stateHome(), '.atris', 'router-overrides.json');
|
|
79
79
|
try {
|
|
80
80
|
const parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
81
81
|
return Array.isArray(parsed) ? parsed : [];
|