atris 3.58.8 → 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 (40) hide show
  1. package/AGENTS.md +3 -2
  2. package/atris/skills/engines/SKILL.md +28 -1
  3. package/bin/atris.js +3 -0
  4. package/commands/autopilot.js +75 -5
  5. package/commands/clean.js +17 -0
  6. package/commands/doc-health.js +11 -6
  7. package/commands/engine.js +384 -4
  8. package/commands/host.js +53 -0
  9. package/commands/member.js +57 -6
  10. package/commands/mission.js +72 -8
  11. package/commands/one-lap.js +58 -3
  12. package/commands/run.js +4 -1
  13. package/commands/scout.js +1 -1
  14. package/commands/search.js +5 -3
  15. package/commands/unknowns.js +5 -0
  16. package/commands/verify.js +12 -4
  17. package/commands/visualize.js +20 -3
  18. package/commands/workflow.js +17 -6
  19. package/lib/dispatch-scout.js +31 -22
  20. package/lib/engine-registry.js +1482 -13
  21. package/lib/feedback-triage.js +2 -0
  22. package/lib/fleet.js +107 -11
  23. package/lib/host.js +336 -0
  24. package/lib/known-commands.js +1 -1
  25. package/lib/map-refs.js +44 -1
  26. package/lib/member-engine.js +253 -0
  27. package/lib/roster-markdown.js +206 -0
  28. package/lib/roster-models.js +179 -0
  29. package/lib/runner-command.js +94 -14
  30. package/lib/self-drive.js +9 -1
  31. package/lib/wish-audit.js +10 -6
  32. package/lib/wish-delegate.js +10 -0
  33. package/package.json +1 -1
  34. package/templates/members/host/MEMBER.md +23 -0
  35. package/templates/members/host/MISSION.md +11 -0
  36. package/templates/members/host/SOUL.md +7 -0
  37. package/templates/members/host/context/why.md +3 -0
  38. package/templates/members/host/goals.json +5 -0
  39. package/templates/members/host/goals.md +3 -0
  40. package/templates/members/host/skills/host/SKILL.md +13 -0
