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/commands/engine.js
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
// same names ride --engine on mission run / autopilot / run.
|
|
8
8
|
//
|
|
9
9
|
// atris engine roster + current default
|
|
10
|
+
// atris engine roster show every job's workers, built-in jobs first
|
|
11
|
+
// atris engine roster confirm renew every dated worker for 30 days
|
|
12
|
+
// atris engine roster session [clear] this shell's own changes
|
|
13
|
+
// atris engine roster --available tools and models on this machine
|
|
14
|
+
// atris engine assign <job> <tool> [--like <kind>] [--model <m>] [--effort <level>] [--max "20 min"] [--backup "<tool> [model]"] [--days <n>] [--add] [--session | --everywhere]
|
|
15
|
+
// atris engine assign <job> --remove <tool> | --clear [--session | --everywhere]
|
|
16
|
+
// jobs are open-ended: search, build, review, or your own ("small build")
|
|
10
17
|
// atris engine cursor make cursor the default engine here
|
|
11
18
|
// atris engine reset back to the house default (atris-fast)
|
|
12
19
|
//
|
|
@@ -27,8 +34,30 @@ const {
|
|
|
27
34
|
} = require('../lib/runner-command');
|
|
28
35
|
const { parseScopeFlag } = require('../lib/cli-scope');
|
|
29
36
|
const { isFreshWorkspace, speakFirstMinute } = require('../lib/first-minute');
|
|
37
|
+
const { teamRosterView } = require('../lib/member-engine');
|
|
38
|
+
const { engineRunsView, availableModels } = require('../lib/roster-models');
|
|
30
39
|
const {
|
|
31
40
|
ENGINE_ROLES,
|
|
41
|
+
ENGINE_JOBS,
|
|
42
|
+
ROSTER_SOURCES,
|
|
43
|
+
ROSTER_LAYERS,
|
|
44
|
+
rosterDecided,
|
|
45
|
+
rosterWorkerWalk,
|
|
46
|
+
rosterToolLabel,
|
|
47
|
+
rosterSessionKey,
|
|
48
|
+
clearSessionRoster,
|
|
49
|
+
NO_SESSION_MESSAGE,
|
|
50
|
+
rosterJobKey,
|
|
51
|
+
rosterJobLabel,
|
|
52
|
+
rosterJobNameError,
|
|
53
|
+
rosterJobRole,
|
|
54
|
+
readRosterState,
|
|
55
|
+
customRosterJobKeys,
|
|
56
|
+
setRosterPick,
|
|
57
|
+
confirmRoster,
|
|
58
|
+
rosterMaxText,
|
|
59
|
+
rosterPinFor,
|
|
60
|
+
parseRosterUntil,
|
|
32
61
|
ENGINE_DUTIES,
|
|
33
62
|
ENGINE_HEALTH_STATUSES,
|
|
34
63
|
binInstalled,
|
|
@@ -908,6 +937,7 @@ async function runEngineSeedCommand(args, root, deps = {}) {
|
|
|
908
937
|
|
|
909
938
|
function printRoster(root, { scope = 'workspace' } = {}) {
|
|
910
939
|
reconcileStaleEngineProbeErrors(root);
|
|
940
|
+
printJobRoster(root);
|
|
911
941
|
const list = roster(root);
|
|
912
942
|
const scoped = scope === 'global'
|
|
913
943
|
? list
|
|
@@ -952,10 +982,336 @@ function registryPayload(root, { scope = 'workspace', includeHidden = false } =
|
|
|
952
982
|
scope,
|
|
953
983
|
default: current.name,
|
|
954
984
|
source: current.source,
|
|
985
|
+
roster: jobRosterView(root),
|
|
955
986
|
engines: scoped,
|
|
956
987
|
};
|
|
957
988
|
}
|
|
958
989
|
|
|
990
|
+
// A hand-edited file can hold a string or a list where a pick belongs. Only
|
|
991
|
+
// an object naming an engine counts; anything else shows as no pick.
|
|
992
|
+
function rosterPickEntry(value) {
|
|
993
|
+
return value && typeof value === 'object' && !Array.isArray(value) && value.engine ? value : null;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const WORKER_SKIP_WORDS = Object.freeze({ 'not ready': 'down', expired: 'expired', 'bad model': 'names a model it cannot run' });
|
|
997
|
+
|
|
998
|
+
// One worker of a job's team as the view shows it: what it runs, and whether
|
|
999
|
+
// it leads now, backs up, or is skipped and why.
|
|
1000
|
+
function workerRow(step, index, leadIndex) {
|
|
1001
|
+
const { worker } = step;
|
|
1002
|
+
const runs = worker.error ? null : engineRunsView(worker.engine, { model: worker.model || '', effort: worker.effort || '' });
|
|
1003
|
+
const status = index === leadIndex ? 'leads' : step.skip ? 'skipped' : 'backup';
|
|
1004
|
+
const why = step.skip === 'bad line' ? `bad line: ${worker.error}` : step.skip ? WORKER_SKIP_WORDS[step.skip] : '';
|
|
1005
|
+
return {
|
|
1006
|
+
engine: worker.engine || null,
|
|
1007
|
+
model: worker.model || null,
|
|
1008
|
+
effort: worker.effort || null,
|
|
1009
|
+
max_seconds: Number(worker.max_seconds) > 0 ? Number(worker.max_seconds) : null,
|
|
1010
|
+
until: worker.until || null,
|
|
1011
|
+
line: worker.line || null,
|
|
1012
|
+
...(worker.error ? { text: worker.text || '' } : {}),
|
|
1013
|
+
status,
|
|
1014
|
+
why,
|
|
1015
|
+
runs,
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function jobRosterRow(job, role, root, state, registry, now, key = role) {
|
|
1020
|
+
const custom = key !== role || !ENGINE_ROLES.includes(key);
|
|
1021
|
+
const layerPicks = Object.fromEntries(ROSTER_LAYERS.map((scope) => [scope, (state[scope] && state[scope].picks) || {}]));
|
|
1022
|
+
const picks = Object.fromEntries(ROSTER_LAYERS.map((scope) => [scope, rosterPickEntry(layerPicks[scope][key])]));
|
|
1023
|
+
const resolved = resolveEngineForRoleRanked(role, root, {
|
|
1024
|
+
now,
|
|
1025
|
+
sessionRosterPicks: layerPicks.session,
|
|
1026
|
+
projectRosterPicks: layerPicks.project,
|
|
1027
|
+
machineRosterPicks: layerPicks.machine,
|
|
1028
|
+
...(custom ? { job: key } : {}),
|
|
1029
|
+
});
|
|
1030
|
+
// Show the layer that decided; when none did, show the first one set. A
|
|
1031
|
+
// custom job that fell back to its kind's pick did not decide on its own.
|
|
1032
|
+
const decided = rosterDecided(resolved.source) && (!custom || resolved.job === job);
|
|
1033
|
+
const source = decided ? resolved.source : ROSTER_LAYERS.find((scope) => picks[scope]) || null;
|
|
1034
|
+
const pick = source ? picks[source] : null;
|
|
1035
|
+
const walk = pick ? rosterWorkerWalk(pick, role, registry, now) : [];
|
|
1036
|
+
const leadIndex = decided && resolved.source === source ? walk.findIndex((step) => !step.skip) : -1;
|
|
1037
|
+
const good = walk.map((step, index) => ({ step, index })).filter(({ step }) => !step.worker.error);
|
|
1038
|
+
const first = good[0] || null;
|
|
1039
|
+
const status = !pick ? 'router'
|
|
1040
|
+
: first && first.step.skip === 'expired' ? 'expired'
|
|
1041
|
+
: first && leadIndex === first.index ? 'picked'
|
|
1042
|
+
: 'not ready';
|
|
1043
|
+
const lead = leadIndex === -1 ? 'none'
|
|
1044
|
+
: first && leadIndex === first.index ? 'first'
|
|
1045
|
+
: good[1] && leadIndex === good[1].index ? 'backup'
|
|
1046
|
+
: 'later';
|
|
1047
|
+
const kind = Object.keys(ENGINE_JOBS).find((name) => ENGINE_JOBS[name] === role);
|
|
1048
|
+
// What the job really runs: its first two good workers, each with its own
|
|
1049
|
+
// model and effort, or the router's engine when no line decides.
|
|
1050
|
+
const runsOf = (entry) => (entry ? engineRunsView(entry.step.worker.engine, { model: entry.step.worker.model || '', effort: entry.step.worker.effort || '' }) : null);
|
|
1051
|
+
const runs = runsOf(first);
|
|
1052
|
+
const nowRuns = resolved.engine
|
|
1053
|
+
? engineRunsView(resolved.engine.id, { model: resolved.engine.roster_model || '', effort: resolved.engine.roster_effort || '' })
|
|
1054
|
+
: null;
|
|
1055
|
+
return {
|
|
1056
|
+
job,
|
|
1057
|
+
role,
|
|
1058
|
+
...(custom ? { key, like: kind } : {}),
|
|
1059
|
+
pick,
|
|
1060
|
+
from: source ? ROSTER_SOURCES[source] : null,
|
|
1061
|
+
file: source ? state[source].file : null,
|
|
1062
|
+
session_pick: picks.session,
|
|
1063
|
+
project_pick: picks.project,
|
|
1064
|
+
machine_pick: picks.machine,
|
|
1065
|
+
engine: resolved.engine ? resolved.engine.id : null,
|
|
1066
|
+
model: resolved.engine && resolved.engine.roster_model ? resolved.engine.roster_model : null,
|
|
1067
|
+
effort: resolved.engine && resolved.engine.roster_effort ? resolved.engine.roster_effort : null,
|
|
1068
|
+
max_seconds: pick && Number(pick.max_seconds) > 0 ? Number(pick.max_seconds) : null,
|
|
1069
|
+
runs: runs || nowRuns,
|
|
1070
|
+
backup_runs: runsOf(good[1]),
|
|
1071
|
+
now_runs: nowRuns,
|
|
1072
|
+
workers: walk.map((step, index) => workerRow(step, index, leadIndex)),
|
|
1073
|
+
lead,
|
|
1074
|
+
status,
|
|
1075
|
+
reason: resolved.reason,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// The three built-in jobs first, then the owner's own jobs from this
|
|
1080
|
+
// session, this project, and the all-projects roster.
|
|
1081
|
+
function jobRosterView(root = process.cwd(), now = new Date(), state = null) {
|
|
1082
|
+
const registry = readEngineRegistry(root);
|
|
1083
|
+
const layers = state || readRosterState(root, { now });
|
|
1084
|
+
const rows = Object.entries(ENGINE_JOBS).map(([job, role]) => jobRosterRow(job, role, root, layers, registry, now));
|
|
1085
|
+
const customKeys = [...new Set(ROSTER_LAYERS.flatMap((scope) => customRosterJobKeys(layers[scope] && layers[scope].picks)))];
|
|
1086
|
+
for (const key of customKeys) {
|
|
1087
|
+
const role = rosterJobRole(key, root, {
|
|
1088
|
+
now,
|
|
1089
|
+
sessionRosterPicks: layers.session.picks,
|
|
1090
|
+
projectRosterPicks: layers.project.picks,
|
|
1091
|
+
machineRosterPicks: layers.machine.picks,
|
|
1092
|
+
});
|
|
1093
|
+
if (!role) continue;
|
|
1094
|
+
rows.push(jobRosterRow(rosterJobLabel(key), role, root, layers, registry, now, key));
|
|
1095
|
+
}
|
|
1096
|
+
return rows;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Jobs, the team, and one warning per roster line that could not be used.
|
|
1100
|
+
function rosterReport(root = process.cwd(), now = new Date()) {
|
|
1101
|
+
const state = readRosterState(root, { now });
|
|
1102
|
+
const jobs = jobRosterView(root, now, state);
|
|
1103
|
+
const team = teamRosterView(root, { now, rosterState: state });
|
|
1104
|
+
return {
|
|
1105
|
+
jobs,
|
|
1106
|
+
team: team.rows,
|
|
1107
|
+
warnings: [...state.session.warnings, ...state.project.warnings, ...state.machine.warnings, ...team.warnings],
|
|
1108
|
+
files: {
|
|
1109
|
+
session: state.session.format === 'none' ? null : state.session.file,
|
|
1110
|
+
project: state.project.format === 'none' ? null : state.project.file,
|
|
1111
|
+
machine: state.machine.format === 'none' ? null : state.machine.file,
|
|
1112
|
+
},
|
|
1113
|
+
session: state.session.key || null,
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function untilText(worker) {
|
|
1118
|
+
const until = parseRosterUntil(worker.until);
|
|
1119
|
+
if (until) return `until ${until.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }).toLowerCase()}`;
|
|
1120
|
+
return worker.never_expires ? 'no end date' : 'until no valid date';
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Every worker under its job, numbered in order, when the job has more than
|
|
1124
|
+
// the lead and one backup (the job's own line already shows those two).
|
|
1125
|
+
function renderWorkerLines(row) {
|
|
1126
|
+
if (!row.workers || row.workers.length <= 2) return [];
|
|
1127
|
+
const width = Math.max(...row.workers.map((worker) => (worker.runs ? worker.runs.text : worker.text || '').length));
|
|
1128
|
+
return row.workers.map((worker, index) => {
|
|
1129
|
+
const what = (worker.runs ? worker.runs.text : worker.text || '').padEnd(width);
|
|
1130
|
+
const extras = [worker.max_seconds ? rosterMaxText(worker.max_seconds) : '', worker.until ? `until ${worker.until}` : ''].filter(Boolean);
|
|
1131
|
+
const state = worker.status === 'leads' ? 'leads now' : worker.status === 'backup' ? 'backup' : `skipped, ${worker.why}`;
|
|
1132
|
+
return ` ${index + 1}. ${what} ${[state, ...extras].join(', ')}`.trimEnd();
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function renderJobRoster(rows) {
|
|
1137
|
+
const width = Math.max(7, ...rows.map((row) => row.job.length));
|
|
1138
|
+
const ownerWidth = Math.max(24, ...rows.filter((row) => row.pick && row.runs).map((row) => row.runs.text.length));
|
|
1139
|
+
const backupWidth = Math.max(16, ...rows.filter((row) => row.backup_runs).map((row) => row.backup_runs.text.length + 7));
|
|
1140
|
+
return rows.map((row) => {
|
|
1141
|
+
const label = row.job.padEnd(width);
|
|
1142
|
+
const nowText = row.now_runs ? row.now_runs.text : 'no ready engine';
|
|
1143
|
+
const fallsTo = `${row.like ? `falls back to ${row.like}` : 'router decides'}: ${nowText}`;
|
|
1144
|
+
if (!row.pick) return `${label} no pick, ${fallsTo}`;
|
|
1145
|
+
const owner = row.runs.text.padEnd(ownerWidth);
|
|
1146
|
+
const backup = row.backup_runs ? `backup ${row.backup_runs.text}` : 'no backup';
|
|
1147
|
+
const cap = row.max_seconds ? `${rosterMaxText(row.max_seconds)}, ` : '';
|
|
1148
|
+
const date = untilText(row.pick);
|
|
1149
|
+
const fallback = row.lead === 'backup' ? 'using backup' : row.lead === 'later' ? `using ${nowText}` : fallsTo;
|
|
1150
|
+
const status = row.status === 'expired' ? `expired, ${fallback}`
|
|
1151
|
+
: row.status === 'not ready' ? `not ready, ${fallback}`
|
|
1152
|
+
: date;
|
|
1153
|
+
const where = row.file ? `${row.from} (${row.file})` : row.from;
|
|
1154
|
+
const head = `${label} ${owner} ${backup.padEnd(backupWidth)} ${cap}${status}, ${where}`.trimEnd();
|
|
1155
|
+
return [head, ...renderWorkerLines(row)].join('\n');
|
|
1156
|
+
}).join('\n');
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function renderTeamRoster(rows) {
|
|
1160
|
+
if (!rows.length) return '';
|
|
1161
|
+
const width = Math.max(6, ...rows.map((row) => row.member.length));
|
|
1162
|
+
const jobWidth = Math.max(6, ...rows.map((row) => String(row.job || '').length));
|
|
1163
|
+
const lines = rows.map((row) => {
|
|
1164
|
+
const engine = row.engine ? engineRunsView(row.engine, { model: row.model || '', effort: row.effort || '' }).text : 'no ready engine';
|
|
1165
|
+
const how = row.source === 'file' ? `from ${row.file}` : 'automatic';
|
|
1166
|
+
return `${row.member.padEnd(width)} ${String(row.job || '').padEnd(jobWidth)} ${engine.padEnd(24)} ${how}`.trimEnd();
|
|
1167
|
+
});
|
|
1168
|
+
return ['team', ...lines].join('\n');
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function renderRosterWarnings(warnings) {
|
|
1172
|
+
return warnings.map((warning) => `warning: ${warning.file} line ${warning.line} "${warning.text}" ${warning.message}.`).join('\n');
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function renderRosterReport(report) {
|
|
1176
|
+
return [renderJobRoster(report.jobs), renderTeamRoster(report.team), renderRosterWarnings(report.warnings)]
|
|
1177
|
+
.filter(Boolean)
|
|
1178
|
+
.join('\n\n');
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function printJobRoster(root, now = new Date()) {
|
|
1182
|
+
console.log(renderRosterReport(rosterReport(root, now)));
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const AVAILABLE_HINT = 'see which tools and models this machine has: atris engine roster --available';
|
|
1186
|
+
|
|
1187
|
+
// Each installed tool and the models it offers, from local files only.
|
|
1188
|
+
function availableReport(root) {
|
|
1189
|
+
const engines = readEngineRegistry(root, { persist: false }).engines
|
|
1190
|
+
.filter((engine) => !HIDDEN_PROFILE_NAMES.includes(engine.id))
|
|
1191
|
+
.filter((engine) => engine.installed || (engine.health && engine.health.status === 'ready'));
|
|
1192
|
+
return availableModels(engines).map((row) => ({ ...row, tool: rosterToolLabel(row.engine) }));
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function renderAvailable(rows) {
|
|
1196
|
+
if (!rows.length) return 'no tools found on this machine. run atris engine doctor to check again';
|
|
1197
|
+
const width = Math.max(...rows.map((row) => row.tool.length));
|
|
1198
|
+
return ['on this machine', ...rows.map((row) => `${row.tool.padEnd(width)} ${row.models.join(', ') || 'its own default'} (${row.source})`)].join('\n');
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Only this session's changes, from its own file.
|
|
1202
|
+
function renderSessionRoster(report, key) {
|
|
1203
|
+
const jobs = report.jobs.filter((row) => row.from === ROSTER_SOURCES.session);
|
|
1204
|
+
const team = report.team.filter((row) => row.source === 'file' && row.file === report.files.session);
|
|
1205
|
+
if (!jobs.length && !team.length) return `this session (${key}) has no roster changes. add one with: atris engine assign <job> <tool> --session`;
|
|
1206
|
+
const head = `this session (${key}) changes${report.files.session ? `, from ${report.files.session}` : ''}`;
|
|
1207
|
+
return [head, renderJobRoster(jobs), renderTeamRoster(team)].filter(Boolean).join('\n\n');
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function runRosterSessionCommand(rest, json, root, now) {
|
|
1211
|
+
const key = rosterSessionKey();
|
|
1212
|
+
if (rest[1] === 'clear') {
|
|
1213
|
+
try {
|
|
1214
|
+
const cleared = clearSessionRoster();
|
|
1215
|
+
console.log(cleared ? 'cleared this session\'s roster changes' : 'this session has no roster changes to clear');
|
|
1216
|
+
return 0;
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
console.error(err.message);
|
|
1219
|
+
return 2;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (!key) {
|
|
1223
|
+
console.error(NO_SESSION_MESSAGE);
|
|
1224
|
+
return 2;
|
|
1225
|
+
}
|
|
1226
|
+
const report = rosterReport(root, now);
|
|
1227
|
+
if (json) {
|
|
1228
|
+
const jobs = report.jobs.filter((row) => row.from === ROSTER_SOURCES.session);
|
|
1229
|
+
const team = report.team.filter((row) => row.source === 'file' && row.file === report.files.session);
|
|
1230
|
+
console.log(JSON.stringify({ session: key, file: report.files.session, jobs, team }, null, 2));
|
|
1231
|
+
} else {
|
|
1232
|
+
console.log(renderSessionRoster(report, key));
|
|
1233
|
+
}
|
|
1234
|
+
return 0;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function runRosterCommand(args, root, now = new Date()) {
|
|
1238
|
+
const json = args.includes('--json');
|
|
1239
|
+
const available = args.includes('--available');
|
|
1240
|
+
const rest = args.filter((arg) => arg !== '--json' && arg !== '--available');
|
|
1241
|
+
const known = !rest.length
|
|
1242
|
+
|| (rest.length === 1 && ['confirm', 'session'].includes(rest[0]))
|
|
1243
|
+
|| (rest.length === 2 && rest[0] === 'session' && rest[1] === 'clear');
|
|
1244
|
+
if (!known || (available && rest.length)) {
|
|
1245
|
+
console.error('usage: atris engine roster [confirm | session [clear] | --available] [--json]');
|
|
1246
|
+
return 2;
|
|
1247
|
+
}
|
|
1248
|
+
if (available) {
|
|
1249
|
+
const rows = availableReport(root);
|
|
1250
|
+
console.log(json ? JSON.stringify({ tools: rows }, null, 2) : renderAvailable(rows));
|
|
1251
|
+
return 0;
|
|
1252
|
+
}
|
|
1253
|
+
if (rest[0] === 'session') return runRosterSessionCommand(rest, json, root, now);
|
|
1254
|
+
if (rest[0] === 'confirm') confirmRoster(root, now);
|
|
1255
|
+
const report = rosterReport(root, now);
|
|
1256
|
+
if (json) console.log(JSON.stringify(report, null, 2));
|
|
1257
|
+
else console.log(`${renderRosterReport(report)}\n\n${AVAILABLE_HINT}`);
|
|
1258
|
+
return 0;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const ASSIGN_USAGE = 'usage: atris engine assign <job> <tool> [--model <m>] [--effort low|medium|high|xhigh|max] [--max "20 min"] [--days <n>] [--add] [--backup "<tool> [model]"] [--like search|build|review] [--session | --everywhere], or atris engine assign <job> --remove <tool> | --clear. jobs live in atris/ROSTER.md (~/.atris/ROSTER.md with --everywhere, this shell only with --session) and you can edit that file any time';
|
|
1262
|
+
|
|
1263
|
+
function runAssignCommand(args, root, now = new Date()) {
|
|
1264
|
+
const job = args[0] && !String(args[0]).startsWith('--') ? args[0] : '';
|
|
1265
|
+
if (!job) {
|
|
1266
|
+
console.error('name the job first: atris engine assign <job> <tool>. jobs: search, build, review, or your own, like "small build"');
|
|
1267
|
+
return 2;
|
|
1268
|
+
}
|
|
1269
|
+
if (!rosterJobKey(job)) {
|
|
1270
|
+
console.error(rosterJobNameError(job));
|
|
1271
|
+
return 2;
|
|
1272
|
+
}
|
|
1273
|
+
const switches = ['--everywhere', '--session', '--clear', '--add'];
|
|
1274
|
+
const everywhere = args.includes('--everywhere');
|
|
1275
|
+
const session = args.includes('--session');
|
|
1276
|
+
const clear = args.includes('--clear');
|
|
1277
|
+
const add = args.includes('--add');
|
|
1278
|
+
const rest = args.slice(1).filter((arg) => !switches.includes(arg));
|
|
1279
|
+
// The tool may be several words: claude code, atris fast.
|
|
1280
|
+
const toolWords = [];
|
|
1281
|
+
while (rest.length && !rest[0].startsWith('--')) toolWords.push(rest.shift());
|
|
1282
|
+
const engine = toolWords.join(' ');
|
|
1283
|
+
const flags = {};
|
|
1284
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
1285
|
+
const arg = rest[i];
|
|
1286
|
+
if (!['--model', '--backup', '--days', '--like', '--effort', '--max', '--remove'].includes(arg) || !rest[i + 1] || rest[i + 1].startsWith('--')) {
|
|
1287
|
+
console.error(`invalid assign option "${arg}"`);
|
|
1288
|
+
return 2;
|
|
1289
|
+
}
|
|
1290
|
+
flags[arg.slice(2)] = rest[++i];
|
|
1291
|
+
}
|
|
1292
|
+
const removing = flags.remove !== undefined;
|
|
1293
|
+
if (clear && (engine || Object.keys(flags).length || add)) {
|
|
1294
|
+
console.error('use --clear without an engine or other options');
|
|
1295
|
+
return 2;
|
|
1296
|
+
}
|
|
1297
|
+
if (removing && (engine || Object.keys(flags).length > 1 || add)) {
|
|
1298
|
+
console.error('use --remove <tool> without other options');
|
|
1299
|
+
return 2;
|
|
1300
|
+
}
|
|
1301
|
+
if (!clear && !removing && !engine) {
|
|
1302
|
+
console.error(ASSIGN_USAGE);
|
|
1303
|
+
return 2;
|
|
1304
|
+
}
|
|
1305
|
+
try {
|
|
1306
|
+
setRosterPick(job, engine, { ...flags, clear, add, session, everywhere, now }, root);
|
|
1307
|
+
printJobRoster(root, now);
|
|
1308
|
+
return 0;
|
|
1309
|
+
} catch (err) {
|
|
1310
|
+
console.error(err.message);
|
|
1311
|
+
return 2;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
959
1315
|
function chartEngineLabel(engine) {
|
|
960
1316
|
const id = String(engine && (engine.id || engine.name) || '').trim();
|
|
961
1317
|
const models = Array.isArray(engine && engine.models)
|
|
@@ -1104,14 +1460,22 @@ function runResolveCommand(args, root) {
|
|
|
1104
1460
|
const json = args.includes('--json');
|
|
1105
1461
|
const role = args.filter((a) => !String(a).startsWith('--'))[0] || '';
|
|
1106
1462
|
if (!role) {
|
|
1107
|
-
const message = `usage: atris engine resolve <role>; roles: ${ENGINE_ROLES.join(', ')}`;
|
|
1463
|
+
const message = `usage: atris engine resolve <role> or <job>; roles: ${ENGINE_ROLES.join(', ')}; jobs: search, build, review, or your own`;
|
|
1108
1464
|
if (json) console.log(JSON.stringify({ ok: false, error: message }, null, 2));
|
|
1109
1465
|
else console.error(message);
|
|
1110
1466
|
return 2;
|
|
1111
1467
|
}
|
|
1112
1468
|
let picked;
|
|
1113
1469
|
try {
|
|
1114
|
-
|
|
1470
|
+
// A role (navigator, executor, validator) resolves as before; any job
|
|
1471
|
+
// name, built-in or the owner's own, resolves through its roster pick.
|
|
1472
|
+
const jobRole = ENGINE_ROLES.includes(String(role).trim().toLowerCase()) ? '' : rosterJobRole(role, root);
|
|
1473
|
+
if (!ENGINE_ROLES.includes(String(role).trim().toLowerCase()) && !jobRole) {
|
|
1474
|
+
throw new Error(`Unknown role "${role}". Known roles: ${ENGINE_ROLES.join(', ')}; jobs: search, build, review, or one set with atris engine assign`);
|
|
1475
|
+
}
|
|
1476
|
+
picked = jobRole
|
|
1477
|
+
? resolveEngineForRoleRanked(jobRole, root, { job: role })
|
|
1478
|
+
: resolveEngineForRoleRanked(role, root);
|
|
1115
1479
|
} catch (err) {
|
|
1116
1480
|
if (json) console.log(JSON.stringify({ ok: false, error: err.message }, null, 2));
|
|
1117
1481
|
else console.error(err.message);
|
|
@@ -1578,7 +1942,20 @@ function runDispatchCommand(args, root) {
|
|
|
1578
1942
|
: `engine dispatch: ${err.message}`);
|
|
1579
1943
|
return 2;
|
|
1580
1944
|
}
|
|
1581
|
-
|
|
1945
|
+
// The build line's model, effort, and time cap ride along when the roster
|
|
1946
|
+
// picks this same engine for build.
|
|
1947
|
+
const pin = rosterPinFor('executor', canonical, root);
|
|
1948
|
+
return runDispatchFlight({
|
|
1949
|
+
root,
|
|
1950
|
+
taskIds,
|
|
1951
|
+
engine: canonical,
|
|
1952
|
+
prompt: promptOverride,
|
|
1953
|
+
yolo,
|
|
1954
|
+
...(base ? { checkoutBase: base } : {}),
|
|
1955
|
+
...(pin.roster_model ? { model: pin.roster_model } : {}),
|
|
1956
|
+
...(pin.roster_effort ? { effort: pin.roster_effort } : {}),
|
|
1957
|
+
...(pin.roster_max_seconds ? { maxSeconds: pin.roster_max_seconds } : {}),
|
|
1958
|
+
}).then((flight) => {
|
|
1582
1959
|
if (json) console.log(JSON.stringify(flight, null, 2));
|
|
1583
1960
|
return flight.paused.length ? 1 : 0;
|
|
1584
1961
|
});
|
|
@@ -1657,6 +2034,9 @@ function engineCommand(args = [], deps = {}) {
|
|
|
1657
2034
|
const positional = scope.args.filter((a) => !a.startsWith('--'));
|
|
1658
2035
|
const sub = (positional[0] || '').trim();
|
|
1659
2036
|
|
|
2037
|
+
if (sub === 'roster') return runRosterCommand(scope.args.slice(scope.args.indexOf('roster') + 1), root, deps.now);
|
|
2038
|
+
if (sub === 'assign') return runAssignCommand(scope.args.slice(scope.args.indexOf('assign') + 1), root, deps.now);
|
|
2039
|
+
|
|
1660
2040
|
if (sub === 'login') {
|
|
1661
2041
|
return runEngineLoginCommand(scope.args.slice(scope.args.indexOf('login') + 1), root);
|
|
1662
2042
|
}
|
|
@@ -1705,7 +2085,7 @@ function engineCommand(args = [], deps = {}) {
|
|
|
1705
2085
|
if (sub === 'help' || args.includes('--help') || args.includes('-h')) {
|
|
1706
2086
|
console.log('\n atris engine watch [<id>|latest] [--no-follow]\n follow one live transcript or list running engine work');
|
|
1707
2087
|
console.log('\n long read-only asks: atris engine ask "..." --engine agy --timeout <seconds>\n quick asks default to 120 seconds; explicit jobs allow up to 3600\n follow live work with atris engine watch latest');
|
|
1708
|
-
console.log('\n atris engine roster + current default\n atris engines --chart show the fleet as an org chart\n atris engine list --json [--all] full registry: default + engines with tier, roles, fallback, health (--all includes hidden engines)\n atris engine set <name> --duty leader|errands|learning [--models "a, b"]\n arrange the fleet and save its model policy\n atris engine resolve <role> [--json]\n choose the best ready engine for navigator|executor|validator\n atris engine health <name> --set ready|not_installed|credit_out\n flip runtime health, for example when credits run out\n atris engine doctor [--json]\n probe which engine CLIs are installed here and sync that into health policy\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine bench [names...] [--runs N]\n ranked latency scoreboard of engine passes\n atris engine ask "<question>" --engine <name> [--engine <name> ...]\n ask several engines in parallel without allowing edits\n atris engine ask --jobs <jobs.json>\n ask different read-only questions in parallel\n atris engine validate <receipt-path|latest> [--engine <name>]\n check ask answers with a different read-only referee\n atris engine validate scoreboard\n show pass rates by worker engine\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]\n one-command claim, worktree, build, verify, ship, ready\n atris engine login <provider> --yes\n upload a local provider CLI login to the backend vault\n atris engine login <provider> --computer [--seat <name>]\n atris engine login <provider> --business <id> [--seat <name>]\n sign in on an Atris computer by device flow\n atris engine login --list | --remove <provider>\n list or remove vaulted provider logins\n atris engine seats show which named accounts are ready to work\n atris engine seed <provider> --business <id>|--user\n push a vaulted login onto an Atris computer\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
|
|
2088
|
+
console.log('\n atris engine roster + current default\n atris engine roster [--json] show every job and its workers in order: search, build, review, and your own\n atris engine roster confirm renew every dated worker for 30 days\n atris engine roster session [clear]\n show or drop the roster changes for this shell only\n atris engine roster --available\n tools on this machine and the models each one offers\n atris engine assign <job> <tool> [--like <kind>] [--model <m>] [--effort <level>] [--max "20 min"] [--backup "<tool> [model]"] [--days <n>] [--add] [--session | --everywhere]\n atris engine assign <job> --remove <tool> | --clear [--session | --everywhere]\n sets the lead worker of the job; --add puts one more at the end; jobs: search, build, review,\n or your own like "small build" (--like search|build|review when the name does not say);\n --everywhere is for all projects; --session is this shell only (set ATRIS_ROSTER_SESSION=<name>)\n atris engines --chart show the fleet as an org chart\n atris engine list --json [--all] full registry: default + engines with tier, roles, fallback, health (--all includes hidden engines)\n atris engine set <name> --duty leader|errands|learning [--models "a, b"]\n arrange the fleet and save its model policy\n atris engine resolve <role> or <job> [--json]\n choose the best ready engine for navigator|executor|validator or a roster job\n atris engine health <name> --set ready|not_installed|credit_out\n flip runtime health, for example when credits run out\n atris engine doctor [--json]\n probe which engine CLIs are installed here and sync that into health policy\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine bench [names...] [--runs N]\n ranked latency scoreboard of engine passes\n atris engine ask "<question>" --engine <name> [--engine <name> ...]\n ask several engines in parallel without allowing edits\n atris engine ask --jobs <jobs.json>\n ask different read-only questions in parallel\n atris engine validate <receipt-path|latest> [--engine <name>]\n check ask answers with a different read-only referee\n atris engine validate scoreboard\n show pass rates by worker engine\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]\n one-command claim, worktree, build, verify, ship, ready\n atris engine login <provider> --yes\n upload a local provider CLI login to the backend vault\n atris engine login <provider> --computer [--seat <name>]\n atris engine login <provider> --business <id> [--seat <name>]\n sign in on an Atris computer by device flow\n atris engine login --list | --remove <provider>\n list or remove vaulted provider logins\n atris engine seats show which named accounts are ready to work\n atris engine seed <provider> --business <id>|--user\n push a vaulted login onto an Atris computer\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
|
|
1709
2089
|
return 0;
|
|
1710
2090
|
}
|
|
1711
2091
|
if (isFreshWorkspace(root)) {
|
|
@@ -1753,6 +2133,8 @@ module.exports = {
|
|
|
1753
2133
|
canonicalEngineName,
|
|
1754
2134
|
readSavedEngine,
|
|
1755
2135
|
roster,
|
|
2136
|
+
rosterReport,
|
|
2137
|
+
jobRosterView,
|
|
1756
2138
|
engineRegistryFile,
|
|
1757
2139
|
readEngineRegistry,
|
|
1758
2140
|
resolveEngineForRole,
|
package/commands/host.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { hostAction } = require('../lib/host');
|
|
4
|
+
|
|
5
|
+
function parse(argv) {
|
|
6
|
+
const options = {};
|
|
7
|
+
const positionals = [];
|
|
8
|
+
const flags = new Set(['id', 'name', 'team', 'manager', 'door', 'now', 'question', 'event-id', 'from', 'text', 'reply-to', 'reply-to-ref', 'ref', 'decision', 'patch', 'expected-revision', 'source', 'evidence', 'reason', 'activity', 'as', 'when']);
|
|
9
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
10
|
+
const item = argv[i];
|
|
11
|
+
if (item === '--json') { options.json = true; continue; }
|
|
12
|
+
if (item.startsWith('--')) {
|
|
13
|
+
const key = item.slice(2);
|
|
14
|
+
if (!flags.has(key) || !argv[i + 1] || argv[i + 1].startsWith('--')) throw new Error(`invalid --${key} value`);
|
|
15
|
+
options[key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())] = argv[++i];
|
|
16
|
+
} else positionals.push(item);
|
|
17
|
+
}
|
|
18
|
+
return { options, positionals };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function human(command, result) {
|
|
22
|
+
if (command === 'room') return result.text;
|
|
23
|
+
if (command === 'due') return result.length ? result.map((entry) => `${entry.name} (${entry.id}) is due for a question.`).join('\n') : 'no questions due';
|
|
24
|
+
if (command === 'outbox') return result.length ? result.map((entry) => `${entry.id} ${entry.kind} to ${entry.to}: ${entry.text}`).join('\n') : 'outbox empty';
|
|
25
|
+
if (command === 'schedule') return result.length ? result.map((entry) => `${entry.attempt_id}: ${entry.names.a} (${entry.a}) and ${entry.names.b} (${entry.b}), ${entry.activity}`).join('\n') : 'no introductions need scheduling';
|
|
26
|
+
if (command === 'people') {
|
|
27
|
+
if (!result.length) return 'no active people';
|
|
28
|
+
return result.map((entry) => {
|
|
29
|
+
const fields = ['into_lately', 'going_for', 'great_at', 'wants_to_meet', 'worth_celebrating']
|
|
30
|
+
.map((key) => ` ${key.replaceAll('_', ' ')}: ${entry[key]}`).join('\n');
|
|
31
|
+
return `${entry.name} (${entry.id})\n team: ${entry.team || 'none'}\n manager: ${entry.manager_id || 'none'}\n status: ${entry.status}\n can be introduced: ${entry.can_be_introduced ? 'yes' : 'no'}\n${fields}`;
|
|
32
|
+
}).join('\n');
|
|
33
|
+
}
|
|
34
|
+
if (command === 'view') return [...result.cards.map((entry) => entry.card), result.own ? `Your private record:\n${JSON.stringify(result.own, null, 2)}` : ''].filter(Boolean).join('\n');
|
|
35
|
+
if (command === 'receive') return result.duplicate ? 'event already recorded' : `${result.kind} recorded for ${result.id}`;
|
|
36
|
+
return `${command} complete: ${JSON.stringify(result)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hostCommand(argv = process.argv.slice(3), root = process.cwd()) {
|
|
40
|
+
let jsonOutput = argv.includes('--json');
|
|
41
|
+
try {
|
|
42
|
+
const [command, ...rest] = argv;
|
|
43
|
+
if (!command || command === '--help' || command === '-h') {
|
|
44
|
+
const usage = 'usage: atris host join|leave|pause|resume|forget|due|ask|receive|card|link|propose|people|outbox|sent|room|view|schedule|scheduled [options]';
|
|
45
|
+
console.log(jsonOutput ? JSON.stringify({ usage }) : usage);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const { options, positionals } = parse(rest);
|
|
49
|
+
jsonOutput = options.json;
|
|
50
|
+
const args = { ...options };
|
|
51
|
+
if (['leave', 'pause', 'resume', 'forget', 'ask', 'card', 'sent', 'scheduled'].includes(command)) args.id = positionals[0];
|
|
52
|
+
if (['link', 'propose'].includes(command)) { args.a = positionals[0]; args.b = positionals[1]; }
|
|
53
|
+
if (command === 'receive') args.eventId = options.eventId;
|
|
54
|
+
const result = hostAction(root, command, args);
|
|
55
|
+
console.log(jsonOutput ? JSON.stringify(result) : human(command, result));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(jsonOutput ? JSON.stringify({ error: error.message }) : `host: ${error.message}`);
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { hostCommand };
|
package/commands/member.js
CHANGED
|
@@ -861,6 +861,22 @@ function pushFlagWhenPresent(out, args, name) {
|
|
|
861
861
|
if (hasFlag(args, name)) out.push(name);
|
|
862
862
|
}
|
|
863
863
|
|
|
864
|
+
// The runner a new member mission starts with. An explicit --runner or
|
|
865
|
+
// --engine wins. With neither, a member the roster names runs on "auto", so
|
|
866
|
+
// every tick picks the member's engine from ROSTER.md (edits apply on the next
|
|
867
|
+
// tick). Otherwise the old default holds.
|
|
868
|
+
function memberRunRunner(owner, args = [], cwd = process.cwd()) {
|
|
869
|
+
const explicit = readFlag(args, '--runner', '');
|
|
870
|
+
if (explicit) return explicit;
|
|
871
|
+
// codex_goal stalls unattended (no live codex session drives its native goal
|
|
872
|
+
// slot). Default to claude unless a live codex session is signalled; explicit
|
|
873
|
+
// --runner still wins. Proven footgun 2026-07-16. See lib/default-runner.js.
|
|
874
|
+
const fallback = defaultObjectiveRunner(args);
|
|
875
|
+
if (fallback !== 'claude' || readFlag(args, '--engine', '')) return fallback;
|
|
876
|
+
const { memberRosterEngine } = require('../lib/member-engine');
|
|
877
|
+
return memberRosterEngine(owner, cwd) ? 'auto' : fallback;
|
|
878
|
+
}
|
|
879
|
+
|
|
864
880
|
function buildMemberRunStartArgs(owner, missionText, args = [], cwd = process.cwd()) {
|
|
865
881
|
const startArgs = [
|
|
866
882
|
'mission',
|
|
@@ -868,11 +884,8 @@ function buildMemberRunStartArgs(owner, missionText, args = [], cwd = process.cw
|
|
|
868
884
|
missionText,
|
|
869
885
|
'--owner',
|
|
870
886
|
owner,
|
|
871
|
-
// codex_goal stalls unattended (no live codex session drives its native goal
|
|
872
|
-
// slot). Default to claude unless a live codex session is signalled; explicit
|
|
873
|
-
// --runner still wins. Proven footgun 2026-07-16. See lib/default-runner.js.
|
|
874
887
|
'--runner',
|
|
875
|
-
|
|
888
|
+
memberRunRunner(owner, args, cwd),
|
|
876
889
|
'--lane',
|
|
877
890
|
readFlag(args, '--lane', 'workspace'),
|
|
878
891
|
];
|
|
@@ -4303,7 +4316,25 @@ function memberInstall(slug) {
|
|
|
4303
4316
|
const memberDir = path.join(process.cwd(), 'atris', 'team', slug);
|
|
4304
4317
|
const already = memberBundlePresent(memberDir);
|
|
4305
4318
|
fs.mkdirSync(path.join(memberDir, 'logs'), { recursive: true });
|
|
4306
|
-
|
|
4319
|
+
const templateDir = path.join(__dirname, '..', 'templates', 'members', slug);
|
|
4320
|
+
const copied = [];
|
|
4321
|
+
if (fs.existsSync(templateDir)) {
|
|
4322
|
+
const copyMissing = (source, relative = '') => {
|
|
4323
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
4324
|
+
const rel = path.join(relative, entry.name);
|
|
4325
|
+
const target = path.join(memberDir, rel);
|
|
4326
|
+
if (entry.isDirectory()) {
|
|
4327
|
+
fs.mkdirSync(target, { recursive: true });
|
|
4328
|
+
copyMissing(path.join(source, entry.name), rel);
|
|
4329
|
+
} else if (entry.isFile() && !fs.existsSync(target)) {
|
|
4330
|
+
fs.writeFileSync(target, fs.readFileSync(path.join(source, entry.name)), { flag: 'wx' });
|
|
4331
|
+
copied.push(rel);
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
};
|
|
4335
|
+
copyMissing(templateDir);
|
|
4336
|
+
}
|
|
4337
|
+
if (already && copied.length === 0) {
|
|
4307
4338
|
console.log(`MEMBER already installed for ${slug}`);
|
|
4308
4339
|
return;
|
|
4309
4340
|
}
|
|
@@ -4312,7 +4343,7 @@ function memberInstall(slug) {
|
|
|
4312
4343
|
name: slug,
|
|
4313
4344
|
source: 'install',
|
|
4314
4345
|
});
|
|
4315
|
-
for (const rel of created) {
|
|
4346
|
+
for (const rel of [...copied, ...created]) {
|
|
4316
4347
|
console.log(`atris/team/${slug}/${rel.split(path.sep).join('/')}`);
|
|
4317
4348
|
}
|
|
4318
4349
|
console.log(`MEMBER installed for ${slug}.`);
|
|
@@ -7787,6 +7818,24 @@ function wakeDecision(name, paths, { force = false, runtimeKind = memberRuntimeK
|
|
|
7787
7818
|
return null;
|
|
7788
7819
|
}
|
|
7789
7820
|
|
|
7821
|
+
// The engine this member's work runs on, from the roster: its own team line,
|
|
7822
|
+
// else its job's pick, else the router. Never throws.
|
|
7823
|
+
function wakeMemberEngine(name, root = process.cwd()) {
|
|
7824
|
+
try {
|
|
7825
|
+
const { resolveEngineForMember } = require('../lib/member-engine');
|
|
7826
|
+
const picked = resolveEngineForMember(name, root);
|
|
7827
|
+
return {
|
|
7828
|
+
id: picked.engine ? picked.engine.id : null,
|
|
7829
|
+
model: picked.model || null,
|
|
7830
|
+
job: picked.job,
|
|
7831
|
+
source: picked.source,
|
|
7832
|
+
reason: picked.reason,
|
|
7833
|
+
};
|
|
7834
|
+
} catch {
|
|
7835
|
+
return null;
|
|
7836
|
+
}
|
|
7837
|
+
}
|
|
7838
|
+
|
|
7790
7839
|
async function runMemberWake(name, { execute = false, confirmed = false, force = false, domainInput = {} } = {}) {
|
|
7791
7840
|
const paths = requireMemberDir(name);
|
|
7792
7841
|
const runtimeKind = paths.runtimeKind || memberRuntimeKind(name);
|
|
@@ -7951,6 +8000,7 @@ async function runMemberWake(name, { execute = false, confirmed = false, force =
|
|
|
7951
8000
|
ok: true,
|
|
7952
8001
|
action: 'wake',
|
|
7953
8002
|
member: name,
|
|
8003
|
+
engine: wakeMemberEngine(name),
|
|
7954
8004
|
mode,
|
|
7955
8005
|
decision,
|
|
7956
8006
|
reason,
|
|
@@ -8073,6 +8123,7 @@ function wakeBootLines(name, result) {
|
|
|
8073
8123
|
` goal ${goal ? clipText(goal, 70) : s.dim('none yet')}`,
|
|
8074
8124
|
...(experiment ? [` working ${clipText(experiment, 70)}`] : []),
|
|
8075
8125
|
...(laneCandidates !== null ? [` lane ${laneCandidates} task${laneCandidates === 1 ? '' : 's'} waiting in ${name}'s lane`] : []),
|
|
8126
|
+
...(result.engine && result.engine.id ? [` engine ${clipText(String(result.engine.reason || '').replace(`${name} does `, ''), 70)}`] : []),
|
|
8076
8127
|
'',
|
|
8077
8128
|
` ${s.bold(name)} looked around and ${s.bold(decisionText)}.`,
|
|
8078
8129
|
` ${s.dim(`Why: ${wakeReasonText(result.reason)}.`)}`,
|