atris 3.38.0 → 3.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/AGENTS.md +25 -6
  2. package/atris/PERSONA.md +8 -4
  3. package/atris.md +7 -0
  4. package/ax +2 -1
  5. package/bin/atris.js +30 -5
  6. package/commands/agent-spawn.js +13 -11
  7. package/commands/autoland.js +28 -77
  8. package/commands/bench.js +10 -12
  9. package/commands/business.js +345 -0
  10. package/commands/chat-scan.js +5 -7
  11. package/commands/codex-goal.js +8 -10
  12. package/commands/console.js +19 -3
  13. package/commands/decide.js +166 -0
  14. package/commands/deck.js +1 -4
  15. package/commands/drill.js +14 -24
  16. package/commands/engine.js +196 -8
  17. package/commands/gm.js +8 -6
  18. package/commands/harvest.js +1 -4
  19. package/commands/init.js +23 -3
  20. package/commands/land.js +8 -14
  21. package/commands/launchpad.js +1 -14
  22. package/commands/lifecycle.js +5 -5
  23. package/commands/log.js +55 -5
  24. package/commands/member.js +558 -574
  25. package/commands/mission.js +456 -237
  26. package/commands/pack.js +2746 -164
  27. package/commands/play.js +6 -4
  28. package/commands/probe.js +2 -2
  29. package/commands/pulse.js +15 -16
  30. package/commands/release.js +10 -9
  31. package/commands/router.js +5 -4
  32. package/commands/site-deploy.js +870 -0
  33. package/commands/site.js +11 -2
  34. package/commands/slop.js +14 -2
  35. package/commands/stream.js +4 -18
  36. package/commands/task.js +880 -558
  37. package/commands/taste.js +101 -0
  38. package/commands/team.js +83 -3
  39. package/commands/vercel.js +4 -2
  40. package/commands/voice.js +195 -0
  41. package/commands/watch.js +1 -22
  42. package/commands/wiki.js +1 -4
  43. package/commands/workflow.js +2 -2
  44. package/commands/worktree.js +1 -14
  45. package/commands/xp.js +27 -24
  46. package/lib/accept-verify-gate.js +5 -1
  47. package/lib/arg-parser.js +41 -0
  48. package/lib/auto-accept-certified.js +116 -1
  49. package/lib/autoland.js +66 -0
  50. package/lib/bench/runner.js +19 -1
  51. package/lib/context-gatherer.js +7 -1
  52. package/lib/engine-registry.js +141 -20
  53. package/lib/falsifier-probe.js +84 -0
  54. package/lib/fleet.js +65 -15
  55. package/lib/git-spawn.js +15 -0
  56. package/lib/json-file.js +37 -0
  57. package/lib/known-commands.js +2 -2
  58. package/lib/lesson-preflight.js +146 -0
  59. package/lib/loop-doctor.js +0 -2
  60. package/lib/mission-human-asks.js +28 -0
  61. package/lib/mission-protected-lane.js +4 -1
  62. package/lib/official-cli-integration.js +47 -2
  63. package/lib/orb-context.js +8 -1
  64. package/lib/pack-capabilities.js +685 -0
  65. package/lib/router-brain.js +51 -1
  66. package/lib/runner-command.js +0 -6
  67. package/lib/self-drive.js +44 -13
  68. package/lib/task-db.js +137 -3
  69. package/lib/task-decision.js +50 -0
  70. package/lib/taste-lessons.js +153 -0
  71. package/lib/tool-result-encode.js +17 -1
  72. package/lib/voice-gate.js +66 -0
  73. package/lib/wish-audit.js +1 -1
  74. package/lib/wish-delegate.js +1 -1
  75. package/lib/zip.js +95 -7
  76. package/package.json +2 -1
  77. package/templates/business-starter/persona.md +9 -0
package/commands/drill.js CHANGED
@@ -3,22 +3,12 @@
3
3
  const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
- const { spawnSync } = require('child_process');
6
+ const { hasFlag } = require('../lib/arg-parser');
7
+ const { runGit } = require('../lib/git-spawn');
7
8
 
8
9
  const VERIFY_COMMAND = 'node -e "process.exit(0)"';
9
10
  const COAUTHOR = 'Co-authored-by: Atris <299057014+atris-builder[bot]@users.noreply.github.com>';
10
11
 
