atris 3.37.1 → 3.40.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 +25 -6
- package/README.md +2 -0
- package/atris/PERSONA.md +11 -11
- package/atris.md +17 -2
- package/ax +160 -34
- package/bin/atris.js +78 -6
- package/commands/activate.js +16 -3
- package/commands/agent-spawn.js +13 -18
- package/commands/autoland.js +76 -47
- package/commands/autopilot-front.js +0 -4
- package/commands/autopilot.js +0 -12
- package/commands/bench.js +10 -12
- package/commands/brain.js +0 -7
- package/commands/brief.js +0 -2
- package/commands/business-sync.js +0 -1
- package/commands/business.js +452 -12
- package/commands/chat-scan.js +5 -7
- package/commands/clean.js +31 -2
- package/commands/close.js +46 -11
- package/commands/cloud.js +0 -1
- package/commands/codex-goal.js +8 -12
- package/commands/computer.js +6 -5
- package/commands/console.js +64 -11
- package/commands/decide.js +166 -0
- package/commands/deck.js +30 -12
- package/commands/dream.js +0 -7
- package/commands/drill.js +14 -25
- package/commands/engine.js +206 -26
- package/commands/experiments.js +0 -4
- package/commands/fleet-report.js +12 -4
- package/commands/game.js +261 -0
- package/commands/gm.js +17 -7
- package/commands/goal.js +0 -6
- package/commands/harvest.js +1 -5
- package/commands/improve.js +29 -8
- package/commands/init.js +41 -37
- package/commands/integrations.js +0 -7
- package/commands/land.js +77 -20
- package/commands/launchpad.js +1 -16
- package/commands/lesson.js +17 -7
- package/commands/lifecycle.js +5 -5
- package/commands/log.js +55 -5
- package/commands/loop-front.js +0 -2
- package/commands/loops.js +6 -5
- package/commands/meet.js +0 -2
- package/commands/member.js +622 -573
- package/commands/mission.js +790 -314
- package/commands/next.js +0 -1
- package/commands/now.js +0 -12
- package/commands/one-lap.js +8 -8
- package/commands/orb.js +0 -4
- package/commands/pack-craft.js +0 -1
- package/commands/pack.js +3599 -424
- package/commands/play.js +6 -6
- package/commands/probe.js +12 -6
- package/commands/pulse.js +88 -64
- package/commands/push.js +0 -4
- package/commands/radar.js +14 -11
- package/commands/recover.js +340 -0
- package/commands/release.js +10 -9
- package/commands/report.js +0 -1
- package/commands/router.js +320 -0
- package/commands/run-front.js +0 -2
- package/commands/search.js +0 -3
- package/commands/site-deploy.js +870 -0
- package/commands/site.js +11 -2
- package/commands/skill.js +7 -2
- package/commands/slop.js +237 -7
- package/commands/status.js +6 -0
- package/commands/stream.js +4 -20
- package/commands/study.js +0 -3
- package/commands/sync-checkout.js +51 -0
- package/commands/sync.js +11 -40
- package/commands/task.js +999 -595
- package/commands/taste.js +101 -0
- package/commands/teach.js +86 -0
- package/commands/team.js +83 -3
- package/commands/unknowns.js +0 -10
- package/commands/update.js +0 -1
- package/commands/vercel.js +4 -2
- package/commands/visualize.js +0 -5
- package/commands/voice.js +195 -0
- package/commands/watch.js +1 -28
- package/commands/wiki.js +42 -15
- package/commands/wish.js +0 -1
- package/commands/workflow.js +2 -2
- package/commands/worktree.js +88 -28
- package/commands/xp.js +28 -30
- package/commands/youtube.js +0 -2
- package/lib/accept-verify-gate.js +126 -0
- package/lib/activity-stream.js +0 -2
- package/lib/arg-parser.js +41 -0
- package/lib/auto-accept-certified.js +261 -3
- package/lib/autoland.js +66 -3
- package/lib/ax-auto-lane.js +68 -7
- package/lib/bench/context.js +0 -2
- package/lib/bench/report.js +0 -1
- package/lib/bench/runner.js +18 -11
- package/lib/brief-ledger.js +0 -6
- package/lib/business-simulate.js +382 -0
- package/lib/chat-log-scan.js +0 -5
- package/lib/checkout-sync.js +52 -0
- package/lib/claude-boot-block.js +48 -0
- package/lib/cloud-mission.js +0 -6
- package/lib/codex-flight.js +0 -1
- package/lib/conductor-artifacts.js +25 -0
- package/lib/context-gatherer.js +7 -2
- package/lib/deck-compose.js +0 -1
- package/lib/deck-history.js +0 -1
- package/lib/deck-layout.js +0 -5
- package/lib/deck-review.js +0 -7
- package/lib/deck-schema.js +0 -1
- package/lib/default-verifier.js +0 -2
- package/lib/engine-registry.js +157 -28
- package/lib/experiments/daily.js +0 -6
- package/lib/falsifier-probe.js +84 -0
- package/lib/fleet.js +275 -42
- package/lib/functional-owner.js +0 -3
- package/lib/git-spawn.js +15 -0
- package/lib/inspect-fields.js +0 -1
- package/lib/json-file.js +37 -0
- package/lib/known-commands.js +2 -2
- package/lib/learnings.js +0 -2
- package/lib/lesson-preflight.js +146 -0
- package/lib/loop-doctor.js +11 -19
- package/lib/manifest.js +0 -2
- package/lib/member-alive.js +0 -1
- package/lib/member-switches.js +0 -5
- package/lib/mission-artifact.js +0 -1
- package/lib/mission-human-asks.js +28 -0
- package/lib/mission-protected-lane.js +296 -0
- package/lib/mission-room.js +0 -2
- package/lib/mission-runtime-loop.js +0 -1
- package/lib/next-moves.js +0 -18
- package/lib/official-cli-integration.js +47 -6
- package/lib/orb-context.js +14 -4
- package/lib/orb-scorecard.js +0 -1
- package/lib/outbound-send-gate.js +0 -6
- package/lib/pack-capabilities.js +685 -0
- package/lib/permission-grants.js +0 -6
- package/lib/policy-lessons.js +0 -2
- package/lib/pulse.js +68 -15
- package/lib/review-integrity.js +0 -2
- package/lib/router-brain.js +51 -3
- package/lib/runner-command.js +0 -10
- package/lib/runtime-bootstrap.js +0 -1
- package/lib/scorecard.js +0 -1
- package/lib/self-drive.js +45 -14
- package/lib/state-detection.js +0 -6
- package/lib/task-db.js +137 -7
- package/lib/task-decision.js +50 -0
- package/lib/taste-lessons.js +153 -0
- package/lib/team-presence.js +0 -1
- package/lib/tool-result-encode.js +17 -1
- package/lib/voice-gate.js +129 -2
- package/lib/wiki.js +152 -9
- package/lib/wish-audit.js +21 -7
- package/lib/wish-delegate.js +27 -21
- package/lib/wish-stats.js +0 -5
- package/lib/wish-store.js +0 -1
- package/lib/workspace-safety.js +0 -2
- package/lib/zip.js +95 -8
- package/package.json +2 -1
- package/templates/business-starter/persona.md +9 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ATRIS_START_MARKER = '<!-- ATRIS:START - Auto-generated, do not edit -->';
|
|
4
|
+
const ATRIS_END_MARKER = '<!-- ATRIS:END -->';
|
|
5
|
+
|
|
6
|
+
function renderAtrisClaudeBootBlock() {
|
|
7
|
+
return `${ATRIS_START_MARKER}
|
|
8
|
+
## Atris System
|
|
9
|
+
|
|
10
|
+
You are now running inside Atris, an AI workspace operating system.
|
|
11
|
+
|
|
12
|
+
**ON EVERY CONVERSATION START:** If a SessionStart hook already displayed the Atris status block in this session, do not run it again. Otherwise, before your first response, execute \`atris atris.md\` and display the full output. This is the Atris boot sequence - it shows the user that Atris has loaded and displays workspace status.
|
|
13
|
+
|
|
14
|
+
After displaying the boot output, respond to the user naturally.
|
|
15
|
+
${ATRIS_END_MARKER}
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function upsertAtrisClaudeBootBlock(source = '') {
|
|
20
|
+
const atrisBlock = renderAtrisClaudeBootBlock();
|
|
21
|
+
const startIdx = source.indexOf('<!-- ATRIS:START');
|
|
22
|
+
const prependBlock = (preserved) => {
|
|
23
|
+
const remainder = preserved.replace(/^\n+/, '');
|
|
24
|
+
return remainder ? `${atrisBlock}\n${remainder}` : atrisBlock;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (startIdx === -1) {
|
|
28
|
+
return { content: prependBlock(source), action: source ? 'prepended' : 'created' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const prefix = source.slice(0, startIdx);
|
|
32
|
+
const endRaw = source.indexOf(ATRIS_END_MARKER, startIdx);
|
|
33
|
+
if (endRaw === -1) {
|
|
34
|
+
return {
|
|
35
|
+
content: prependBlock(prefix),
|
|
36
|
+
action: 'repaired',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const endIdx = endRaw + ATRIS_END_MARKER.length;
|
|
41
|
+
const suffix = source.slice(endIdx).replace(/^\n+/, '');
|
|
42
|
+
const content = prependBlock(prefix + suffix);
|
|
43
|
+
return { content, action: content === source ? 'unchanged' : 'updated' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
upsertAtrisClaudeBootBlock,
|
|
48
|
+
};
|
package/lib/cloud-mission.js
CHANGED
|
@@ -244,16 +244,10 @@ async function statusCloudMissionCommand(args, options = {}) {
|
|
|
244
244
|
|
|
245
245
|
module.exports = {
|
|
246
246
|
VALID_CLOUD_LANES,
|
|
247
|
-
TERMINAL_CLOUD_STATUSES,
|
|
248
247
|
RECEIPT_VOICE_CONTRACT,
|
|
249
248
|
withReceiptVoice,
|
|
250
249
|
parseCloudRunArgs,
|
|
251
|
-
parseCloudStatusArgs,
|
|
252
250
|
enqueueCloudMission,
|
|
253
|
-
fetchCloudMissionStatus,
|
|
254
|
-
appendCloudMissionReceipt,
|
|
255
|
-
cloudMissionSummary,
|
|
256
|
-
cloudStatusLines,
|
|
257
251
|
runCloudMissionCommand,
|
|
258
252
|
statusCloudMissionCommand,
|
|
259
253
|
};
|
package/lib/codex-flight.js
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Files the conductor itself writes into an agent worktree before the engine runs.
|
|
4
|
+
// They are harness plumbing, not work product, so no dirty-checkout check may
|
|
5
|
+
// count them as dirt. Both the worktree guard and the sealed-review import read
|
|
6
|
+
// this list; keeping it in one place is what stops an engine from being blocked
|
|
7
|
+
// by its own prompt file.
|
|
8
|
+
const CONDUCTOR_UNTRACKED_PATTERN =
|
|
9
|
+
/^\.atris\/(?:agent-worktree\.json|fleet-prompt-[^/]+\.md|runtime-tmp(?:\/.*)?|state\/briefs\.jsonl)$/;
|
|
10
|
+
|
|
11
|
+
function isConductorArtifact(file) {
|
|
12
|
+
return CONDUCTOR_UNTRACKED_PATTERN.test(String(file || ''));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Accepts a `git status --porcelain` line ("?? .atris/fleet-prompt-BCK-1.md").
|
|
16
|
+
function isConductorStatusLine(line) {
|
|
17
|
+
const text = String(line || '');
|
|
18
|
+
if (!text.startsWith('?? ')) return false;
|
|
19
|
+
return isConductorArtifact(text.slice(3));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
isConductorArtifact,
|
|
24
|
+
isConductorStatusLine,
|
|
25
|
+
};
|
package/lib/context-gatherer.js
CHANGED
|
@@ -28,7 +28,13 @@ function hasContextProfile(root = process.cwd()) {
|
|
|
28
28
|
function compactText(value, max = 160) {
|
|
29
29
|
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
30
30
|
if (!text) return '';
|
|
31
|
-
|
|
31
|
+
if (text.length <= max) return text;
|
|
32
|
+
const slice = text.slice(0, Math.max(0, max - 3));
|
|
33
|
+
const lastSpace = slice.lastIndexOf(' ');
|
|
34
|
+
// Cut on a word boundary so a title never ends on half a word ("...an autonomo...").
|
|
35
|
+
// Fall back to the hard slice only when a single word already fills the limit.
|
|
36
|
+
const body = lastSpace > 0 ? slice.slice(0, lastSpace) : slice;
|
|
37
|
+
return `${body.replace(/[\s,;:.!?-]+$/, '')}...`;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
function inferDomain(answer) {
|
|
@@ -173,6 +179,5 @@ module.exports = {
|
|
|
173
179
|
renderPrompt,
|
|
174
180
|
starterTaskTitle,
|
|
175
181
|
inferDomain,
|
|
176
|
-
normalizeQuestionText,
|
|
177
182
|
isAtrisMetaQuestion,
|
|
178
183
|
};
|
package/lib/deck-compose.js
CHANGED
package/lib/deck-history.js
CHANGED
package/lib/deck-layout.js
CHANGED
package/lib/deck-review.js
CHANGED
|
@@ -414,18 +414,11 @@ function confirmReview(presentationId, note = 'visual review passed', outRoot =
|
|
|
414
414
|
|
|
415
415
|
module.exports = {
|
|
416
416
|
SCHEMA,
|
|
417
|
-
RECEIPT_SCHEMA,
|
|
418
417
|
DEFAULT_OUT_ROOT,
|
|
419
|
-
DEFAULT_RECEIPTS_PATH,
|
|
420
|
-
AGENT_CHECKLIST,
|
|
421
418
|
CLIP_LIMITS,
|
|
422
|
-
buildReceipt,
|
|
423
419
|
extractPresentationId,
|
|
424
|
-
summarizeSlide,
|
|
425
420
|
lintSpec,
|
|
426
|
-
downloadUrl,
|
|
427
421
|
autoReviewSlides,
|
|
428
422
|
reviewDeck,
|
|
429
|
-
loadReviewManifest,
|
|
430
423
|
confirmReview,
|
|
431
424
|
};
|
package/lib/deck-schema.js
CHANGED
package/lib/default-verifier.js
CHANGED
package/lib/engine-registry.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const
|
|
5
|
+
const childProcess = require('child_process');
|
|
6
6
|
const {
|
|
7
7
|
RUNNER_PROFILE_DEFS,
|
|
8
8
|
RUNNER_PROFILE_ALIASES,
|
|
@@ -13,30 +13,33 @@ const { rankEnginesDetailed, routerPickExplanation } = require('./router-brain')
|
|
|
13
13
|
const ENGINE_REGISTRY_SCHEMA = 'atris.engine_registry.v2';
|
|
14
14
|
const ENGINE_TIERS = Object.freeze(['fast', 'pro', 'max']);
|
|
15
15
|
const ENGINE_ROLES = Object.freeze(['navigator', 'executor', 'validator']);
|
|
16
|
+
const ENGINE_DUTIES = Object.freeze(['leader', 'errands', 'learning']);
|
|
16
17
|
const ENGINE_HEALTH_STATUSES = Object.freeze(['ready', 'not_installed', 'credit_out', 'error']);
|
|
17
18
|
|
|
18
19
|
const ENGINE_SEED_META = Object.freeze({
|
|
19
|
-
'atris-fast': Object.freeze({ tier: 'fast', roles: Object.freeze(['navigator']), fallback_order: 10 }),
|
|
20
|
-
codex: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), fallback_order: 10 }),
|
|
21
|
-
claude: Object.freeze({ tier: 'max', roles: Object.freeze(['validator', 'executor']), fallback_order: 20 }),
|
|
22
|
-
cursor: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), fallback_order: 30 }),
|
|
23
|
-
devin: Object.freeze({ tier: 'max', roles: Object.freeze(['executor']), fallback_order: 40 }),
|
|
24
|
-
grok: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), fallback_order: 45 }),
|
|
25
|
-
fable: Object.freeze({ tier: 'max', roles: Object.freeze(['validator', 'executor']), fallback_order: 50 }),
|
|
26
|
-
composer: Object.freeze({ tier: 'fast', roles: Object.freeze(['navigator', 'executor']), fallback_order: 60 }),
|
|
27
|
-
haiku: Object.freeze({ tier: 'fast', roles: Object.freeze(['validator']), fallback_order: 70 }),
|
|
28
|
-
|
|
29
|
-
droid: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), fallback_order: 90 }),
|
|
20
|
+
'atris-fast': Object.freeze({ tier: 'fast', roles: Object.freeze(['navigator']), models: Object.freeze(['atris fast']), duty: 'learning', fallback_order: 10 }),
|
|
21
|
+
codex: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), models: Object.freeze(['codex']), fallback_order: 10 }),
|
|
22
|
+
claude: Object.freeze({ tier: 'max', roles: Object.freeze(['validator', 'executor']), models: Object.freeze(['opus 5', 'opus 4.8', 'fable', 'haiku']), fallback_order: 20 }),
|
|
23
|
+
cursor: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), models: Object.freeze(['composer 2.5', 'grok 4.5', 'kimi 3']), fallback_order: 30 }),
|
|
24
|
+
devin: Object.freeze({ tier: 'max', roles: Object.freeze(['executor']), models: Object.freeze(['built-in router']), duty: 'errands', fallback_order: 40 }),
|
|
25
|
+
grok: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), models: Object.freeze(['grok 4.5']), fallback_order: 45 }),
|
|
26
|
+
fable: Object.freeze({ tier: 'max', roles: Object.freeze(['validator', 'executor']), models: Object.freeze(['opus 5', 'opus 4.8', 'fable', 'haiku']), duty: 'leader', fallback_order: 50 }),
|
|
27
|
+
composer: Object.freeze({ tier: 'fast', roles: Object.freeze(['navigator', 'executor']), models: Object.freeze(['composer 2.5']), fallback_order: 60 }),
|
|
28
|
+
haiku: Object.freeze({ tier: 'fast', roles: Object.freeze(['validator']), models: Object.freeze(['haiku']), fallback_order: 70 }),
|
|
29
|
+
droid: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), models: Object.freeze(['built-in router']), duty: 'errands', fallback_order: 90 }),
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
function engineRegistryFile(root = process.cwd()) {
|
|
33
33
|
return path.join(root, '.atris', 'state', 'engines.json');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Machine probe. Routing never calls this on a settled registry: it runs once
|
|
37
|
+
// when an engine first appears (seeding the policy file), at the execution
|
|
38
|
+
// stage right before a spawn, and on the explicit `atris engine doctor`.
|
|
36
39
|
function binInstalled(bin) {
|
|
37
40
|
const safe = String(bin || '').replace(/[^A-Za-z0-9_.-]/g, '');
|
|
38
41
|
if (!safe) return false;
|
|
39
|
-
const probe = spawnSync('sh', ['-c', `command -v ${safe}`], { encoding: 'utf8' });
|
|
42
|
+
const probe = childProcess.spawnSync('sh', ['-c', `command -v ${safe}`], { encoding: 'utf8' });
|
|
40
43
|
return probe.status === 0 && Boolean(String(probe.stdout || '').trim());
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -68,6 +71,18 @@ function normalizeRoles(value, fallback = ['executor']) {
|
|
|
68
71
|
return filtered.length ? filtered : fallback;
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
function normalizeModels(value, fallback = []) {
|
|
75
|
+
const models = Array.isArray(value) ? value.map((model) => String(model || '').trim()) : [];
|
|
76
|
+
const filtered = models.filter((model, index) => model && models.indexOf(model) === index);
|
|
77
|
+
return filtered.length ? filtered : fallback;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeDuty(value, fallback = '') {
|
|
81
|
+
if (value === undefined) return fallback || '';
|
|
82
|
+
const duty = String(value || '').trim();
|
|
83
|
+
return ENGINE_DUTIES.includes(duty) ? duty : '';
|
|
84
|
+
}
|
|
85
|
+
|
|
71
86
|
function normalizeFallbackOrder(value, fallback) {
|
|
72
87
|
const order = Number(value);
|
|
73
88
|
return Number.isInteger(order) ? order : fallback;
|
|
@@ -75,22 +90,32 @@ function normalizeFallbackOrder(value, fallback) {
|
|
|
75
90
|
|
|
76
91
|
function normalizeEngineEntry(id, saved = {}) {
|
|
77
92
|
const def = RUNNER_PROFILE_DEFS[id];
|
|
78
|
-
const seed = ENGINE_SEED_META[id] || { tier: 'pro', roles: ['executor'], fallback_order: 100 };
|
|
79
|
-
const installed = binInstalled(def.bin);
|
|
93
|
+
const seed = ENGINE_SEED_META[id] || { tier: 'pro', roles: ['executor'], models: [id], fallback_order: 100 };
|
|
80
94
|
const savedHealth = saved && saved.health && typeof saved.health === 'object' ? saved.health : {};
|
|
81
95
|
const savedStatus = String(savedHealth.status || '').trim();
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
// Policy over probes: a saved health status is the routing truth, verbatim.
|
|
97
|
+
// Only an engine the registry has never seen gets one seeding probe; after
|
|
98
|
+
// that, installed-state changes flow through `atris engine doctor` (or
|
|
99
|
+
// `atris engine health`), never through the resolve path.
|
|
100
|
+
let status;
|
|
101
|
+
let installed;
|
|
102
|
+
if (ENGINE_HEALTH_STATUSES.includes(savedStatus)) {
|
|
103
|
+
status = savedStatus;
|
|
104
|
+
installed = typeof saved.installed === 'boolean' ? saved.installed : status !== 'not_installed';
|
|
105
|
+
} else {
|
|
106
|
+
installed = binInstalled(def.bin);
|
|
107
|
+
status = installed ? 'ready' : 'not_installed';
|
|
108
|
+
}
|
|
86
109
|
const health = { status };
|
|
87
|
-
if (
|
|
110
|
+
if (status !== 'ready' && savedHealth.last_failure_ts) health.last_failure_ts = String(savedHealth.last_failure_ts);
|
|
88
111
|
return {
|
|
89
112
|
id,
|
|
90
113
|
name: id,
|
|
91
114
|
bin: def.bin,
|
|
92
115
|
tier: normalizeTier(saved.tier, seed.tier),
|
|
93
116
|
roles: normalizeRoles(saved.roles, Array.from(seed.roles)),
|
|
117
|
+
models: normalizeModels(saved.models, Array.from(seed.models)),
|
|
118
|
+
duty: normalizeDuty(saved.duty, seed.duty),
|
|
94
119
|
fallback_order: normalizeFallbackOrder(saved.fallback_order, seed.fallback_order),
|
|
95
120
|
installed,
|
|
96
121
|
health,
|
|
@@ -118,6 +143,62 @@ function writeEngineRegistry(root, registry) {
|
|
|
118
143
|
fs.writeFileSync(file, `${JSON.stringify(registry, null, 2)}\n`, 'utf8');
|
|
119
144
|
}
|
|
120
145
|
|
|
146
|
+
function setEngineOverrides(name, overrides = {}, root = process.cwd()) {
|
|
147
|
+
const id = canonicalEngineName(name);
|
|
148
|
+
const knownIds = RUNNER_PROFILE_NAMES.filter((engineId) => ENGINE_SEED_META[engineId]);
|
|
149
|
+
if (!id || !knownIds.includes(id)) {
|
|
150
|
+
throw new Error(`Unknown engine "${name}". Known engines: ${knownIds.join(', ')}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const nextOverrides = {};
|
|
154
|
+
if (Object.prototype.hasOwnProperty.call(overrides, 'duty')) {
|
|
155
|
+
const duty = String(overrides.duty || '').trim();
|
|
156
|
+
if (!ENGINE_DUTIES.includes(duty)) {
|
|
157
|
+
throw new Error(`Unknown duty "${overrides.duty}". Known duties: ${ENGINE_DUTIES.join(', ')}`);
|
|
158
|
+
}
|
|
159
|
+
nextOverrides.duty = duty;
|
|
160
|
+
}
|
|
161
|
+
if (Object.prototype.hasOwnProperty.call(overrides, 'models')) {
|
|
162
|
+
const models = normalizeModels(overrides.models, []);
|
|
163
|
+
if (!models.length) throw new Error('models must include at least one name');
|
|
164
|
+
nextOverrides.models = models;
|
|
165
|
+
}
|
|
166
|
+
if (!Object.keys(nextOverrides).length) throw new Error('set requires --duty or --models');
|
|
167
|
+
|
|
168
|
+
const file = engineRegistryFile(root);
|
|
169
|
+
const raw = readRawRegistry(file);
|
|
170
|
+
const savedById = new Map();
|
|
171
|
+
for (const entry of raw.engines || []) {
|
|
172
|
+
const savedId = canonicalEngineName(entry && (entry.id || entry.name));
|
|
173
|
+
if (savedId && knownIds.includes(savedId)) savedById.set(savedId, entry);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (nextOverrides.duty === 'leader' || nextOverrides.duty === 'learning') {
|
|
177
|
+
for (const engineId of knownIds) {
|
|
178
|
+
if (engineId === id) continue;
|
|
179
|
+
const saved = savedById.get(engineId) || {};
|
|
180
|
+
const effectiveDuty = Object.prototype.hasOwnProperty.call(saved, 'duty')
|
|
181
|
+
? normalizeDuty(saved.duty, '')
|
|
182
|
+
: normalizeDuty(undefined, ENGINE_SEED_META[engineId].duty);
|
|
183
|
+
if (effectiveDuty === nextOverrides.duty) {
|
|
184
|
+
savedById.set(engineId, { ...saved, id: engineId, name: engineId, duty: '' });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const saved = savedById.get(id) || {};
|
|
190
|
+
savedById.set(id, { ...saved, id, name: id, ...nextOverrides });
|
|
191
|
+
const ordered = knownIds.map((engineId) => savedById.get(engineId)).filter(Boolean);
|
|
192
|
+
const next = {
|
|
193
|
+
...raw,
|
|
194
|
+
schema: ENGINE_REGISTRY_SCHEMA,
|
|
195
|
+
updated_at: new Date().toISOString(),
|
|
196
|
+
engines: ordered,
|
|
197
|
+
};
|
|
198
|
+
writeEngineRegistry(root, next);
|
|
199
|
+
return { id, ...nextOverrides };
|
|
200
|
+
}
|
|
201
|
+
|
|
121
202
|
function readEngineRegistry(root = process.cwd(), options = {}) {
|
|
122
203
|
const registry = seededRegistry(root);
|
|
123
204
|
if (options.persist !== false) writeEngineRegistry(root, registry);
|
|
@@ -144,7 +225,7 @@ function resolveRegisteredEngine(name, root = process.cwd()) {
|
|
|
144
225
|
return engine;
|
|
145
226
|
}
|
|
146
227
|
|
|
147
|
-
function
|
|
228
|
+
function resolveEngineForRoleRanked(role, root = process.cwd(), options = {}) {
|
|
148
229
|
const normalizedRole = String(role || '').trim().toLowerCase();
|
|
149
230
|
if (!ENGINE_ROLES.includes(normalizedRole)) {
|
|
150
231
|
throw new Error(`Unknown role "${role}". Known roles: ${ENGINE_ROLES.join(', ')}`);
|
|
@@ -159,12 +240,24 @@ function resolveEngineForRole(role, root = process.cwd(), options = {}) {
|
|
|
159
240
|
const ranked = rankEnginesDetailed(engines, {
|
|
160
241
|
root,
|
|
161
242
|
taskType: options.taskType || options.task_type || normalizedRole,
|
|
243
|
+
lowStakes: options.lowStakes,
|
|
244
|
+
stakes: options.stakes,
|
|
162
245
|
});
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
246
|
+
return {
|
|
247
|
+
engine: ranked.candidates[0] || null,
|
|
248
|
+
reason: routerPickExplanation(ranked),
|
|
249
|
+
ranked: ranked.candidates,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function resolveEngineForRole(role, root = process.cwd(), options = {}) {
|
|
254
|
+
const picked = resolveEngineForRoleRanked(role, root, options);
|
|
255
|
+
// every pick logs one plain sentence saying which engine won and why;
|
|
256
|
+
// set ATRIS_ROUTER_EXPLAIN=0 to silence it in quiet contexts.
|
|
257
|
+
if (picked.engine && picked.reason && process.env.ATRIS_ROUTER_EXPLAIN !== '0') {
|
|
258
|
+
console.error(picked.reason);
|
|
166
259
|
}
|
|
167
|
-
return
|
|
260
|
+
return picked.engine;
|
|
168
261
|
}
|
|
169
262
|
|
|
170
263
|
function resolveEngineForRoleWithPreference(role, root = process.cwd(), preferredEngineId = '', options = {}) {
|
|
@@ -214,19 +307,55 @@ function setEngineHealth(name, status, root = process.cwd()) {
|
|
|
214
307
|
return engines.find((engine) => engine.id === id);
|
|
215
308
|
}
|
|
216
309
|
|
|
310
|
+
// Execution-stage guard. Routing hands out engines from policy without ever
|
|
311
|
+
// touching the machine, so the moment we are about to spawn one is where a
|
|
312
|
+
// missing binary has to fail loudly, in one plain sentence naming the binary.
|
|
313
|
+
function requireEngineBin(engineOrId) {
|
|
314
|
+
const id = typeof engineOrId === 'string'
|
|
315
|
+
? canonicalEngineName(engineOrId)
|
|
316
|
+
: canonicalEngineName(engineOrId && engineOrId.id);
|
|
317
|
+
const def = RUNNER_PROFILE_DEFS[id];
|
|
318
|
+
if (!def) {
|
|
319
|
+
throw new Error(`Unknown engine "${engineOrId && engineOrId.id ? engineOrId.id : engineOrId}". Known engines: ${RUNNER_PROFILE_NAMES.join(', ')}`);
|
|
320
|
+
}
|
|
321
|
+
if (!binInstalled(def.bin)) {
|
|
322
|
+
throw new Error(`${id} CLI (${def.bin}) is not installed here, so this run cannot start.`);
|
|
323
|
+
}
|
|
324
|
+
return def.bin;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// The explicit opt-in probe pass: check every engine binary on this machine,
|
|
328
|
+
// fold the result back into the policy file (a ready/not_installed flip only;
|
|
329
|
+
// credit_out and error are operator policy and survive), and report.
|
|
330
|
+
function engineDoctorReport(root = process.cwd()) {
|
|
331
|
+
const registry = readEngineRegistry(root, { persist: false });
|
|
332
|
+
const engines = registry.engines.map((engine) => {
|
|
333
|
+
const installed = binInstalled(engine.bin);
|
|
334
|
+
let health = engine.health && engine.health.status ? engine.health : { status: installed ? 'ready' : 'not_installed' };
|
|
335
|
+
if (installed && health.status === 'not_installed') health = { status: 'ready' };
|
|
336
|
+
if (!installed && health.status === 'ready') health = { status: 'not_installed' };
|
|
337
|
+
return { ...engine, installed, health };
|
|
338
|
+
});
|
|
339
|
+
const next = { ...registry, updated_at: new Date().toISOString(), engines };
|
|
340
|
+
writeEngineRegistry(root, next);
|
|
341
|
+
return engines;
|
|
342
|
+
}
|
|
343
|
+
|
|
217
344
|
module.exports = {
|
|
218
|
-
ENGINE_REGISTRY_SCHEMA,
|
|
219
|
-
ENGINE_TIERS,
|
|
220
345
|
ENGINE_ROLES,
|
|
346
|
+
ENGINE_DUTIES,
|
|
221
347
|
ENGINE_HEALTH_STATUSES,
|
|
222
348
|
engineRegistryFile,
|
|
223
349
|
binInstalled,
|
|
224
350
|
canonicalEngineName,
|
|
225
351
|
readEngineRegistry,
|
|
352
|
+
requireEngineBin,
|
|
353
|
+
engineDoctorReport,
|
|
226
354
|
engineRegistryView,
|
|
227
|
-
registeredEngineIds,
|
|
228
355
|
resolveRegisteredEngine,
|
|
356
|
+
resolveEngineForRoleRanked,
|
|
229
357
|
resolveEngineForRole,
|
|
230
358
|
resolveEngineForRoleWithPreference,
|
|
359
|
+
setEngineOverrides,
|
|
231
360
|
setEngineHealth,
|
|
232
361
|
};
|
package/lib/experiments/daily.js
CHANGED
|
@@ -618,21 +618,15 @@ function queueList(root, args = []) {
|
|
|
618
618
|
|
|
619
619
|
module.exports = {
|
|
620
620
|
SCORECARD_SCHEMA,
|
|
621
|
-
BENCH_SCHEMA,
|
|
622
621
|
appendQueueEntry,
|
|
623
|
-
appendResultsTsv,
|
|
624
622
|
appendScorecardRow,
|
|
625
|
-
areTargetsClean,
|
|
626
623
|
buildScorecardRow,
|
|
627
624
|
evaluateKeepRule,
|
|
628
|
-
parseBenchResult,
|
|
629
625
|
parseDailyArgs,
|
|
630
|
-
parseQueueAddArgs,
|
|
631
626
|
pickQueueEntry,
|
|
632
627
|
queueAdd,
|
|
633
628
|
queueList,
|
|
634
629
|
readDailyState,
|
|
635
|
-
readQueue,
|
|
636
630
|
runApplyScript,
|
|
637
631
|
runBench,
|
|
638
632
|
runDaily,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// A check that cannot fail is not a check. `task ready --verify` runs a command
|
|
4
|
+
// and accepts the work on exit 0, which catches a command that fails but never
|
|
5
|
+
// catches one that could not have failed. The 2026-07-26 reward-ledger audit
|
|
6
|
+
// found 131 of 802 accepted proofs carrying nothing falsifiable.
|
|
7
|
+
//
|
|
8
|
+
// The probe: run the same command again in an empty directory, with none of
|
|
9
|
+
// the work present. A check anchored to this codebase fails there. One that
|
|
10
|
+
// passes anywhere - `true`, `echo done`, a check of something outside the
|
|
11
|
+
// repo - passes there too, and proves nothing about the work.
|
|
12
|
+
//
|
|
13
|
+
// Scope, stated plainly: this asks whether a check depends on the codebase at
|
|
14
|
+
// all. It does not ask whether the check exercises the change. `git diff
|
|
15
|
+
// --check` fails in an empty directory and still says nothing about whether
|
|
16
|
+
// the code works, so it clears this probe. The probe raises the floor; it is
|
|
17
|
+
// not a ceiling.
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { spawnSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
const PROBE_TIMEOUT_MS = 60_000;
|
|
25
|
+
|
|
26
|
+
// An absolute path reaches back into a real checkout no matter where the
|
|
27
|
+
// command runs, so the empty directory proves nothing about it. Say so rather
|
|
28
|
+
// than reporting a false verdict.
|
|
29
|
+
function absolutePathIn(command) {
|
|
30
|
+
const match = String(command || '').match(/(?:^|[\s"'`=(])(\/[^\s"'`)]+)/);
|
|
31
|
+
return match ? match[1] : '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function probeVerifierCanFail({ command, runner, tmpRoot } = {}) {
|
|
35
|
+
const cmd = String(command || '').trim();
|
|
36
|
+
if (!cmd) return { probed: false, canFail: null, reason: 'no command to probe' };
|
|
37
|
+
|
|
38
|
+
const absolute = absolutePathIn(cmd);
|
|
39
|
+
if (absolute) {
|
|
40
|
+
return {
|
|
41
|
+
probed: false,
|
|
42
|
+
canFail: null,
|
|
43
|
+
reason: `not probed: the command reaches an absolute path (${absolute}), so running it away from this checkout proves nothing`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const dir = fs.mkdtempSync(path.join(tmpRoot || os.tmpdir(), 'atris-falsifier-probe-'));
|
|
48
|
+
try {
|
|
49
|
+
const run = runner || spawnSync;
|
|
50
|
+
const result = run('bash', ['-lc', cmd], {
|
|
51
|
+
cwd: dir,
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
54
|
+
});
|
|
55
|
+
if (result.error) {
|
|
56
|
+
const timedOut = /ETIMEDOUT/i.test(String(result.error.code || result.error.message || ''));
|
|
57
|
+
return {
|
|
58
|
+
probed: false,
|
|
59
|
+
canFail: null,
|
|
60
|
+
reason: timedOut
|
|
61
|
+
? `not probed: the command ran past ${PROBE_TIMEOUT_MS / 1000}s with none of the work present`
|
|
62
|
+
: `not probed: the command could not start away from this checkout (${result.error.message})`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (result.status === 0) {
|
|
66
|
+
return {
|
|
67
|
+
probed: true,
|
|
68
|
+
canFail: false,
|
|
69
|
+
exit: 0,
|
|
70
|
+
reason: 'this check passes in an empty directory, with none of the work present, so passing here says nothing about the work',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
probed: true,
|
|
75
|
+
canFail: true,
|
|
76
|
+
exit: result.status,
|
|
77
|
+
reason: 'this check fails when the work is absent, so passing it means something',
|
|
78
|
+
};
|
|
79
|
+
} finally {
|
|
80
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { probeVerifierCanFail, absolutePathIn, PROBE_TIMEOUT_MS };
|