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.
- package/AGENTS.md +3 -2
- package/atris/skills/engines/SKILL.md +28 -1
- package/bin/atris.js +3 -0
- package/commands/autopilot.js +75 -5
- package/commands/clean.js +17 -0
- package/commands/doc-health.js +11 -6
- package/commands/engine.js +384 -4
- package/commands/host.js +53 -0
- package/commands/member.js +57 -6
- package/commands/mission.js +72 -8
- package/commands/one-lap.js +58 -3
- package/commands/run.js +4 -1
- package/commands/scout.js +1 -1
- package/commands/search.js +5 -3
- package/commands/unknowns.js +5 -0
- package/commands/verify.js +12 -4
- package/commands/visualize.js +20 -3
- package/commands/workflow.js +17 -6
- 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 +107 -11
- package/lib/host.js +336 -0
- package/lib/known-commands.js +1 -1
- package/lib/map-refs.js +44 -1
- package/lib/member-engine.js +253 -0
- package/lib/roster-markdown.js +206 -0
- package/lib/roster-models.js +179 -0
- package/lib/runner-command.js +94 -14
- package/lib/self-drive.js +9 -1
- package/lib/wish-audit.js +10 -6
- package/lib/wish-delegate.js +10 -0
- package/package.json +1 -1
- 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
|
@@ -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
|
+
};
|
package/lib/runner-command.js
CHANGED
|
@@ -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: '
|
|
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/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,
|
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
|
@@ -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
|
@@ -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,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.
|