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/lib/runner-command.js
CHANGED
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
// so autonomous runs are reproducible, while keeping explicit per-run/env knobs
|
|
9
9
|
// first in precedence. Precedence: explicit model -> ATRIS_RUNNER_MODEL env ->
|
|
10
10
|
// ATRIS_RUNNER_PROFILE -> legacy ATRIS_CLAUDE_MODEL env -> pinned default.
|
|
11
|
-
const DEFAULT_CLAUDE_RUNNER_MODEL = 'claude-opus-
|
|
11
|
+
const DEFAULT_CLAUDE_RUNNER_MODEL = 'claude-opus-5-5';
|
|
12
12
|
const DEFAULT_FABLE_RUNNER_MODEL = 'claude-fable-5';
|
|
13
13
|
const DEFAULT_CLAUDE_RUNNER_BIN = 'claude';
|
|
14
|
+
// Effort words a roster line may carry after the model. Each engine lists the
|
|
15
|
+
// ones its CLI takes; an engine with no list takes none.
|
|
16
|
+
const EFFORT_WORDS = Object.freeze(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
17
|
+
const CLAUDE_EFFORTS = EFFORT_WORDS;
|
|
14
18
|
|
|
15
19
|
// Canonical runner profiles. Each entry is the config an operator would pick
|
|
16
20
|
// on purpose. Aliases (spelling variants of the same profile) resolve to the
|
|
@@ -30,16 +34,24 @@ const RUNNER_PROFILE_DEFS = Object.freeze({
|
|
|
30
34
|
bin: 'claude',
|
|
31
35
|
model: '',
|
|
32
36
|
commandTemplate: '',
|
|
37
|
+
effortFlag: '--effort',
|
|
38
|
+
efforts: CLAUDE_EFFORTS,
|
|
33
39
|
}),
|
|
40
|
+
// No -m or effort unless the roster pins one: codex then reads its own
|
|
41
|
+
// ~/.codex/config.toml, which is what the roster view reports.
|
|
34
42
|
codex: Object.freeze({
|
|
35
43
|
bin: 'codex',
|
|
36
44
|
model: '',
|
|
37
|
-
commandTemplate: '{bin} exec {prompt}',
|
|
45
|
+
commandTemplate: '{bin} exec {pinnedModelFlag} {pinnedEffortFlag} {prompt}',
|
|
46
|
+
effortFlag: '-c model_reasoning_effort=',
|
|
47
|
+
efforts: Object.freeze(['low', 'medium', 'high', 'xhigh']),
|
|
38
48
|
}),
|
|
49
|
+
// cursor-agent takes effort only inside a bracketed model name some models
|
|
50
|
+
// accept, so a roster effort on cursor is refused rather than guessed.
|
|
39
51
|
cursor: Object.freeze({
|
|
40
52
|
bin: 'cursor-agent',
|
|
41
53
|
model: '',
|
|
42
|
-
commandTemplate: '{bin} --trust -p {prompt}',
|
|
54
|
+
commandTemplate: '{bin} --trust {pinnedModelFlag} -p {prompt}',
|
|
43
55
|
}),
|
|
44
56
|
// Fable is a distinct Claude Code model, not a friendly name for whatever
|
|
45
57
|
// Claude model happens to be selected locally.
|
|
@@ -47,6 +59,8 @@ const RUNNER_PROFILE_DEFS = Object.freeze({
|
|
|
47
59
|
bin: 'claude',
|
|
48
60
|
model: DEFAULT_FABLE_RUNNER_MODEL,
|
|
49
61
|
commandTemplate: '',
|
|
62
|
+
effortFlag: '--effort',
|
|
63
|
+
efforts: CLAUDE_EFFORTS,
|
|
50
64
|
}),
|
|
51
65
|
composer: Object.freeze({
|
|
52
66
|
bin: 'ax',
|
|
@@ -57,29 +71,46 @@ const RUNNER_PROFILE_DEFS = Object.freeze({
|
|
|
57
71
|
bin: 'claude',
|
|
58
72
|
model: 'claude-haiku-4-5',
|
|
59
73
|
commandTemplate: '',
|
|
74
|
+
effortFlag: '--effort',
|
|
75
|
+
efforts: CLAUDE_EFFORTS,
|
|
60
76
|
}),
|
|
77
|
+
// Every engine with a model flag takes --model only when one is pinned (a
|
|
78
|
+
// roster pick or a mission model); otherwise it rides its own CLI default.
|
|
79
|
+
// {pinnedModelFlag} and {pinnedEffortFlag} render to nothing when unpinned.
|
|
80
|
+
// devin has no effort flag, so a roster effort on devin is refused.
|
|
61
81
|
devin: Object.freeze({
|
|
62
82
|
bin: 'devin',
|
|
63
83
|
model: '',
|
|
64
|
-
commandTemplate: '{bin} -p -- {prompt}',
|
|
84
|
+
commandTemplate: '{bin} -p {pinnedModelFlag} -- {prompt}',
|
|
65
85
|
}),
|
|
86
|
+
// No hard pin: a pinned grok-4.6 went stale when the grok CLI moved its
|
|
87
|
+
// own default to grok-4.7-build-fast, so an unpinned run rides the CLI.
|
|
66
88
|
grok: Object.freeze({
|
|
67
89
|
bin: 'grok',
|
|
68
|
-
model: '
|
|
69
|
-
commandTemplate: '{bin} --always-approve -p {prompt}',
|
|
90
|
+
model: '',
|
|
91
|
+
commandTemplate: '{bin} --always-approve {pinnedModelFlag} {pinnedEffortFlag} -p {prompt}',
|
|
92
|
+
effortFlag: '--reasoning-effort',
|
|
93
|
+
efforts: Object.freeze(['low', 'medium', 'high']),
|
|
70
94
|
}),
|
|
71
95
|
agy: Object.freeze({
|
|
72
96
|
bin: 'agy',
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
97
|
+
// No hard pin: the agy CLI keeps its own session model, so an unpinned
|
|
98
|
+
// run rides that and a roster line names gemini 3.8 flash (the 2026-09-04
|
|
99
|
+
// bake-off pick) when it wants it. --add-dir or agy edits its own scratch
|
|
100
|
+
// folder; the headless note or gemini stops to ask "may I edit?" and
|
|
101
|
+
// exits 0 having changed nothing.
|
|
102
|
+
model: '',
|
|
103
|
+
commandTemplate: '{bin} --mode accept-edits --dangerously-skip-permissions --add-dir "$PWD" {pinnedModelFlag} {pinnedEffortFlag} -p "You are running headless with edit permission already granted. Apply changes directly and never ask for confirmation. "{prompt}',
|
|
104
|
+
effortFlag: '--effort',
|
|
105
|
+
efforts: Object.freeze(['low', 'medium', 'high']),
|
|
78
106
|
}),
|
|
107
|
+
// opencode names models provider/model; --variant is its effort knob.
|
|
79
108
|
opencode: Object.freeze({
|
|
80
109
|
bin: 'opencode',
|
|
81
110
|
model: '',
|
|
82
|
-
commandTemplate: '{bin} run {prompt}',
|
|
111
|
+
commandTemplate: '{bin} run {pinnedModelFlag} {pinnedEffortFlag} {prompt}',
|
|
112
|
+
effortFlag: '--variant',
|
|
113
|
+
efforts: Object.freeze(['low', 'medium', 'high', 'xhigh', 'max']),
|
|
83
114
|
}),
|
|
84
115
|
// Private engine: fully functional (routable, selectable, probeable) but
|
|
85
116
|
// kept out of every operator-facing list. For engines an operator wants to
|
|
@@ -171,6 +202,15 @@ function resolveClaudeRunnerModel(mission) {
|
|
|
171
202
|
return DEFAULT_CLAUDE_RUNNER_MODEL;
|
|
172
203
|
}
|
|
173
204
|
|
|
205
|
+
// The model chosen for this launch: an explicit model (a roster pick, a
|
|
206
|
+
// mission model) or the profile's own pin. ATRIS_RUNNER_MODEL is left out on
|
|
207
|
+
// purpose: it usually names a claude model, which devin or grok would reject.
|
|
208
|
+
// '' means nothing is pinned, so the engine's CLI picks its own default.
|
|
209
|
+
function resolvePinnedRunnerModel(mission) {
|
|
210
|
+
const explicit = mission && mission.model != null ? String(mission.model).trim() : '';
|
|
211
|
+
return explicit || runnerProfileValue('model');
|
|
212
|
+
}
|
|
213
|
+
|
|
174
214
|
function resolveClaudeRunnerBin() {
|
|
175
215
|
const env = firstConfiguredEnv(['ATRIS_RUNNER_BIN']);
|
|
176
216
|
if (env) return env;
|
|
@@ -206,8 +246,29 @@ function runnerAvailabilityFailureMessage(error) {
|
|
|
206
246
|
return `${runnerBin} CLI not found. Set ATRIS_RUNNER_BIN (or legacy ATRIS_CLAUDE_BIN), or install the configured runner first.`;
|
|
207
247
|
}
|
|
208
248
|
|
|
209
|
-
|
|
249
|
+
// The effort flag for the active profile, or '' when no effort is pinned.
|
|
250
|
+
// Throws a plain sentence when the engine cannot take that effort, so a bad
|
|
251
|
+
// pin never launches silently at the wrong effort.
|
|
252
|
+
function runnerEffortFlag(effort) {
|
|
253
|
+
const word = String(effort || '').trim().toLowerCase();
|
|
254
|
+
if (!word) return '';
|
|
255
|
+
const profile = resolveRunnerProfile();
|
|
256
|
+
const flag = profile ? profile.effortFlag : RUNNER_PROFILE_DEFS.claude.effortFlag;
|
|
257
|
+
const allowed = profile ? profile.efforts : RUNNER_PROFILE_DEFS.claude.efforts;
|
|
258
|
+
const name = resolveRunnerProfileName() || 'claude';
|
|
259
|
+
if (!flag || !allowed || !allowed.includes(word)) throw new Error(`${name} cannot take the effort "${word}"`);
|
|
260
|
+
return flag.endsWith('=') ? `${flag}${word}` : `${flag} ${word}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function renderRunnerCommandTemplate(template, { promptFile, allowedTools, model, pinnedModel = '', effort = '' }) {
|
|
210
264
|
const allowedToolsFlag = allowedTools ? `--allowedTools ${shellWord(allowedTools)}` : '';
|
|
265
|
+
// The pinned flags take their leading space with them, so an unpinned
|
|
266
|
+
// launch reads exactly like the template without the placeholders.
|
|
267
|
+
template = template.replace(/\s*\{pinnedModelFlag\}/g, pinnedModel ? ` --model ${shellWord(pinnedModel)}` : '');
|
|
268
|
+
// Checked even when the template has no slot, so an engine that cannot
|
|
269
|
+
// take effort refuses instead of running at its default.
|
|
270
|
+
const effortFlag = runnerEffortFlag(effort);
|
|
271
|
+
template = template.replace(/\s*\{pinnedEffortFlag\}/g, effortFlag ? ` ${effortFlag}` : '');
|
|
211
272
|
const promptFileWord = shellWord(promptFile);
|
|
212
273
|
const values = {
|
|
213
274
|
bin: shellWord(resolveClaudeRunnerBin()),
|
|
@@ -231,24 +292,42 @@ function renderRunnerCommandTemplate(template, { promptFile, allowedTools, model
|
|
|
231
292
|
// for GLM/OpenAI/other local runners. The old ATRIS_CLAUDE_* env vars remain
|
|
232
293
|
// aliases for existing installs. allowedTools is optional: some call sites
|
|
233
294
|
// (e.g. horizon proposal) run without a tool allowlist.
|
|
234
|
-
function buildRunnerCommand({ promptFile, allowedTools, model } = {}) {
|
|
295
|
+
function buildRunnerCommand({ promptFile, allowedTools, model, effort } = {}) {
|
|
235
296
|
if (!promptFile) {
|
|
236
297
|
throw new Error('buildRunnerCommand: promptFile is required');
|
|
237
298
|
}
|
|
238
299
|
const resolved = resolveClaudeRunnerModel({ model });
|
|
239
300
|
const template = resolveClaudeRunnerCommandTemplate();
|
|
240
301
|
if (template) {
|
|
241
|
-
return renderRunnerCommandTemplate(template, { promptFile, allowedTools, model: resolved });
|
|
302
|
+
return renderRunnerCommandTemplate(template, { promptFile, allowedTools, model: resolved, pinnedModel: resolvePinnedRunnerModel({ model }), effort });
|
|
242
303
|
}
|
|
243
304
|
const safePath = String(promptFile).replace(/'/g, "'\\''");
|
|
244
305
|
let cmd = `${shellWord(resolveClaudeRunnerBin())} -p "$(cat '${safePath}')" --model ${shellWord(resolved)}`;
|
|
306
|
+
const effortFlag = runnerEffortFlag(effort);
|
|
307
|
+
if (effortFlag) cmd += ` ${effortFlag}`;
|
|
245
308
|
if (allowedTools) {
|
|
246
309
|
cmd += ` --allowedTools ${shellWord(allowedTools)}`;
|
|
247
310
|
}
|
|
248
311
|
return cmd;
|
|
249
312
|
}
|
|
250
313
|
|
|
314
|
+
// Whether this engine's launch can carry a pinned model, and which effort
|
|
315
|
+
// words it takes. The roster checks both when it reads a line.
|
|
316
|
+
function engineTakesModel(engineId) {
|
|
317
|
+
const def = RUNNER_PROFILE_DEFS[RUNNER_PROFILE_ALIASES[engineId] || engineId];
|
|
318
|
+
if (!def) return false;
|
|
319
|
+
return !def.commandTemplate || def.commandTemplate.includes('{pinnedModelFlag}');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function engineEffortLevels(engineId) {
|
|
323
|
+
const def = RUNNER_PROFILE_DEFS[RUNNER_PROFILE_ALIASES[engineId] || engineId];
|
|
324
|
+
return def && def.efforts ? def.efforts : [];
|
|
325
|
+
}
|
|
326
|
+
|
|
251
327
|
module.exports = {
|
|
328
|
+
EFFORT_WORDS,
|
|
329
|
+
engineTakesModel,
|
|
330
|
+
engineEffortLevels,
|
|
252
331
|
DEFAULT_CLAUDE_RUNNER_MODEL,
|
|
253
332
|
DEFAULT_FABLE_RUNNER_MODEL,
|
|
254
333
|
DEFAULT_CLAUDE_RUNNER_BIN,
|
|
@@ -259,6 +338,7 @@ module.exports = {
|
|
|
259
338
|
HIDDEN_PROFILE_NAMES,
|
|
260
339
|
resolveRunnerProfile,
|
|
261
340
|
resolveClaudeRunnerModel,
|
|
341
|
+
resolvePinnedRunnerModel,
|
|
262
342
|
resolveClaudeRunnerBin,
|
|
263
343
|
resolveClaudeRunnerCommandTemplate,
|
|
264
344
|
buildRunnerAvailabilityCommand,
|
package/lib/scorecard.js
CHANGED
|
@@ -245,14 +245,14 @@ function writeScorecard(atrisDir, data) {
|
|
|
245
245
|
|
|
246
246
|
// Ensure scorecards.md exists
|
|
247
247
|
if (!fs.existsSync(scorecardsPath)) {
|
|
248
|
-
const template = `# scorecards.md
|
|
248
|
+
const template = `# scorecards.md: Endgame Results\n\n> Append-only. One line per closed endgame. Records outcome metrics from the horizon.\n\n---\n\n`;
|
|
249
249
|
fs.writeFileSync(scorecardsPath, template, 'utf8');
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
// Format: - **[date] slug
|
|
252
|
+
// Format: - **[date] slug** - shipped: X/Y - wall-clock: Nh - halt: Z% - reward: total - lessons: N
|
|
253
253
|
const haltPercent = Math.round(haltRatio * 100);
|
|
254
254
|
const wallClockStr = wallClockHours < 1 ? `${Math.round(wallClockHours * 60)}m` : `${wallClockHours.toFixed(1)}h`;
|
|
255
|
-
const line = `- **[${endDate}] ${slug}**
|
|
255
|
+
const line = `- **[${endDate}] ${slug}** - shipped: ${tasksShipped}/${tasksAttempted} - wall-clock: ${wallClockStr} - halt: ${haltPercent}% - reward: ${totalReward} - lessons: ${lessonsGenerated}\n`;
|
|
256
256
|
|
|
257
257
|
// Append to file
|
|
258
258
|
fs.appendFileSync(scorecardsPath, line, 'utf8');
|
|
@@ -302,7 +302,7 @@ function readScorecards(atrisDir) {
|
|
|
302
302
|
const scorecards = [];
|
|
303
303
|
|
|
304
304
|
for (const line of content.split('\n')) {
|
|
305
|
-
const match = line.match(/^- \*\*\[(.+?)\]\s+(.+?)\*\*\s
|
|
305
|
+
const match = line.match(/^- \*\*\[(.+?)\]\s+(.+?)\*\*\s*[-\u2014]\s*shipped:\s*(\d+)\/(\d+)\s*[-\u2014]\s*wall-clock:\s*(.+?)\s*[-\u2014]\s*halt:\s*(\d+)%\s*[-\u2014]\s*reward:\s*(-?\d+)\s*[-\u2014]\s*lessons:\s*(\d+)$/);
|
|
306
306
|
if (!match) continue;
|
|
307
307
|
|
|
308
308
|
const [, endDate, slug, shipped, attempted, wallClockStr, haltPercent, reward, lessons] = match;
|
package/lib/self-drive.js
CHANGED
|
@@ -272,7 +272,15 @@ function handleMissionBlocker({ mission, stopReason, workspaceRoot, appendEvent
|
|
|
272
272
|
agent: engine.id,
|
|
273
273
|
task: `mission-blocker-${String(ref).toLowerCase()}`,
|
|
274
274
|
});
|
|
275
|
-
deps.dispatchToEngine({
|
|
275
|
+
deps.dispatchToEngine({
|
|
276
|
+
task,
|
|
277
|
+
engine: engine.id,
|
|
278
|
+
worktreePath: worktree.path,
|
|
279
|
+
root: scopedRoot,
|
|
280
|
+
...(engine.roster_model ? { model: engine.roster_model } : {}),
|
|
281
|
+
...(engine.roster_effort ? { effort: engine.roster_effort } : {}),
|
|
282
|
+
...(engine.roster_max_seconds ? { maxSeconds: engine.roster_max_seconds } : {}),
|
|
283
|
+
});
|
|
276
284
|
projectTasks(deps.taskDb, db, scopedRoot);
|
|
277
285
|
appendEvent?.('mission_blocker_dispatched', {
|
|
278
286
|
task_id: ref,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Block the current thread for ms milliseconds without burning a core.
|
|
4
|
+
// Atomics.wait parks the thread; a Date.now() loop keeps it at full CPU.
|
|
5
|
+
function sleepSync(ms) {
|
|
6
|
+
const duration = Math.max(0, Number(ms) || 0);
|
|
7
|
+
if (!duration) return;
|
|
8
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, duration);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = { sleepSync };
|
package/lib/state-detection.js
CHANGED
|
@@ -271,6 +271,32 @@ function getTodayInboxItems(workspaceDir) {
|
|
|
271
271
|
/**
|
|
272
272
|
* Detect workspace state: fresh install, has inbox items, in-progress work, or blocked
|
|
273
273
|
*/
|
|
274
|
+
// Blocked means a task whose status is failed: the DB has no separate
|
|
275
|
+
// blocked status, doneTask logs a failed task as a "blocked" event, and the
|
|
276
|
+
// board renders failed tasks under "## Blocked". Once this workspace has rows
|
|
277
|
+
// in the task DB, the DB is the only source. The DB file is shared by every
|
|
278
|
+
// workspace on the machine, so a workspace with no rows there falls back to
|
|
279
|
+
// the Blocked section of its own TODO.md, never a word elsewhere in a title.
|
|
280
|
+
function hasBlockedTasks(atrisDir, todoFile, legacyTaskContextFile) {
|
|
281
|
+
try {
|
|
282
|
+
const taskDb = require('./task-db');
|
|
283
|
+
if (fs.existsSync(taskDb.getDbPath())) {
|
|
284
|
+
const db = taskDb.open();
|
|
285
|
+
const workspaceRoot = taskDb.workspaceRoot(path.dirname(path.resolve(atrisDir)));
|
|
286
|
+
if (taskDb.listTasks(db, { workspaceRoot, limit: 1 }).length) {
|
|
287
|
+
return taskDb.listTasks(db, { workspaceRoot, status: 'failed', limit: 1 }).length > 0;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} catch (e) {
|
|
291
|
+
if (process.env.ATRIS_DEBUG) console.error('[state-detection] db blocked read failed:', e.message);
|
|
292
|
+
}
|
|
293
|
+
const taskFilePath = fs.existsSync(todoFile)
|
|
294
|
+
? todoFile
|
|
295
|
+
: (fs.existsSync(legacyTaskContextFile) ? legacyTaskContextFile : null);
|
|
296
|
+
if (!taskFilePath) return false;
|
|
297
|
+
return getTasksFromTodoSection(fs.readFileSync(taskFilePath, 'utf8'), 'Blocked').length > 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
274
300
|
function detectWorkspaceState(workspaceDir) {
|
|
275
301
|
const atrisDir = path.join(workspaceDir, 'atris');
|
|
276
302
|
const featuresDir = path.join(atrisDir, 'features');
|
|
@@ -317,15 +343,8 @@ function detectWorkspaceState(workspaceDir) {
|
|
|
317
343
|
}
|
|
318
344
|
}
|
|
319
345
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
? todoFile
|
|
323
|
-
: (fs.existsSync(legacyTaskContextFile) ? legacyTaskContextFile : null);
|
|
324
|
-
if (taskFilePath) {
|
|
325
|
-
const content = fs.readFileSync(taskFilePath, 'utf8');
|
|
326
|
-
if (content.includes('Risk Level: High') || /\bblocked\b/i.test(content)) {
|
|
327
|
-
return { state: 'blocked', reason: 'Tasks marked as blocked or high risk' };
|
|
328
|
-
}
|
|
346
|
+
if (hasBlockedTasks(atrisDir, todoFile, legacyTaskContextFile)) {
|
|
347
|
+
return { state: 'blocked', reason: 'Tasks marked as blocked' };
|
|
329
348
|
}
|
|
330
349
|
|
|
331
350
|
// Default: setup complete but no active work
|
package/lib/task-db.js
CHANGED
|
@@ -34,6 +34,7 @@ const { isDecisionTask } = require('./task-decision');
|
|
|
34
34
|
const { parseVerifyCommand } = require('./auto-accept-certified');
|
|
35
35
|
const { taskExplanation } = require('./task-explanation');
|
|
36
36
|
const { treeHashFor } = require('./tree-hash');
|
|
37
|
+
const { sleepSync } = require('./sleep-sync');
|
|
37
38
|
|
|
38
39
|
const DEFAULT_DB_PATH = path.join(os.homedir(), '.atris', 'tasks.db');
|
|
39
40
|
const TASK_EPISODES_FILE = path.join('.atris', 'state', 'task_episodes.jsonl');
|
|
@@ -2224,8 +2225,7 @@ function withBusyRetry(fn, attempts = 8) {
|
|
|
2224
2225
|
const busy = /SQLITE_BUSY|database is locked/i.test(msg) || code === 'SQLITE_BUSY' || code === 5;
|
|
2225
2226
|
if (!busy) throw e;
|
|
2226
2227
|
// Sleep synchronously, node:sqlite is sync; matches the rest of the API
|
|
2227
|
-
|
|
2228
|
-
while (Date.now() < end) {} // tight loop is fine, delay is small
|
|
2228
|
+
sleepSync(delay + Math.floor(Math.random() * delay));
|
|
2229
2229
|
delay = Math.min(delay * 2, 500);
|
|
2230
2230
|
}
|
|
2231
2231
|
}
|
|
@@ -2234,6 +2234,7 @@ function withBusyRetry(fn, attempts = 8) {
|
|
|
2234
2234
|
|
|
2235
2235
|
module.exports = {
|
|
2236
2236
|
open,
|
|
2237
|
+
withBusyRetry,
|
|
2237
2238
|
close,
|
|
2238
2239
|
getDbPath,
|
|
2239
2240
|
workspaceRoot,
|
package/lib/todo-fallback.js
CHANGED
|
@@ -34,7 +34,7 @@ function cleanTaskTitle(text) {
|
|
|
34
34
|
const withoutTags = raw.replace(/\s*\[[\w-]+\]/g, '').trim();
|
|
35
35
|
const bold = withoutTags.match(/^\*\*(.+?)\*\*\s*(?:[\u2014-]\s*)?(.*)$/);
|
|
36
36
|
if (!bold) return withoutTags;
|
|
37
|
-
return [bold[1], bold[2]].filter(Boolean).join(' \
|
|
37
|
+
return [bold[1], bold[2]].filter(Boolean).join(' - ').replace(/\s*\u2014\s*/g, ' - ').trim();
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function parseSection(content, sectionName) {
|
package/lib/wish-audit.js
CHANGED
|
@@ -407,9 +407,9 @@ function inferBudgetTier(text) {
|
|
|
407
407
|
return tier || 'long';
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
-
function resolveRole(role, root) {
|
|
410
|
+
function resolveRole(role, root, options = {}) {
|
|
411
411
|
const resolver = engineRegistry.resolveRole || engineRegistry.resolve || engineRegistry.resolveEngineForRole;
|
|
412
|
-
return resolver(role, root);
|
|
412
|
+
return resolver(role, root, options);
|
|
413
413
|
}
|
|
414
414
|
|
|
415
415
|
// Route-time validator policy: the routing decision must not depend on which
|
|
@@ -430,12 +430,15 @@ function policyValidator(root, excludeId = '') {
|
|
|
430
430
|
}
|
|
431
431
|
}
|
|
432
432
|
|
|
433
|
-
|
|
433
|
+
// A wish already sized quick (a small fix, tweak, or patch) asks the roster
|
|
434
|
+
// for the owner's small build pick first; with none set, it routes as any
|
|
435
|
+
// build. The router's own ranking is unchanged either way.
|
|
436
|
+
function resolveExecutor(root, engineOverride = '', options = {}) {
|
|
434
437
|
if (engineOverride) {
|
|
435
|
-
return engineRegistry.resolveEngineForRoleWithPreference('executor', root, engineOverride);
|
|
438
|
+
return engineRegistry.resolveEngineForRoleWithPreference('executor', root, engineOverride, options);
|
|
436
439
|
}
|
|
437
440
|
return {
|
|
438
|
-
engine: resolveRole('executor', root),
|
|
441
|
+
engine: resolveRole('executor', root, options),
|
|
439
442
|
requested_engine: null,
|
|
440
443
|
engine_fallback_reason: null,
|
|
441
444
|
};
|
|
@@ -1034,7 +1037,8 @@ function auditWish(text, root = process.cwd(), options = {}) {
|
|
|
1034
1037
|
let requestedEngine = null;
|
|
1035
1038
|
let engineFallbackReason = null;
|
|
1036
1039
|
try {
|
|
1037
|
-
const
|
|
1040
|
+
const small = inferBudgetTier(text) === 'quick' ? { job: engineRegistry.SMALL_BUILD_JOB } : {};
|
|
1041
|
+
const resolved = resolveExecutor(root, engineOverride, small);
|
|
1038
1042
|
executor = resolved.engine;
|
|
1039
1043
|
requestedEngine = resolved.requested_engine;
|
|
1040
1044
|
engineFallbackReason = resolved.engine_fallback_reason;
|
package/lib/wish-delegate.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { BACKEND_ROOT_HINT, resolveBackendRoot } = require('../utils/backend-root');
|
|
5
6
|
const { getLogPath, ensureLogDirectory, createLogFile, addInboxIdea } = require('./file-ops');
|
|
6
7
|
const { resolveFunctionalOwner } = require('./functional-owner');
|
|
7
8
|
const { buildMissionRoom, writeMissionRoomReceipt } = require('./mission-room');
|
|
@@ -55,7 +56,6 @@ const {
|
|
|
55
56
|
} = require('./wish-audit');
|
|
56
57
|
|
|
57
58
|
const METRIC_OPS = { '>=': '--gte', '<=': '--lte', '==': '--eq', '>': '--gt', '<': '--lt' };
|
|
58
|
-
const METRIC_VERIFY_ROOT = '/Users/keshavrao/arena/atrisos-backend';
|
|
59
59
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
60
60
|
const HOUR_MS = 60 * 60 * 1000;
|
|
61
61
|
const IMPLICIT_ANSWER_STALE_MS = HOUR_MS;
|
|
@@ -105,8 +105,11 @@ function parseMetricExpression(text) {
|
|
|
105
105
|
|
|
106
106
|
// Wire to backend/scripts/metric_verify.py: exits 0 when the metric meets
|
|
107
107
|
// target and prints a JSON line with value + proof_uri.
|
|
108
|
-
function metricVerifierCommand(parsed) {
|
|
109
|
-
|
|
108
|
+
function metricVerifierCommand(parsed, workspace) {
|
|
109
|
+
const backend = resolveBackendRoot(workspace);
|
|
110
|
+
if (!backend) throw new Error(BACKEND_ROOT_HINT);
|
|
111
|
+
const quoted = `'${backend.replace(/'/g, `'\\''`)}'`;
|
|
112
|
+
return `cd ${quoted} && venv/bin/python backend/scripts/metric_verify.py ${parsed.metric} ${parsed.flag} ${parsed.target}`;
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
const MISSION_TERMINAL_STATUSES = new Set(['complete', 'stopped']);
|
|
@@ -489,7 +492,9 @@ function scorecardText(value) {
|
|
|
489
492
|
}
|
|
490
493
|
|
|
491
494
|
function readWishReviewRewardRows(root = process.cwd()) {
|
|
492
|
-
const
|
|
495
|
+
const backend = resolveBackendRoot(root);
|
|
496
|
+
if (!backend) return [];
|
|
497
|
+
const file = path.join(backend, '.atris', 'state', 'scorecards.jsonl');
|
|
493
498
|
return readJsonLines(file).filter((row) => {
|
|
494
499
|
const haystack = `${scorecardText(row.source)} ${scorecardText(row.feedback)}`;
|
|
495
500
|
return /wish_review/i.test(haystack);
|
|
@@ -514,6 +519,7 @@ function reviewLine(review) {
|
|
|
514
519
|
|
|
515
520
|
function printRewards(root = process.cwd()) {
|
|
516
521
|
const reviews = readWishEvents(root).filter((event) => event && event.kind === 'review');
|
|
522
|
+
if (!resolveBackendRoot(root)) console.error(BACKEND_ROOT_HINT);
|
|
517
523
|
const rewardRows = readWishReviewRewardRows(root);
|
|
518
524
|
console.log(`reviews count: ${reviews.length}`);
|
|
519
525
|
console.log(`avg score: ${averageReviewScore(reviews)}`);
|
|
@@ -749,7 +755,7 @@ function startWishDelegation(wish, audit, root, options = {}) {
|
|
|
749
755
|
const suppliedVerifyHasCommand = !!(suppliedVerifyPlan && suppliedVerifyPlan.command);
|
|
750
756
|
const userVerifyProvided = options.userVerifyProvided === true || options.verifyPlanExplicit === true;
|
|
751
757
|
let verifyPlan = metricPlan
|
|
752
|
-
? { command: metricVerifierCommand(metricPlan), outcome: `metric ${metricPlan.expression} is met`, status: 'metric' }
|
|
758
|
+
? { command: metricVerifierCommand(metricPlan, root), outcome: `metric ${metricPlan.expression} is met`, status: 'metric' }
|
|
753
759
|
: (suppliedVerifyPlan || deriveVerifyPlan(taskText));
|
|
754
760
|
if (!metricPlan && frontendWish && designContext.auditCommand && !userVerifyProvided && !suppliedVerifyHasCommand && !(verifyPlan && verifyPlan.command)) {
|
|
755
761
|
verifyPlan = designVerifyPlan(designContext);
|
|
@@ -757,6 +763,13 @@ function startWishDelegation(wish, audit, root, options = {}) {
|
|
|
757
763
|
const missionVerifier = verifyPlan.command || resolveDefaultVerifier(root);
|
|
758
764
|
const verifyWarning = verifierBudgetWarning(missionVerifier);
|
|
759
765
|
const engine = wishDelegationEngine(audit, options);
|
|
766
|
+
// The build roster can pin a model for this engine; carry it onto the
|
|
767
|
+
// mission so the runner does not fall back to the engine default.
|
|
768
|
+
// Its effort and time cap ride along the same way.
|
|
769
|
+
const rosterPicked = Boolean(audit.executor && audit.executor.id === engine);
|
|
770
|
+
const rosterModel = audit.executor && audit.executor.id === engine && audit.executor.roster_model
|
|
771
|
+
? audit.executor.roster_model
|
|
772
|
+
: '';
|
|
760
773
|
const now = stampIso();
|
|
761
774
|
const ownerResolution = resolveFunctionalOwner({
|
|
762
775
|
requestedOwner: audit.executor.id,
|
|
@@ -893,6 +906,9 @@ function startWishDelegation(wish, audit, root, options = {}) {
|
|
|
893
906
|
mission_room_receipt_path: writtenRoom.relativePath,
|
|
894
907
|
mission_room_name: writtenRoom.room?.name || null,
|
|
895
908
|
source: 'wish',
|
|
909
|
+
...(rosterModel ? { model: rosterModel } : {}),
|
|
910
|
+
...(rosterPicked && audit.executor.roster_effort ? { roster_effort: audit.executor.roster_effort } : {}),
|
|
911
|
+
...(rosterPicked && audit.executor.roster_max_seconds ? { roster_max_seconds: audit.executor.roster_max_seconds } : {}),
|
|
896
912
|
...(verifyWarning ? { verifier_budget_warning: verifyWarning } : {}),
|
|
897
913
|
...(designBrief ? {
|
|
898
914
|
design_brief_paths: designBrief.paths,
|
|
@@ -1529,6 +1545,10 @@ function runCapturedWish(text, root = process.cwd(), options = {}) {
|
|
|
1529
1545
|
const asJson = !!options.asJson;
|
|
1530
1546
|
const noMission = !!options.noMission;
|
|
1531
1547
|
const builderMode = String(options.asMode || '').trim() === 'builder';
|
|
1548
|
+
if (options.metric?.ok && !resolveBackendRoot(root)) {
|
|
1549
|
+
console.error(BACKEND_ROOT_HINT);
|
|
1550
|
+
return 2;
|
|
1551
|
+
}
|
|
1532
1552
|
let engineOverride = '';
|
|
1533
1553
|
try {
|
|
1534
1554
|
engineOverride = validateEngineOverride(options.engineOverride, root);
|
|
@@ -1617,6 +1637,10 @@ function runAgainWish(positionals, root = process.cwd(), options = {}) {
|
|
|
1617
1637
|
function applyAnswer(wish, answer, root, options = {}) {
|
|
1618
1638
|
const asJson = !!options.asJson;
|
|
1619
1639
|
const noMission = !!options.noMission;
|
|
1640
|
+
if (wish.metric && !resolveBackendRoot(root)) {
|
|
1641
|
+
console.error(BACKEND_ROOT_HINT);
|
|
1642
|
+
return 2;
|
|
1643
|
+
}
|
|
1620
1644
|
let engineOverride = '';
|
|
1621
1645
|
try {
|
|
1622
1646
|
engineOverride = validateEngineOverride(options.engineOverride, root);
|
package/mcp/atris-mcp/index.mjs
CHANGED
|
@@ -4,11 +4,9 @@
|
|
|
4
4
|
// Auth resolves the same way as the CLI: ATRIS_API_KEY, then
|
|
5
5
|
// the logged-in atris token, then ~/.atris/design-api-key.
|
|
6
6
|
|
|
7
|
-
import { realpathSync } from 'node:fs';
|
|
7
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
8
|
+
import { createInterface } from 'node:readline';
|
|
8
9
|
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
10
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
12
10
|
import designApi from '../../lib/design-api.js';
|
|
13
11
|
|
|
14
12
|
const { resolveDesignKey, designRequest, pollDesignJob, billingOf } = designApi;
|
|
@@ -141,34 +139,89 @@ export async function handleTool(name, args = {}, key) {
|
|
|
141
139
|
return fail(`unknown tool: ${name}`);
|
|
142
140
|
}
|
|
143
141
|
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
);
|
|
142
|
+
async function serveStdio() {
|
|
143
|
+
const { name, version } = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
144
|
+
const knownVersions = new Set(['2024-11-05', '2025-03-26', '2025-06-18', '2025-11-25']);
|
|
145
|
+
const send = (message, onWritten) => process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', ...message })}\n`, onWritten);
|
|
146
|
+
const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
147
|
+
const inFlight = new Set();
|
|
148
|
+
const pendingById = new Map();
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const key = resolveDesignKey();
|
|
154
|
-
if (!key) {
|
|
155
|
-
return fail('no api key found. set ATRIS_API_KEY or run: atris login (or save a key in ~/.atris/design-api-key)');
|
|
156
|
-
}
|
|
150
|
+
for await (const line of input) {
|
|
151
|
+
if (!line.trim()) continue;
|
|
152
|
+
let request;
|
|
157
153
|
try {
|
|
158
|
-
|
|
159
|
-
} catch
|
|
160
|
-
|
|
154
|
+
request = JSON.parse(line);
|
|
155
|
+
} catch {
|
|
156
|
+
send({ id: null, error: { code: -32700, message: 'parse error' } });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!request || typeof request !== 'object' || Array.isArray(request)
|
|
160
|
+
|| request.jsonrpc !== '2.0' || typeof request.method !== 'string') {
|
|
161
|
+
send({ id: request?.id ?? null, error: { code: -32600, message: 'invalid request' } });
|
|
162
|
+
continue;
|
|
161
163
|
}
|
|
162
|
-
|
|
164
|
+
if (request.method === 'notifications/cancelled') {
|
|
165
|
+
const pending = pendingById.get(request.params?.requestId);
|
|
166
|
+
if (pending) pending.cancelled = true;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!Object.hasOwn(request, 'id')) continue;
|
|
163
170
|
|
|
164
|
-
|
|
171
|
+
const id = request.id;
|
|
172
|
+
let result;
|
|
173
|
+
if (request.method === 'initialize') {
|
|
174
|
+
result = {
|
|
175
|
+
protocolVersion: knownVersions.has(request.params?.protocolVersion)
|
|
176
|
+
? request.params.protocolVersion : '2025-06-18',
|
|
177
|
+
capabilities: { tools: {} },
|
|
178
|
+
serverInfo: { name, version },
|
|
179
|
+
};
|
|
180
|
+
} else if (request.method === 'ping') {
|
|
181
|
+
result = {};
|
|
182
|
+
} else if (request.method === 'tools/list') {
|
|
183
|
+
result = { tools: TOOLS };
|
|
184
|
+
} else if (request.method === 'tools/call') {
|
|
185
|
+
const pending = { cancelled: false };
|
|
186
|
+
const work = (async () => {
|
|
187
|
+
try {
|
|
188
|
+
const key = resolveDesignKey();
|
|
189
|
+
result = key
|
|
190
|
+
? await handleTool(request.params?.name, request.params?.arguments || {}, key)
|
|
191
|
+
: fail('no api key found. set ATRIS_API_KEY or run: atris login (or save a key in ~/.atris/design-api-key)');
|
|
192
|
+
} catch (error) {
|
|
193
|
+
result = fail(`design call failed: ${(error && error.message) || error}`);
|
|
194
|
+
}
|
|
195
|
+
if (!pending.cancelled) {
|
|
196
|
+
await new Promise((resolve, reject) => {
|
|
197
|
+
send({ id, result }, (error) => (error ? reject(error) : resolve()));
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
})();
|
|
201
|
+
inFlight.add(work);
|
|
202
|
+
pendingById.set(id, pending);
|
|
203
|
+
const done = () => {
|
|
204
|
+
inFlight.delete(work);
|
|
205
|
+
if (pendingById.get(id) === pending) pendingById.delete(id);
|
|
206
|
+
};
|
|
207
|
+
work.then(done, (error) => {
|
|
208
|
+
done();
|
|
209
|
+
process.stderr.write(`mcp response write failed: ${(error && error.message) || error}\n`);
|
|
210
|
+
process.exitCode = 1;
|
|
211
|
+
});
|
|
212
|
+
continue;
|
|
213
|
+
} else {
|
|
214
|
+
send({ id, error: { code: -32601, message: 'method not found' } });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
send({ id, result });
|
|
218
|
+
}
|
|
219
|
+
await Promise.allSettled(inFlight);
|
|
165
220
|
}
|
|
166
221
|
|
|
167
222
|
// Auto-start only when run directly (`atris mcp`, `npx atris-mcp`,
|
|
168
223
|
// node index.mjs); realpathSync resolves the npm bin symlink.
|
|
169
224
|
const invokedPath = process.argv[1] ? realpathSync(process.argv[1]) : '';
|
|
170
225
|
if (invokedPath && invokedPath === fileURLToPath(import.meta.url)) {
|
|
171
|
-
|
|
172
|
-
const transport = new StdioServerTransport();
|
|
173
|
-
await server.connect(transport);
|
|
226
|
+
await serveStdio();
|
|
174
227
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atris",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.59.0",
|
|
4
4
|
"description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
|
|
5
5
|
"main": "bin/atris.js",
|
|
6
6
|
"bin": {
|
|
@@ -101,8 +101,5 @@
|
|
|
101
101
|
"bugs": {
|
|
102
102
|
"url": "https://github.com/atrislabs/atris/issues"
|
|
103
103
|
},
|
|
104
|
-
"homepage": "https://github.com/atrislabs/atris#readme"
|
|
105
|
-
"dependencies": {
|
|
106
|
-
"@modelcontextprotocol/sdk": "^1.30.0"
|
|
107
|
-
}
|
|
104
|
+
"homepage": "https://github.com/atrislabs/atris#readme"
|
|
108
105
|
}
|