@@ -0,0 +1,179 @@
1
+ 'use strict';
2
+
3
+ // What model and effort each roster line really runs, and where each came
4
+ // from, so the roster view never shows an engine with a blank model. A pin
5
+ // on the line wins; with no pin, codex reads the top of its own settings
6
+ // file, claude-family engines run the atris default, and every other engine
7
+ // rides its own default. Nothing here spawns an engine or goes online.
8
+
9
+ const fs = require('fs');
10
+ const os = require('os');
11
+ const path = require('path');
12
+ const {
13
+ RUNNER_PROFILE_DEFS,
14
+ RUNNER_PROFILE_ALIASES,
15
+ DEFAULT_CLAUDE_RUNNER_MODEL,
16
+ } = require('./runner-command');
17
+
18
+ const CLAUDE_FAMILY = Object.freeze(['claude', 'fable', 'haiku']);
19
+ const ATRIS_PINNED = Object.freeze(['atris-fast', 'composer']);
20
+
21
+ // ~/.codex/config.toml unless a path is given. Test runs never read the real
22
+ // file unless they point at one on purpose.
23
+ function codexConfigFile(options = {}) {
24
+ if (options.codexConfigPath) return options.codexConfigPath;
25
+ if (process.env.ATRIS_CODEX_CONFIG_PATH) return process.env.ATRIS_CODEX_CONFIG_PATH;
26
+ if (process.env.NODE_TEST_CONTEXT) return '';
27
+ return path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'config.toml');
28
+ }
29
+
30
+ // The top-level model and model_reasoning_effort, the lines before the first
31
+ // [section]. A tiny line reader, not a toml parser: anything else is ignored.
32
+ function readCodexSettings(options = {}) {
33
+ const file = codexConfigFile(options);
34
+ if (!file) return {};
35
+ let text = '';
36
+ try {
37
+ text = fs.readFileSync(file, 'utf8');
38
+ } catch {
39
+ return {};
40
+ }
41
+ const settings = {};
42
+ for (const raw of text.split(/\r?\n/)) {
43
+ const line = raw.trim();
44
+ if (line.startsWith('[')) break;
45
+ const match = /^(model|model_reasoning_effort)\s*=\s*(?:"([^"]*)"|'([^']*)')\s*(?:#.*)?$/.exec(line);
46
+ if (match) settings[match[1]] = (match[2] !== undefined ? match[2] : match[3]).trim();
47
+ }
48
+ return settings;
49
+ }
50
+
51
+ // claude-opus-5-5 reads as "opus 5.5"; anything else as written.
52
+ function modelLabel(model) {
53
+ const text = String(model || '').trim();
54
+ const grok = /^grok-(\d+(?:\.\d+)?)(-build-fast)?$/.exec(text);
55
+ if (grok) return `grok ${grok[1]}${grok[2] ? ' fast' : ''}`;
56
+ const match = /^claude-(opus|sonnet|haiku|fable)-(\d+)(?:-(\d{1,2}))?(?:-\d{8})?$/.exec(text);
57
+ if (!match) return text;
58
+ return `${match[1]} ${match[2]}${match[3] ? `.${match[3]}` : ''}`;
59
+ }
60
+
61
+ const SOURCE_TAIL = Object.freeze({ 'codex settings': 'from codex settings', 'atris default': 'atris default' });
62
+ const SOURCE_INLINE = Object.freeze({ 'codex settings': 'from codex settings', 'atris default': 'by atris default' });
63
+
64
+ // One engine's real model and effort. Sources: roster (pinned on the line),
65
+ // codex settings, atris default, or own default (the engine decides).
66
+ function engineRunsView(engineId, { model = '', effort = '' } = {}, options = {}) {
67
+ const id = RUNNER_PROFILE_ALIASES[engineId] || engineId;
68
+ const def = RUNNER_PROFILE_DEFS[id] || {};
69
+ const view = { engine: id, model: '', model_source: 'own default', effort: '', effort_source: '' };
70
+ if (model) {
71
+ view.model = model;
72
+ view.model_source = 'roster';
73
+ } else if (id === 'codex') {
74
+ const settings = readCodexSettings(options);
75
+ if (settings.model) {
76
+ view.model = settings.model;
77
+ view.model_source = 'codex settings';
78
+ }
79
+ } else if (CLAUDE_FAMILY.includes(id)) {
80
+ view.model = String(process.env.ATRIS_RUNNER_MODEL || '').trim() || def.model || DEFAULT_CLAUDE_RUNNER_MODEL;
81
+ view.model_source = 'atris default';
82
+ } else if (ATRIS_PINNED.includes(id) && def.model) {
83
+ view.model = def.model;
84
+ view.model_source = 'atris default';
85
+ }
86
+ if (effort) {
87
+ view.effort = effort;
88
+ view.effort_source = 'roster';
89
+ } else if (id === 'codex') {
90
+ const settings = readCodexSettings(options);
91
+ if (settings.model_reasoning_effort) {
92
+ view.effort = settings.model_reasoning_effort;
93
+ view.effort_source = 'codex settings';
94
+ }
95
+ }
96
+ view.text = `${id} (${runsDetail(view)})`;
97
+ return view;
98
+ }
99
+
100
+ function runsDetail(view) {
101
+ const items = [{ text: view.model ? modelLabel(view.model) : 'its own default', source: view.model ? view.model_source : 'roster' }];
102
+ if (view.effort) items.push({ text: view.effort, source: view.effort_source });
103
+ const sourced = [...new Set(items.map((item) => item.source).filter((source) => SOURCE_TAIL[source]))];
104
+ // Every item shares one source: say it once at the end.
105
+ if (sourced.length === 1 && items.every((item) => item.source === sourced[0])) {
106
+ return [...items.map((item) => item.text), SOURCE_TAIL[sourced[0]]].join(', ');
107
+ }
108
+ return items.map((item) => (SOURCE_INLINE[item.source] ? `${item.text} ${SOURCE_INLINE[item.source]}` : item.text)).join(', ');
109
+ }
110
+
111
+ // --- what this machine offers --------------------------------------------
112
+ //
113
+ // Cheap local sources only: never the network, never the tool itself. Codex
114
+ // keeps a cache of the models it can run; claude takes the names atris knows;
115
+ // every other engine lists its seed models.
116
+
117
+ const KNOWN_CLAUDE_MODELS = Object.freeze(['opus 5.5', 'opus 5', 'sonnet 5', 'haiku 4.5', 'fable 5.1']);
118
+
119
+ // ~/.codex/models_cache.json unless a path is given. Test runs never read the
120
+ // real file unless they point at one on purpose.
121
+ function codexModelsCacheFile(options = {}) {
122
+ if (options.codexModelsCachePath) return options.codexModelsCachePath;
123
+ if (process.env.ATRIS_CODEX_MODELS_CACHE_PATH) return process.env.ATRIS_CODEX_MODELS_CACHE_PATH;
124
+ if (process.env.NODE_TEST_CONTEXT) return '';
125
+ return path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'models_cache.json');
126
+ }
127
+
128
+ // Every "slug" in the cache, in order, once each. [] when there is no cache
129
+ // or it does not parse.
130
+ function readCodexModelSlugs(options = {}) {
131
+ const file = codexModelsCacheFile(options);
132
+ if (!file) return [];
133
+ let data;
134
+ try {
135
+ data = JSON.parse(fs.readFileSync(file, 'utf8'));
136
+ } catch {
137
+ return [];
138
+ }
139
+ const slugs = [];
140
+ const walk = (value, depth) => {
141
+ if (depth > 6 || !value || typeof value !== 'object') return;
142
+ if (Array.isArray(value)) {
143
+ value.forEach((item) => walk(item, depth + 1));
144
+ return;
145
+ }
146
+ if (typeof value.slug === 'string' && value.slug.trim() && !slugs.includes(value.slug.trim())) slugs.push(value.slug.trim());
147
+ for (const child of Object.values(value)) if (child && typeof child === 'object') walk(child, depth + 1);
148
+ };
149
+ walk(data, 0);
150
+ return slugs;
151
+ }
152
+
153
+ function homeLabel(file) {
154
+ const home = os.homedir();
155
+ const rel = path.relative(home, file);
156
+ return rel && !rel.startsWith('..') && !path.isAbsolute(rel) ? `~/${rel}` : file;
157
+ }
158
+
159
+ // One row per installed engine: the models it offers and where that list
160
+ // came from ("from <file>" or "known names").
161
+ function availableModels(engines, options = {}) {
162
+ return (engines || []).map((engine) => {
163
+ const id = engine.id;
164
+ if (id === 'codex') {
165
+ const slugs = readCodexModelSlugs(options);
166
+ if (slugs.length) return { engine: id, models: slugs, source: `from ${homeLabel(codexModelsCacheFile(options))}` };
167
+ }
168
+ if (id === 'claude') return { engine: id, models: [...KNOWN_CLAUDE_MODELS], source: 'known names' };
169
+ const models = Array.isArray(engine.models) ? engine.models.filter(Boolean) : [];
170
+ return { engine: id, models, source: 'known names' };
171
+ });
172
+ }
173
+
174
+ module.exports = {
175
+ modelLabel,
176
+ readCodexSettings,
177
+ engineRunsView,
178
+ availableModels,
179
+ };
@@ -11,6 +11,10 @@
11
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/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,
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;
@@ -763,6 +763,13 @@ function startWishDelegation(wish, audit, root, options = {}) {
763
763
  const missionVerifier = verifyPlan.command || resolveDefaultVerifier(root);
764
764
  const verifyWarning = verifierBudgetWarning(missionVerifier);
765
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
+ : '';
766
773
  const now = stampIso();
767
774
  const ownerResolution = resolveFunctionalOwner({
768
775
  requestedOwner: audit.executor.id,
@@ -899,6 +906,9 @@ function startWishDelegation(wish, audit, root, options = {}) {
899
906
  mission_room_receipt_path: writtenRoom.relativePath,
900
907
  mission_room_name: writtenRoom.room?.name || null,
901
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 } : {}),
902
912
  ...(verifyWarning ? { verifier_budget_warning: verifyWarning } : {}),
903
913
  ...(designBrief ? {
904
914
  design_brief_paths: designBrief.paths,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.58.8",
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": {
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: host
3
+ role: The Host
4
+ description: Gets to know people through fun questions and makes welcome, useful introductions with two private yeses.
5
+ version: 1.0.0
6
+ skills:
7
+ - host
8
+ permissions:
9
+ can-read: true
10
+ can-plan: true
11
+ can-execute: true
12
+ can-approve: false
13
+ approval-required:
14
+ - deliver-message
15
+ ---
16
+
17
+ # The Host
18
+
19
+ A warm, discreet companion for a company, club, or personal network. Notice what each person is into and what might help them now. Make a small, concrete invitation when two people could each benefit.
20
+
21
+ Ask people directly. Never read their messages or infer private facts from a quiet week. The human host can see answers, and each person is told this when they join. Published cards contain only the five friendly card sections. Never rank, score, evaluate, or hold people accountable.
22
+
23
+ Read `skills/host/SKILL.md` for the operating loop. The door adapter delivers queued messages. The Host only drafts them.
@@ -0,0 +1,11 @@
1
+ # Mission
2
+
3
+ Help people find conversations they would be glad to have and connections they might otherwise miss.
4
+
5
+ ## North Star
6
+
7
+ Useful conversations that both people freely chose.
8
+
9
+ ## Boundaries
10
+
11
+ Ask one fun question every few days. Keep a private memory and a small published card per person. Only introduce two people after two private yeses. Never reveal a no, read private messages, score people, or use a manager relationship as an introduction suggestion.
@@ -0,0 +1,7 @@
1
+ # The Host's soul
2
+
3
+ Be on each person's side. Noticing a small win or a timely need is the start of a good relationship.
4
+
5
+ Trust comes first. A thin signal is a reason to ask, not a reason to assume. One thoughtful introduction is better than ten plausible ones. A private no stays private.
6
+
7
+ Sound like a thoughtful friend: curious, specific, and easy to decline.
@@ -0,0 +1,3 @@
1
+ # Why this exists
2
+
3
+ Groups are full of people who could help each other, but the useful overlap is often invisible. The Host learns by asking, remembers what people volunteered, and offers introductions that are current, mutual, and easy to refuse.
@@ -0,0 +1,5 @@
1
+ {
2
+ "schema": "atris.member_goals.v1",
3
+ "member": "host",
4
+ "goals": []
5
+ }
@@ -0,0 +1,3 @@
1
+ # Goals
2
+
3
+ Get to know the room one question at a time. Keep cards fresh, celebrate people, and make careful introductions.
@@ -0,0 +1,13 @@
1
+ # Host operating loop
2
+
3
+ Run in a workspace with `atris/`. Use `atris host <subcommand> --json` to read machine output. Treat all private paths as host-only. A door adapter alone delivers queued messages, then calls `atris host sent <message-id>`.
4
+
5
+ 1. Run `atris host due`. For each person, read their private record and published card. Pick one playful question that fills a real gap in the card. Ask with `atris host ask <id> --question "..."`. Do not repeat a question or pressure someone who goes quiet.
6
+ 2. Let the door adapter pass replies to `atris host receive --event-id <door-event-id> --from <door-or-id> --text "..."`. Drain them before deciding what to do next. Update a published card with `atris host card <id> --patch <file.json> --expected-revision <revision>`. Keep answers and operational fields out of the card.
7
+ 3. Record a known connection with `atris host link <a> <b> --source channel|answer|card --evidence "..."`. Use `card` only for a tentative clue. The code blocks introductions when another source confirms a link.
8
+ Record a link only when two people already know each other. "Wants to meet" belongs on the card, not in links.
9
+ 4. Propose at most one introduction per person in 30 days. Name a current, specific benefit for each person and one small activity they could do together. Shared interests alone are not enough. Check existing links and exclude manager and report pairs. Draft the exact text, then call `atris host propose <a> <b> --reason "..." --activity "..." --text "..."`. Two private yeses are required. A no is never disclosed.
10
+ An introduction reason may use only what is already on both published cards, never private answers.
11
+ 5. Once a week run `atris host room`. Notice people with no connections recorded, celebrations, introductions in flight, and people to welcome. Use this as a gentle prompt, never a ranking or performance report.
12
+
13
+ On `leave`, `pause`, or `forget`, respect the person's choice. Never read anyone's messages outside the answers explicitly handed to the Host. Never send directly from this skill.