atris 3.58.8 → 3.60.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 +13 -0
- package/commands/autopilot.js +75 -5
- package/commands/clean.js +17 -0
- package/commands/doc-health.js +11 -6
- package/commands/engine.js +386 -4
- package/commands/host.js +62 -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/team.js +20 -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 +461 -0
- package/lib/intents.js +15 -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/team-lineup.js +167 -0
- 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 +15 -0
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,
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Who does each job, worded for `atris team` and the session boot. The rows
|
|
4
|
+
// come from the same resolver `atris engine roster` uses (jobRosterView and
|
|
5
|
+
// rosterReport in commands/engine.js); this file only picks and words them.
|
|
6
|
+
|
|
7
|
+
const { engineRunsView, modelLabel } = require('./roster-models');
|
|
8
|
+
const { rosterToolLabel } = require('./engine-registry');
|
|
9
|
+
|
|
10
|
+
const BUILT_IN_JOBS = Object.freeze(['build', 'review', 'search']);
|
|
11
|
+
const CLAUDE_FAMILY = Object.freeze(['claude', 'fable', 'haiku']);
|
|
12
|
+
|
|
13
|
+
// Built-in jobs in the order people say them, then the owner's own jobs.
|
|
14
|
+
function orderJobs(rows) {
|
|
15
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
16
|
+
const builtIn = BUILT_IN_JOBS.map((job) => list.find((row) => row.job === job && !row.key)).filter(Boolean);
|
|
17
|
+
return [...builtIn, ...list.filter((row) => !builtIn.includes(row))];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// "opus 5.5" for a claude model, "codex gpt-6-astra" for anything else, and
|
|
21
|
+
// just the tool when it runs its own default.
|
|
22
|
+
function shortRuns(view) {
|
|
23
|
+
if (!view || !view.engine) return '';
|
|
24
|
+
const tool = rosterToolLabel(view.engine);
|
|
25
|
+
if (!view.model) return tool;
|
|
26
|
+
const label = modelLabel(view.model);
|
|
27
|
+
// "atris:fast" on atris fast says the tool twice; "grok 4.7 fast" on grok
|
|
28
|
+
// already names it.
|
|
29
|
+
const plain = (text) => String(text).toLowerCase().replace(/[^a-z0-9.]+/g, ' ').trim();
|
|
30
|
+
if (plain(label) === plain(tool)) return tool;
|
|
31
|
+
if (CLAUDE_FAMILY.includes(view.engine) || plain(label).startsWith(`${plain(tool)} `)) return label;
|
|
32
|
+
return `${tool} ${label}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The full lineup: every job and every member, read once. Throws when the
|
|
36
|
+
// roster cannot be read; callers decide how to say so.
|
|
37
|
+
function readLineup(root = process.cwd(), now = new Date()) {
|
|
38
|
+
const { rosterReport } = require('../commands/engine');
|
|
39
|
+
const report = rosterReport(root, now);
|
|
40
|
+
return { jobs: orderJobs(report.jobs), team: report.team || [] };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The same read, never throwing: { ok: false, error } when it fails.
|
|
44
|
+
function readLineupSafe(root = process.cwd(), now = new Date()) {
|
|
45
|
+
try {
|
|
46
|
+
return { ok: true, ...readLineup(root, now) };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return { ok: false, error: String((error && error.message) || error || 'unknown error'), jobs: [], team: [] };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// One boot line: the tool and model that runs each built-in job right now,
|
|
53
|
+
// custom jobs only when all of them still fit. '' when nothing resolves.
|
|
54
|
+
function bootTeamLine(root = process.cwd(), { width = 69, now = new Date(), jobs = null } = {}) {
|
|
55
|
+
let rows = jobs;
|
|
56
|
+
if (!rows) {
|
|
57
|
+
const { jobRosterView } = require('../commands/engine');
|
|
58
|
+
rows = jobRosterView(root, now);
|
|
59
|
+
}
|
|
60
|
+
const ordered = orderJobs(rows);
|
|
61
|
+
const part = (row) => {
|
|
62
|
+
const text = shortRuns(row.now_runs);
|
|
63
|
+
return text ? `${row.job} ${text}` : '';
|
|
64
|
+
};
|
|
65
|
+
const builtIn = ordered.filter((row) => BUILT_IN_JOBS.includes(row.job) && !row.key).map(part).filter(Boolean);
|
|
66
|
+
const custom = ordered.filter((row) => !BUILT_IN_JOBS.includes(row.job) || row.key).map(part).filter(Boolean);
|
|
67
|
+
if (!builtIn.length && !custom.length) return '';
|
|
68
|
+
const all = [...builtIn, ...custom].join(' · ');
|
|
69
|
+
if (all.length <= width || !builtIn.length) return all;
|
|
70
|
+
return builtIn.join(' · ');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function runsText(view) {
|
|
74
|
+
return view && view.text ? view.text : 'no ready tool';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// "who does each job" lines: the tool and model running each job now, its
|
|
78
|
+
// backup, and a short note when the router or a backup is standing in.
|
|
79
|
+
function renderJobLines(jobs, { width = 80 } = {}) {
|
|
80
|
+
const rows = orderJobs(jobs);
|
|
81
|
+
if (!rows.length) return [];
|
|
82
|
+
const labelWidth = Math.max(...rows.map((row) => row.job.length));
|
|
83
|
+
return rows.map((row) => {
|
|
84
|
+
const head = ` ${row.job.padEnd(labelWidth)} ${runsText(row.now_runs)}`;
|
|
85
|
+
let note = '';
|
|
86
|
+
if (!row.pick) note = row.like ? `, same as ${row.like}` : ', picked by the router';
|
|
87
|
+
else if (row.lead === 'first' && row.backup_runs) note = `, backup ${row.backup_runs.text}`;
|
|
88
|
+
else if (row.lead !== 'first') note = row.status === 'expired' ? ', first pick expired' : ', first pick not ready';
|
|
89
|
+
const line = `${head}${note}`;
|
|
90
|
+
return line.length <= width || row.lead !== 'first' ? line : head;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Names under each job, wrapped to the width. A member running something
|
|
95
|
+
// other than its job's tool and model says what it runs.
|
|
96
|
+
function renderMemberLines(team, jobs, { width = 80 } = {}) {
|
|
97
|
+
const members = Array.isArray(team) ? team : [];
|
|
98
|
+
if (!members.length) return [];
|
|
99
|
+
const rows = orderJobs(jobs);
|
|
100
|
+
const order = [...rows.map((row) => row.job)];
|
|
101
|
+
for (const member of members) if (member.job && !order.includes(member.job)) order.push(member.job);
|
|
102
|
+
const groups = order
|
|
103
|
+
.map((job) => ({ job, row: rows.find((row) => row.job === job) || null, names: members.filter((member) => member.job === job) }))
|
|
104
|
+
.filter((group) => group.names.length);
|
|
105
|
+
const labelWidth = Math.max(...groups.map((group) => group.job.length));
|
|
106
|
+
const indent = ' '.repeat(labelWidth + 4);
|
|
107
|
+
const lines = [];
|
|
108
|
+
for (const group of groups) {
|
|
109
|
+
const jobText = group.row && group.row.now_runs ? group.row.now_runs.text : '';
|
|
110
|
+
const names = group.names.map((member) => {
|
|
111
|
+
const text = member.engine ? engineRunsView(member.engine, { model: member.model || '', effort: member.effort || '' }).text : 'no ready tool';
|
|
112
|
+
return text === jobText ? member.member : `${member.member} on ${text}`;
|
|
113
|
+
});
|
|
114
|
+
let line = ` ${group.job.padEnd(labelWidth)} `;
|
|
115
|
+
let fresh = true;
|
|
116
|
+
for (const name of names) {
|
|
117
|
+
const piece = fresh ? name : `, ${name}`;
|
|
118
|
+
if (!fresh && line.length + piece.length + 1 > width) {
|
|
119
|
+
lines.push(`${line},`);
|
|
120
|
+
line = `${indent}${name}`;
|
|
121
|
+
} else {
|
|
122
|
+
line += piece;
|
|
123
|
+
}
|
|
124
|
+
fresh = false;
|
|
125
|
+
}
|
|
126
|
+
lines.push(line);
|
|
127
|
+
}
|
|
128
|
+
return lines;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The block `atris team` prints before today's active and rest lists.
|
|
132
|
+
function renderLineup(lineup, { width = 80 } = {}) {
|
|
133
|
+
if (!lineup || !lineup.ok) return '';
|
|
134
|
+
const jobLines = renderJobLines(lineup.jobs, { width });
|
|
135
|
+
const memberLines = renderMemberLines(lineup.team, lineup.jobs, { width });
|
|
136
|
+
const blocks = [];
|
|
137
|
+
if (jobLines.length) blocks.push(['who does each job:', ...jobLines].join('\n'));
|
|
138
|
+
if (memberLines.length) blocks.push(['who is on each job:', ...memberLines].join('\n'));
|
|
139
|
+
return blocks.join('\n\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Per member, for json: the job, tool, model, and whether a roster line or
|
|
143
|
+
// the automatic match decided it. null for a name the lineup does not know.
|
|
144
|
+
function memberLineup(lineup, name) {
|
|
145
|
+
if (!lineup || !lineup.ok) return null;
|
|
146
|
+
const key = String(name || '').trim().toLowerCase();
|
|
147
|
+
const row = (lineup.team || []).find((member) => String(member.member || '').toLowerCase() === key);
|
|
148
|
+
if (!row) return null;
|
|
149
|
+
return {
|
|
150
|
+
job: row.job || null,
|
|
151
|
+
engine: row.engine || null,
|
|
152
|
+
model: row.model || null,
|
|
153
|
+
effort: row.effort || null,
|
|
154
|
+
source: row.source === 'file' ? 'roster' : 'automatic',
|
|
155
|
+
file: row.file || null,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const LINEUP_UNREADABLE = 'could not read who does each job, so tools and models are not shown. try: atris engine roster';
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
LINEUP_UNREADABLE,
|
|
163
|
+
bootTeamLine,
|
|
164
|
+
memberLineup,
|
|
165
|
+
readLineupSafe,
|
|
166
|
+
renderLineup,
|
|
167
|
+
};
|
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,15 @@
|
|
|
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
|
+
Doors pass `--ref <provider-ref>` when marking a message sent and `--reply-to-ref <provider-ref>` when handing back a reply.
|
|
5
|
+
|
|
6
|
+
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.
|
|
7
|
+
2. Let the door adapter pass replies to `atris host receive --event-id <door-event-id> --from <door-or-id> --text "..." --reply-to <message-id>` when the door provides the original outbox message id. When a reply is not a plain yes or no, read it and pass `--decision yes` or `--decision no` with what the person meant. If it is truly unclear, pass no decision. Drain replies 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.
|
|
8
|
+
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.
|
|
9
|
+
Record a link only when two people already know each other. "Wants to meet" belongs on the card, not in links.
|
|
10
|
+
4. Read `atris host people --json` before proposing. Only propose people with `can_be_introduced` true. New people get a warmer welcome with up to 3 introductions in their first month; prioritize them when proposing. Everyone else gets at most one introduction per 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.
|
|
11
|
+
An introduction reason may use only what is already on both published cards, never private answers.
|
|
12
|
+
5. Run `atris host schedule --json` after handling replies. For each requested introduction, use the calendar tools available in the workspace to find a time for both people, then call `atris host scheduled <attempt-id> --when "<time>"`. A nudge is private, and either person's yes is enough to request scheduling.
|
|
13
|
+
6. 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.
|
|
14
|
+
|
|
15
|
+
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.
|