11
- function hasFlag(args, name) {
12
- return args.includes(name);
13
- }
14
-
15
- function runGit(args, cwd, options = {}) {
16
- const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
17
- if (options.check === false || result.status === 0) return result;
18
- const output = (result.stderr || result.stdout || `git ${args.join(' ')} failed`).trim();
19
- throw new Error(output);
20
- }
21
-
22
12
  function parseJsonOutput(text) {
23
13
  const raw = String(text || '').trim();
24
14
  if (!raw) throw new Error('expected JSON output, got empty output');
@@ -97,9 +87,9 @@ async function createSandbox(ctx) {
97
87
  ctx.tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-drill-'));
98
88
  ctx.sandboxPath = path.join(ctx.tmpRoot, 'sandbox');
99
89
  fs.mkdirSync(ctx.sandboxPath, { recursive: true });
100
- runGit(['init', '-b', 'master'], ctx.sandboxPath);
101
- runGit(['config', 'user.email', 'drill@example.invalid'], ctx.sandboxPath);
102
- runGit(['config', 'user.name', 'Atris Drill'], ctx.sandboxPath);
90
+ runGit(['init', '-b', 'master'], { cwd: ctx.sandboxPath });
91
+ runGit(['config', 'user.email', 'drill@example.invalid'], { cwd: ctx.sandboxPath });
92
+ runGit(['config', 'user.name', 'Atris Drill'], { cwd: ctx.sandboxPath });
103
93
  fs.writeFileSync(path.join(ctx.sandboxPath, 'README.md'), '# Atris drill sandbox\n', 'utf8');
104
94
  await withSandboxProcessState(ctx.sandboxPath, {
105
95
  ATRIS_SKIP_UPDATE_CHECK: '1',
@@ -108,8 +98,8 @@ async function createSandbox(ctx) {
108
98
  }, async () => {
109
99
  await callInitAtris(ctx.sandboxPath);
110
100
  });
111
- runGit(['add', '-A'], ctx.sandboxPath);
112
- runGit(['commit', '-m', 'initial sandbox'], ctx.sandboxPath);
101
+ runGit(['add', '-A'], { cwd: ctx.sandboxPath });
102
+ runGit(['commit', '-m', 'initial sandbox'], { cwd: ctx.sandboxPath });
113
103
  }
114
104
 
115
105
  async function wishCaptured(ctx) {
@@ -199,10 +189,10 @@ async function landingShippedLocal(ctx) {
199
189
  try {
200
190
  require('../lib/task-db').close();
201
191
  } catch {}
202
- const dirty = runGit(['status', '--porcelain'], ctx.sandboxPath).stdout.trim();
192
+ const dirty = runGit(['status', '--porcelain'], { cwd: ctx.sandboxPath }).stdout.trim();
203
193
  if (dirty) {
204
- runGit(['add', '-A'], ctx.sandboxPath);
205
- runGit(['commit', '-m', 'record drill pipeline state'], ctx.sandboxPath);
194
+ runGit(['add', '-A'], { cwd: ctx.sandboxPath });
195
+ runGit(['commit', '-m', 'record drill pipeline state'], { cwd: ctx.sandboxPath });
206
196
  }
207
197
  ctx.landingWorktree = path.join(ctx.tmpRoot, 'landing-worktree');
208
198
  const created = worktree.createAgentWorktree({
@@ -229,9 +219,9 @@ async function landingShippedLocal(ctx) {
229
219
  assertOk(shipped.value === 0, `worktree ship exited ${shipped.value}`);
230
220
  assertOk(/local mode/.test(`${shipped.stdout}\n${shipped.stderr}`), 'worktree ship did not report local mode');
231
221
  });
232
- runGit(['worktree', 'remove', ctx.landingWorktree, '--force'], ctx.sandboxPath);
233
- runGit(['branch', '-d', ctx.landingBranch], ctx.sandboxPath);
234
- const landed = runGit(['show', 'master:drill-landing.txt'], ctx.sandboxPath).stdout;
222
+ runGit(['worktree', 'remove', ctx.landingWorktree, '--force'], { cwd: ctx.sandboxPath });
223
+ runGit(['branch', '-d', ctx.landingBranch], { cwd: ctx.sandboxPath });
224
+ const landed = runGit(['show', 'master:drill-landing.txt'], { cwd: ctx.sandboxPath }).stdout;
235
225
  assertOk(landed.trim() === 'local landing ok', 'landing file was not merged into sandbox master');
236
226
  }
237
227
 
@@ -244,7 +234,7 @@ async function ledgerChecked(ctx) {
244
234
  assertOk(currentMission, `mission not found: ${ctx.missionId}`);
245
235
  assertOk(currentMission.verifier_result && currentMission.verifier_result.passed === true, 'mission verifier result is not passing');
246
236
  assertOk(currentMission.receipt_path && fs.existsSync(path.join(ctx.sandboxPath, currentMission.receipt_path)), `mission receipt missing: ${currentMission.receipt_path}`);
247
- const unmerged = runGit(['branch', '--no-merged', 'master'], ctx.sandboxPath).stdout
237
+ const unmerged = runGit(['branch', '--no-merged', 'master'], { cwd: ctx.sandboxPath }).stdout
248
238
  .split(/\r?\n/)
249
239
  .map((line) => line.replace(/^\*/, '').trim())
250
240
  .filter(Boolean)
@@ -26,6 +26,7 @@ const {
26
26
  } = require('../lib/runner-command');
27
27
  const {
28
28
  ENGINE_ROLES,
29
+ ENGINE_DUTIES,
29
30
  ENGINE_HEALTH_STATUSES,
30
31
  binInstalled,
31
32
  canonicalEngineName,
@@ -34,6 +35,9 @@ const {
34
35
  readEngineRegistry,
35
36
  resolveEngineForRole,
36
37
  resolveEngineForRoleRanked,
38
+ requireEngineBin,
39
+ engineDoctorReport,
40
+ setEngineOverrides,
37
41
  setEngineHealth,
38
42
  } = require('../lib/engine-registry');
39
43
  const { FLEET_CAPABLE, runDispatchFlight } = require('../lib/fleet');
@@ -90,14 +94,22 @@ function readSavedEngine(root = process.cwd()) {
90
94
 
91
95
  // The default engine for this workspace, in precedence order:
92
96
  // env (per-run flags land here) -> .atris/engine.json -> house default.
93
- // The house default is our own intelligence when it is installed.
97
+ // When nothing is configured the pick comes from the registry's saved health
98
+ // (seeded once on first sight, refreshed by `atris engine doctor`), never a
99
+ // machine probe: policy over probes. A stale "ready" is the execution stage's
100
+ // problem, where requireEngineBin fails in one plain sentence.
94
101
  function resolveDefaultEngine(root = process.cwd()) {
95
102
  const env = canonicalEngineName(process.env.ATRIS_RUNNER_PROFILE);
96
103
  if (env) return { name: env, source: 'env' };
97
104
  const saved = readSavedEngine(root);
98
105
  if (saved) return { name: saved, source: 'saved' };
99
- if (binInstalled(RUNNER_PROFILE_DEFS[HOUSE_ENGINE].bin)) return { name: HOUSE_ENGINE, source: 'house' };
100
- const fallback = RUNNER_PROFILE_NAMES.find((name) => binInstalled(RUNNER_PROFILE_DEFS[name].bin));
106
+ const ready = new Set(
107
+ engineRegistryView(root)
108
+ .filter((engine) => engine.health && engine.health.status === 'ready')
109
+ .map((engine) => engine.id)
110
+ );
111
+ if (ready.has(HOUSE_ENGINE)) return { name: HOUSE_ENGINE, source: 'house' };
112
+ const fallback = RUNNER_PROFILE_NAMES.find((name) => ready.has(name));
101
113
  return fallback ? { name: fallback, source: 'detected' } : { name: HOUSE_ENGINE, source: 'none' };
102
114
  }
103
115
 
@@ -887,13 +899,16 @@ function printRoster(root) {
887
899
  const found = list.filter((e) => e.installed).length;
888
900
  const current = resolveDefaultEngine(root);
889
901
  console.log('');
890
- console.log(` engines — ${found} intelligence${found === 1 ? '' : 's'} found`);
902
+ console.log(` engines: ${found} intelligence${found === 1 ? '' : 's'} found`);
891
903
  console.log('');
892
904
  for (const engine of list) {
893
905
  const mark = engine.default ? '→' : ' ';
894
906
  const state = engine.health.status === 'not_installed' ? 'not installed' : engine.health.status.replace(/_/g, ' ');
895
907
  const roles = engine.roles.join(',');
896
908
  console.log(` ${mark} ${engine.id.padEnd(12)} ${state.padEnd(13)} ${engine.tier.padEnd(4)} ${roles}`);
909
+ const details = [`models: ${engine.models.join(', ')}`];
910
+ if (engine.duty) details.push(`duty: ${engine.duty}`);
911
+ console.log(` ${details.join(' ')}`);
897
912
  }
898
913
  console.log('');
899
914
  console.log(` default: ${current.name}${current.source === 'saved' ? ' (set here)' : current.source === 'env' ? ' (this session)' : ''}`);
@@ -901,6 +916,139 @@ function printRoster(root) {
901
916
  console.log('');
902
917
  }
903
918
 
919
+ function chartEngineLabel(engine) {
920
+ const id = String(engine && (engine.id || engine.name) || '').trim();
921
+ const models = Array.isArray(engine && engine.models)
922
+ ? engine.models.map((model) => String(model || '').trim()).filter(Boolean)
923
+ : [];
924
+ return models.length ? `${id}: ${models.join(', ')}` : id;
925
+ }
926
+
927
+ function chartBox(title, engines, note = '', emptyLabel = 'unassigned') {
928
+ const labels = engines.length ? engines.map(chartEngineLabel) : (emptyLabel ? [emptyLabel] : []);
929
+ const body = [title, ...labels, ...(note ? [note] : [])];
930
+ const innerWidth = Math.max(...body.map((line) => line.length)) + 2;
931
+ return {
932
+ width: innerWidth + 2,
933
+ lines: [
934
+ `┌${'─'.repeat(innerWidth)}┐`,
935
+ ...body.map((line) => `│ ${line.padEnd(innerWidth - 2)} │`),
936
+ `└${'─'.repeat(innerWidth)}┘`,
937
+ ],
938
+ };
939
+ }
940
+
941
+ function centerChartLine(line, width, center = Math.floor((width - 1) / 2)) {
942
+ const left = Math.max(0, center - Math.floor(line.length / 2));
943
+ return `${' '.repeat(left)}${line}`.padEnd(width);
944
+ }
945
+
946
+ function renderEngineChart(registry) {
947
+ const engines = Array.isArray(registry) ? registry : ((registry && registry.engines) || []);
948
+ const specialDuties = new Set(ENGINE_DUTIES);
949
+ const leaders = engines.filter((entry) => entry.duty === 'leader');
950
+ const rolePool = engines.filter((entry) => !specialDuties.has(entry.duty));
951
+ const builders = rolePool.filter((entry) => Array.isArray(entry.roles) && entry.roles.includes('executor'));
952
+ const checkers = rolePool.filter((entry) => Array.isArray(entry.roles) && entry.roles.includes('validator'));
953
+ const errands = engines.filter((entry) => entry.duty === 'errands');
954
+ const apprentices = engines.filter((entry) => entry.duty === 'learning');
955
+
956
+ const ownerBox = chartBox('owner', [], '', '');
957
+ const leaderBox = chartBox('leader', leaders);
958
+ const columns = [
959
+ chartBox('builders', builders),
960
+ chartBox('checkers', checkers),
961
+ chartBox('errands', errands),
962
+ ];
963
+ const gap = 3;
964
+ const totalWidth = columns.reduce((sum, column) => sum + column.width, 0) + (gap * (columns.length - 1));
965
+ const columnHeight = Math.max(...columns.map((column) => column.lines.length));
966
+ const columnLines = [];
967
+ for (let row = 0; row < columnHeight; row += 1) {
968
+ columnLines.push(columns.map((column) => (column.lines[row] || '').padEnd(column.width)).join(' '.repeat(gap)));
969
+ }
970
+
971
+ const centers = [];
972
+ let offset = 0;
973
+ for (const column of columns) {
974
+ centers.push(offset + Math.floor(column.width / 2));
975
+ offset += column.width + gap;
976
+ }
977
+ const fleetCenter = centers[1];
978
+ const branch = Array(totalWidth).fill(' ');
979
+ for (let i = centers[0]; i <= centers[centers.length - 1]; i += 1) branch[i] = '─';
980
+ branch[centers[0]] = '┌';
981
+ branch[centers[centers.length - 1]] = '┐';
982
+ for (const center of centers.slice(1, -1)) branch[center] = '┬';
983
+ branch[fleetCenter] = centers.includes(fleetCenter) ? '┼' : '┴';
984
+ const stems = Array(totalWidth).fill(' ');
985
+ for (const center of centers) stems[center] = '│';
986
+
987
+ const lines = [
988
+ ...ownerBox.lines.map((line) => centerChartLine(line, totalWidth, fleetCenter)),
989
+ centerChartLine('│', totalWidth, fleetCenter),
990
+ ...leaderBox.lines.map((line) => centerChartLine(line, totalWidth, fleetCenter)),
991
+ centerChartLine('│', totalWidth, fleetCenter),
992
+ branch.join(''),
993
+ stems.join(''),
994
+ ...columnLines,
995
+ ];
996
+ if (apprentices.length) {
997
+ const apprenticeBox = chartBox('apprentice', apprentices, 'learning the system');
998
+ lines.push(centerChartLine('│', totalWidth, fleetCenter));
999
+ lines.push(...apprenticeBox.lines.map((line) => centerChartLine(line, totalWidth, fleetCenter)));
1000
+ }
1001
+ return lines.map((line) => line.replace(/\s+$/, '')).join('\n');
1002
+ }
1003
+
1004
+ function printEngineChart(root) {
1005
+ console.log(renderEngineChart(readEngineRegistry(root)));
1006
+ }
1007
+
1008
+ function flagValue(args, flag) {
1009
+ const equals = `${flag}=`;
1010
+ for (let i = 0; i < args.length; i += 1) {
1011
+ const value = String(args[i] || '');
1012
+ if (value === flag) return { present: true, value: args[i + 1] || '' };
1013
+ if (value.startsWith(equals)) return { present: true, value: value.slice(equals.length) };
1014
+ }
1015
+ return { present: false, value: '' };
1016
+ }
1017
+
1018
+ function runSetEngineCommand(args, root) {
1019
+ const name = String(args[0] || '').trim();
1020
+ const dutyFlag = flagValue(args.slice(1), '--duty');
1021
+ const modelsFlag = flagValue(args.slice(1), '--models');
1022
+ const duty = String(dutyFlag.value || '').trim();
1023
+ const models = String(modelsFlag.value || '').split(',').map((model) => model.trim()).filter(Boolean);
1024
+ if (!name || (!dutyFlag.present && !modelsFlag.present)) {
1025
+ console.error('usage: atris engine set <name> [--duty leader|errands|learning] [--models "a, b"]');
1026
+ return 2;
1027
+ }
1028
+ if (dutyFlag.present && !ENGINE_DUTIES.includes(duty)) {
1029
+ console.error(`unknown duty "${duty}". known duties: ${ENGINE_DUTIES.join(', ')}`);
1030
+ return 2;
1031
+ }
1032
+ if (modelsFlag.present && !models.length) {
1033
+ console.error('models must include at least one name');
1034
+ return 2;
1035
+ }
1036
+ try {
1037
+ const updated = setEngineOverrides(name, {
1038
+ ...(dutyFlag.present ? { duty } : {}),
1039
+ ...(modelsFlag.present ? { models } : {}),
1040
+ }, root);
1041
+ const details = [];
1042
+ if (updated.duty) details.push(`duty ${updated.duty}`);
1043
+ if (updated.models) details.push(`models ${updated.models.join(', ')}`);
1044
+ console.log(`${updated.id} updated: ${details.join('; ')}`);
1045
+ return 0;
1046
+ } catch (err) {
1047
+ console.error(String(err.message || err).replace(/^Unknown/, 'unknown'));
1048
+ return 2;
1049
+ }
1050
+ }
1051
+
904
1052
  function registryPayload(root) {
905
1053
  const current = resolveDefaultEngine(root);
906
1054
  const registry = readEngineRegistry(root);
@@ -956,6 +1104,28 @@ function runResolveCommand(args, root) {
956
1104
  return 0;
957
1105
  }
958
1106
 
1107
+ // Doctor is the one opt-in place that probes the machine: it checks every
1108
+ // engine binary, reports installed state, and folds ready/not_installed flips
1109
+ // back into the policy file. Routing itself never probes.
1110
+ function runDoctorCommand(args, root) {
1111
+ const json = args.includes('--json');
1112
+ const engines = engineDoctorReport(root);
1113
+ if (json) {
1114
+ console.log(JSON.stringify({ ok: true, engines }, null, 2));
1115
+ return 0;
1116
+ }
1117
+ console.log('');
1118
+ for (const engine of engines) {
1119
+ const state = engine.installed ? 'installed' : 'missing';
1120
+ const health = engine.health.status.replace(/_/g, ' ');
1121
+ console.log(` ${engine.id.padEnd(12)} ${String(engine.bin).padEnd(14)} ${state.padEnd(10)} health: ${health}`);
1122
+ }
1123
+ console.log('');
1124
+ console.log(' probed just now and saved to the registry; routing reads that saved policy, never the machine.');
1125
+ console.log('');
1126
+ return 0;
1127
+ }
1128
+
959
1129
  function runHealthCommand(args, root) {
960
1130
  const json = args.includes('--json');
961
1131
  const positional = args.filter((a) => !String(a).startsWith('--'));
@@ -1203,9 +1373,12 @@ function runDispatchCommand(args, root) {
1203
1373
  return 2;
1204
1374
  }
1205
1375
  }
1206
- const def = RUNNER_PROFILE_DEFS[canonical];
1207
- if (!binInstalled(def.bin)) {
1208
- console.error(`engine dispatch: ${canonical} CLI (${def.bin}) is not installed here`);
1376
+ // Execution stage: this is where the binary must exist. Routing above never
1377
+ // probed the machine; a missing CLI fails loudly here instead.
1378
+ try {
1379
+ requireEngineBin(canonical);
1380
+ } catch (err) {
1381
+ console.error(`engine dispatch: ${err.message}`);
1209
1382
  return 2;
1210
1383
  }
1211
1384
  return runDispatchFlight({ root, taskIds, engine: canonical, prompt: promptOverride, yolo, ...(base ? { checkoutBase: base } : {}) }).then((flight) => {
@@ -1248,6 +1421,19 @@ function engineCommand(args = [], deps = {}) {
1248
1421
  return runHealthCommand(args.slice(args.indexOf('health') + 1), root);
1249
1422
  }
1250
1423
 
1424
+ if (sub === 'doctor') {
1425
+ return runDoctorCommand(args.slice(args.indexOf('doctor') + 1), root);
1426
+ }
1427
+
1428
+ if (sub === 'set') {
1429
+ return runSetEngineCommand(args.slice(args.indexOf('set') + 1), root);
1430
+ }
1431
+
1432
+ if (sub === 'chart' || args.includes('--chart')) {
1433
+ printEngineChart(root);
1434
+ return 0;
1435
+ }
1436
+
1251
1437
  if (!sub || sub === 'list' || sub === 'status') {
1252
1438
  if (json) {
1253
1439
  console.log(JSON.stringify(registryPayload(root), null, 2));
@@ -1266,7 +1452,7 @@ function engineCommand(args = [], deps = {}) {
1266
1452
  }
1267
1453
 
1268
1454
  if (sub === 'help') {
1269
- console.log('\n atris engine roster + current default\n atris engine list --json full registry: default + engines with tier, roles, fallback, health\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 <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\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');
1455
+ console.log('\n atris engine roster + current default\n atris engines --chart show the fleet as an org chart\n atris engine list --json full registry: default + engines with tier, roles, fallback, health\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 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');
1270
1456
  return 0;
1271
1457
  }
1272
1458
 
@@ -1304,6 +1490,8 @@ module.exports = {
1304
1490
  readEngineRegistry,
1305
1491
  resolveEngineForRole,
1306
1492
  setEngineHealth,
1493
+ setEngineOverrides,
1494
+ renderEngineChart,
1307
1495
  parseDispatchArgs,
1308
1496
  runDispatchCommand,
1309
1497
  ENGINE_LOGIN_MANIFESTS,
package/commands/gm.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
+ const { hasFlag: hasExactFlag } = require('../lib/arg-parser');
6
7
 
7
8
  const AGENTXP_LEADERBOARD_URL = 'https://api.atris.ai/api/agentxp/leaderboard';
8
9
  const AGENTXP_GLOBAL_SYNC_RULE = 'Run atris login, then sync. Owner-provided sync tokens are guided-demo fallback only.';
@@ -54,8 +55,9 @@ function flag(args, name) {
54
55
  return null;
55
56
  }
56
57
 
57
- function hasFlag(args, name) {
58
- return args.includes(name) || args.some(arg => arg.startsWith(`${name}=`));
58
+ // This command treats --name=value as flag presence as well as a standalone flag.
59
+ function hasFlagOrValue(args, name) {
60
+ return hasExactFlag(args, name) || args.some(arg => arg.startsWith(`${name}=`));
59
61
  }
60
62
 
61
63
  function positional(args) {
@@ -185,7 +187,7 @@ function pickSeedPlayer(workspaceRoot, tasks, args = []) {
185
187
  }
186
188
 
187
189
  function ensureStarterMission(taskDb, db, workspaceRoot, tasks, args = []) {
188
- if (hasFlag(args, '--no-seed')) return { tasks, seeded: null };
190
+ if (hasFlagOrValue(args, '--no-seed')) return { tasks, seeded: null };
189
191
  if (!fs.existsSync(path.join(workspaceRoot, 'atris'))) return { tasks, seeded: null };
190
192
  if (activeAgentXpTasks(tasks).length) return { tasks, seeded: null };
191
193
 
@@ -658,7 +660,7 @@ async function gmCommand(...args) {
658
660
  return memberCommand('wake', member, ...passthrough);
659
661
  }
660
662
 
661
- if (!hasFlag(args, '--json') && !hasFlag(args, '--watch')) {
663
+ if (!hasFlagOrValue(args, '--json') && !hasFlagOrValue(args, '--watch')) {
662
664
  try {
663
665
  const { gameCommand } = require('./game');
664
666
  const dashboardShown = await gameCommand([], { silentMissing: true });
@@ -669,12 +671,12 @@ async function gmCommand(...args) {
669
671
  }
670
672
 
671
673
  const state = gmState(args);
672
- if (hasFlag(args, '--json')) {
674
+ if (hasFlagOrValue(args, '--json')) {
673
675
  console.log(JSON.stringify(state, null, 2));
674
676
  return;
675
677
  }
676
678
  render(state);
677
- if (hasFlag(args, '--watch')) {
679
+ if (hasFlagOrValue(args, '--watch')) {
678
680
  await gmWatch(args);
679
681
  }
680
682
  }
@@ -1,14 +1,11 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const { hasFlag } = require('../lib/arg-parser');
3
4
  const {
4
5
  isGenericInboxPlaceholder,
5
6
  seedInboxFromMove,
6
7
  } = require('../lib/next-moves');
7
8
 
8
- function hasFlag(args, name) {
9
- return args.includes(name);
10
- }
11
-
12
9
  function safeRead(file) {
13
10
  try { return fs.readFileSync(file, 'utf8'); } catch { return ''; }
14
11
  }
package/commands/init.js CHANGED
@@ -282,6 +282,7 @@ function initAtris() {
282
282
  const printReadyCounts = () => {
283
283
  console.log(`workspace files ready (${ready.workspace.size})`);
284
284
  console.log(`team members ready (${ready.team.size})`);
285
+ console.log('more members are available with atris member create <name>');
285
286
  console.log(`agent adapters ready (${ready.adapters.size})`);
286
287
  console.log(`skills installed (${ready.skills.size})`);
287
288
  };
@@ -601,8 +602,8 @@ function initAtris() {
601
602
 
602
603
 
603
604
  // Copy team members (MEMBER.md format — directory per member with skills/tools/context)
604
- const members = ['navigator', 'executor', 'validator', 'launcher', 'brainstormer', 'researcher'];
605
- members.forEach(name => {
605
+ const starterMembers = ['navigator', 'executor', 'validator', 'mission-lead', 'improver'];
606
+ starterMembers.forEach(name => {
606
607
  const sourceFile = path.join(__dirname, '..', 'atris', 'team', name, 'MEMBER.md');
607
608
  const targetMemberDir = path.join(teamDir, name);
608
609
  const targetFile = path.join(targetMemberDir, 'MEMBER.md');
@@ -644,7 +645,7 @@ tools: []
644
645
  # Insert persona, workflow, and rules below
645
646
  `;
646
647
  fs.writeFileSync(path.join(templateTargetDir, 'MEMBER.md'), templateContent);
647
- markReady('team', '_template', '✓ Created team/_template/MEMBER.md');
648
+ markReady('workspace', 'team/_template/MEMBER.md', '✓ Created team/_template/MEMBER.md');
648
649
  }
649
650
 
650
651
  // Detect project context and generate profile
@@ -676,6 +677,25 @@ atris atris.md
676
677
 
677
678
  This is the Atris boot sequence. Show the output to the user, then respond naturally.
678
679
 
680
+ ## The way of talking (every reply, no exceptions)
681
+
682
+ Talk like a person. This governs every message you send the operator: boot, status, summaries, everything.
683
+
684
+ - Plain words. Say what happened and what it means for the reader, cause and effect. Not the machinery.
685
+ - No task codes, branch names, commit hashes, PR numbers, or system nouns (worktree, verifier, projection, tick) in the message body. If the reader needs a command, put ONE copyable line at the end.
686
+ - No insider terms without defining them in the same breath. A sharp non-engineer must follow it on first read.
687
+ - Lead with the outcome. Complete sentences, one or two per paragraph, blank line between. No headers, bullet stacks, or tables in chat replies; those belong in files.
688
+ - Detail lives in files and receipts. Offer "want the detail?" instead of dumping it.
689
+ - No em dashes anywhere; use a comma, colon, or period.
690
+ - The test before sending: read it fried at 2am. If decoding takes work, rewrite it.
691
+
692
+ copy these shapes:
693
+ - **Status:** "The reply check is built. I am running the final checks now, so the result is not ready yet."
694
+ - **Landing:** "Replies now get a plain-language check before they reach you. The checks passed, and the change is ready."
695
+ - **Failure:** "The plain-language check could not run because its model was unavailable. Your reply still went through, so no work was blocked."
696
+
697
+ Canonical source: \`atris/atris.md\` \`## voice\`. This block is a copy for tools that only read \`AGENTS.md\`; if they differ, atris.md wins.
698
+
679
699
  ## Core Files
680
700
 
681
701
  Atris is the source of truth. This file is only an adapter for tools that read
package/commands/land.js CHANGED
@@ -1,7 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { spawnSync } = require('child_process');
4
-
3
+ const { runGit } = require('../lib/git-spawn');
5
4
  const { listWorktrees, statusCounts } = require('./worktree');
6
5
 
7
6
  const DEFAULT_TTL_DAYS = 7;
@@ -14,14 +13,6 @@ function countLabel(n, word) {
14
13
  return `${n} ${word}${n === 1 ? '' : 's'}`;
15
14
  }
16
15
 
17
- function runGit(args, { cwd = process.cwd(), check = true, maxBuffer } = {}) {
18
- const result = spawnSync('git', args, { cwd, encoding: 'utf8', ...(maxBuffer ? { maxBuffer } : {}) });
19
- if (check && result.status !== 0) {
20
- throw new Error(`git ${args.join(' ')} failed: ${(result.stderr || result.stdout || '').trim()}`);
21
- }
22
- return result;
23
- }
24
-
25
16
  function repoRoot(cwd = process.cwd()) {
26
17
  const result = runGit(['rev-parse', '--show-toplevel'], { cwd, check: false });
27
18
  if (result.status !== 0) return '';
@@ -614,7 +605,9 @@ function printReceipt(receipt) {
614
605
  // fresh-grace keeps are healthy active work, not a call to action —
615
606
  // "needs a human" on those trained readers to ignore the real ones.
616
607
  for (const k of receipt.keptWorktrees || []) {
608
+ // The receipt keeps the raw codes; the human line speaks in sentences.
617
609
  if (String(k).includes('(fresh_worktree_grace)')) console.log(` in use, left alone: ${k.replace(' (fresh_worktree_grace)', '')}`);
610
+ else if (String(k).includes('(current_checkout)')) console.log(` you are standing in it, left alone: ${k.replace(' (current_checkout)', '')}`);
618
611
  else console.log(` ✋ kept, needs a human: ${k}`);
619
612
  }
620
613
  for (const m of receipt.keptMovedBranches || []) console.log(` moved since scan, left alone: ${m}`);
@@ -624,7 +617,8 @@ function printReceipt(receipt) {
624
617
  console.log('');
625
618
  }
626
619
 
627
- function readFlag(args, name, fallback = '') {
620
+ // Preserve split-only parsing and its fallback for missing or falsey values.
621
+ function readFollowingFlag(args, name, fallback = '') {
628
622
  const idx = args.indexOf(name);
629
623
  if (idx === -1) return fallback;
630
624
  return args[idx + 1] || fallback;
@@ -665,10 +659,10 @@ function landCommand(args = []) {
665
659
  console.error('not a git repository');
666
660
  return 1;
667
661
  }
668
- const ttlRaw = readFlag(args, '--ttl', '');
662
+ const ttlRaw = readFollowingFlag(args, '--ttl', '');
669
663
  const ttlParsed = Number(ttlRaw);
670
664
  const ttlDays = ttlRaw !== '' && Number.isFinite(ttlParsed) && ttlParsed >= 0 ? ttlParsed : DEFAULT_TTL_DAYS;
671
- const base = readFlag(args, '--base', '');
665
+ const base = readFollowingFlag(args, '--base', '');
672
666
  const json = args.includes('--json');
673
667
 
674
668
  if (args.includes('--reap')) {
@@ -679,7 +673,7 @@ function landCommand(args = []) {
679
673
  }
680
674
 
681
675
  const lookupArgs = args[0] === 'status' ? args.slice(1) : args;
682
- const name = lookupArgs.find((a) => !a.startsWith('-') && a !== readFlag(lookupArgs, '--ttl') && a !== readFlag(lookupArgs, '--base'));
676
+ const name = lookupArgs.find((a) => !a.startsWith('-') && a !== readFollowingFlag(lookupArgs, '--ttl') && a !== readFollowingFlag(lookupArgs, '--base'));
683
677
  if (name) {
684
678
  const story = collectStory(root, name, { base });
685
679
  if (!story) {
@@ -2,23 +2,10 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const { hasFlag, readFlag } = require('../lib/arg-parser');
5
6
 
6
7
  const ACTIVE_MISSION_STATUSES = new Set(['planning', 'running', 'ready']);
7
8
 
8
- function hasFlag(args, name) {
9
- return args.includes(name);
10
- }
11
-
12
- function readFlag(args, name, fallback = '') {
13
- const prefix = `${name}=`;
14
- for (let i = 0; i < args.length; i += 1) {
15
- const arg = String(args[i]);
16
- if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) return String(args[i + 1]);
17
- if (arg.startsWith(prefix)) return arg.slice(prefix.length);
18
- }
19
- return fallback;
20
- }
21
-
22
9
  function readJson(file) {
23
10
  try {
24
11
  return JSON.parse(fs.readFileSync(file, 'utf8'));
@@ -168,7 +168,7 @@ async function sleepAtris() {
168
168
  }
169
169
 
170
170
  /**
171
- * Wake a sleeping workspace so agents resume automatically.
171
+ * Wake a sleeping workspace: remote wake request, or local wake flag.
172
172
  * @returns {Promise<void>}
173
173
  */
174
174
  async function wakeAtris() {
@@ -179,7 +179,7 @@ async function wakeAtris() {
179
179
  if (!slug || slug === '--help' || slug === '-h' || slug === 'help') {
180
180
  console.log('Usage: atris wake [business|member] [--loop <loop-id>]');
181
181
  console.log('');
182
- console.log(' Wake a sleeping workspace. Agents resume automatically.');
182
+ console.log(' Wake a sleeping workspace. Wake signal sent; loops pick it up on their next pass.');
183
183
  console.log(' Or flip a local member / member-loop switch awake.');
184
184
  process.exit(0);
185
185
  }
@@ -207,13 +207,13 @@ async function wakeAtris() {
207
207
  console.log(`Waking business computer '${business.name || slug}'...`);
208
208
 
209
209
  if (result.data && result.data.status === 'running' && result.data.endpoint) {
210
- console.log(`Business computer '${business.name || slug}' is alive. Agents resuming.`);
210
+ console.log(`Business computer '${business.name || slug}' is alive. Wake signal sent; loops pick it up on their next pass.`);
211
211
  return;
212
212
  }
213
213
 
214
214
  const running = await waitForBusinessComputer(creds.token, business);
215
215
  if (running) {
216
- console.log(`Business computer '${business.name || slug}' is alive. Agents resuming.`);
216
+ console.log(`Business computer '${business.name || slug}' is alive. Wake signal sent; loops pick it up on their next pass.`);
217
217
  return;
218
218
  }
219
219
 
@@ -246,7 +246,7 @@ async function wakeAtris() {
246
246
  });
247
247
 
248
248
  if (status.ok && status.data && status.data.status === 'running') {
249
- console.log(`Workspace '${slug}' is alive. Agents resuming.`);
249
+ console.log(`Workspace '${slug}' is alive. Wake signal sent; loops pick it up on their next pass.`);
250
250
  return;
251
251
  }
252
252
  }