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.
Files changed (95) hide show
  1. package/AGENTS.md +3 -2
  2. package/GETTING_STARTED.md +2 -2
  3. package/PERSONA.md +11 -11
  4. package/README.md +1 -1
  5. package/atris/GEMINI.md +7 -6
  6. package/atris/PERSONA.md +4 -4
  7. package/atris/features/README.md +77 -6
  8. package/atris/skills/README.md +44 -15
  9. package/atris/skills/aeo/SKILL.md +10 -10
  10. package/atris/skills/atris/SKILL.md +1 -1
  11. package/atris/skills/engines/SKILL.md +69 -50
  12. package/atris/skills/meta/SKILL.md +4 -4
  13. package/bin/atris.js +8 -6
  14. package/commands/aeo.js +9 -12
  15. package/commands/analytics.js +8 -11
  16. package/commands/autopilot.js +79 -8
  17. package/commands/avail.js +1 -1
  18. package/commands/brain.js +3 -1
  19. package/commands/business.js +13 -8
  20. package/commands/clean.js +17 -0
  21. package/commands/close.js +2 -1
  22. package/commands/computer.js +15 -10
  23. package/commands/context-sync.js +3 -3
  24. package/commands/doc-health.js +55 -2
  25. package/commands/engine.js +384 -4
  26. package/commands/experiments.js +13 -4
  27. package/commands/host.js +53 -0
  28. package/commands/improve.js +1 -1
  29. package/commands/integrations.js +2 -5
  30. package/commands/learn.js +1 -1
  31. package/commands/member.js +231 -53
  32. package/commands/mission.js +129 -19
  33. package/commands/one-lap.js +58 -3
  34. package/commands/pack.js +2 -2
  35. package/commands/probe.js +11 -5
  36. package/commands/pull.js +1 -1
  37. package/commands/push.js +1 -1
  38. package/commands/rainmaker.js +3 -16
  39. package/commands/run.js +4 -1
  40. package/commands/scout.js +1 -1
  41. package/commands/search.js +5 -3
  42. package/commands/spaceship.js +1 -1
  43. package/commands/stream.js +27 -2
  44. package/commands/study.js +15 -9
  45. package/commands/sync.js +1 -1
  46. package/commands/task.js +13 -2
  47. package/commands/unknowns.js +5 -0
  48. package/commands/verify.js +12 -4
  49. package/commands/visualize.js +20 -3
  50. package/commands/workflow.js +23 -7
  51. package/lib/codex-flight.js +28 -5
  52. package/lib/dispatch-scout.js +31 -22
  53. package/lib/engine-registry.js +1482 -13
  54. package/lib/feedback-triage.js +2 -0
  55. package/lib/fleet.js +111 -12
  56. package/lib/host.js +336 -0
  57. package/lib/improve-vitals-html.js +3 -2
  58. package/lib/known-commands.js +1 -1
  59. package/lib/lesson-contradiction.js +2 -2
  60. package/lib/map-refs.js +226 -0
  61. package/lib/member-engine.js +253 -0
  62. package/lib/mission-room.js +4 -4
  63. package/lib/noninteractive.js +15 -4
  64. package/lib/policy-lessons.js +1 -1
  65. package/lib/receipt-block.js +137 -28
  66. package/lib/roster-markdown.js +206 -0
  67. package/lib/roster-models.js +179 -0
  68. package/lib/runner-command.js +95 -15
  69. package/lib/scorecard.js +4 -4
  70. package/lib/self-drive.js +9 -1
  71. package/lib/sleep-sync.js +11 -0
  72. package/lib/state-detection.js +28 -9
  73. package/lib/task-db.js +3 -2
  74. package/lib/todo-fallback.js +1 -1
  75. package/lib/wish-audit.js +10 -6
  76. package/lib/wish-delegate.js +29 -5
  77. package/mcp/atris-mcp/index.mjs +77 -24
  78. package/package.json +2 -5
  79. package/scripts/det/det.js +2 -2
  80. package/scripts/det/pr-description.js +2 -2
  81. package/scripts/det/test.js +2 -2
  82. package/templates/members/host/MEMBER.md +23 -0
  83. package/templates/members/host/MISSION.md +11 -0
  84. package/templates/members/host/SOUL.md +7 -0
  85. package/templates/members/host/context/why.md +3 -0
  86. package/templates/members/host/goals.json +5 -0
  87. package/templates/members/host/goals.md +3 -0
  88. package/templates/members/host/skills/host/SKILL.md +13 -0
  89. package/utils/api.js +1 -1
  90. package/utils/auth.js +17 -7
  91. package/utils/backend-root.js +31 -0
  92. package/utils/config.js +5 -4
  93. package/utils/owner-identity.js +61 -0
  94. package/atris/policies/LESSONS.md +0 -13
  95. package/atris/skills/autopilot/atris-autopilot.md +0 -71
@@ -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-4-8';
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: 'grok-4.6',
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
- // gemini 3.8 flash is the 2026-09-04 bake-off pick beside opencode muse.
74
- // --add-dir or agy edits its own scratch folder; the headless note or
75
- // gemini stops to ask "may I edit?" and exits 0 having changed nothing.
76
- model: 'gemini-3.8-flash-high',
77
- commandTemplate: '{bin} --mode accept-edits --dangerously-skip-permissions --add-dir "$PWD" {modelFlag} -p "You are running headless with edit permission already granted. Apply changes directly and never ask for confirmation. "{prompt}',
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
- function renderRunnerCommandTemplate(template, { promptFile, allowedTools, model }) {
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 \u2014 Endgame Results\n\n> Append-only. One line per closed endgame. Records outcome metrics from the horizon.\n\n---\n\n`;
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**, shipped: X/Y, wall-clock: Nh, halt: Z%, reward: total, lessons: N
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}** \u2014 shipped: ${tasksShipped}/${tasksAttempted} \u2014 wall-clock: ${wallClockStr} \u2014 halt: ${haltPercent}% \u2014 reward: ${totalReward} \u2014 lessons: ${lessonsGenerated}\n`;
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*\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+)$/);
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({ task, engine: engine.id, worktreePath: worktree.path, root: scopedRoot });
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 };
@@ -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
- // Check for blocked tasks (prefer TODO.md, fallback to legacy TASK_CONTEXTS.md)
321
- const taskFilePath = fs.existsSync(todoFile)
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
- const end = Date.now() + delay + Math.floor(Math.random() * delay);
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,
@@ -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(' \u2014 ').trim();
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
- function resolveExecutor(root, engineOverride = '') {
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 resolved = resolveExecutor(root, engineOverride);
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;
@@ -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
- return `cd ${METRIC_VERIFY_ROOT} && venv/bin/python backend/scripts/metric_verify.py ${parsed.metric} ${parsed.flag} ${parsed.target}`;
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 file = path.resolve(root, '..', 'atrisos-backend', '.atris', 'state', 'scorecards.jsonl');
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);
@@ -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
- export function createServer() {
145
- const server = new Server(
146
- { name: 'atris', version: '1.0.0' },
147
- { capabilities: { tools: {} } },
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
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
151
-
152
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
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
- return await handleTool(request.params.name, request.params.arguments || {}, key);
159
- } catch (error) {
160
- return fail(`design call failed: ${(error && error.message) || error}`);
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
- return server;
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
- const server = createServer();
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.58.7",
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
  }