atris 3.31.0 → 3.33.3

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/bin/atris.js CHANGED
@@ -127,8 +127,30 @@ function isOptionValue(args, index, optionNames) {
127
127
  }
128
128
 
129
129
  function applyRunnerFlags(args) {
130
+ // --engine <name> is the operator-facing spelling of --runner-profile:
131
+ // one flag rents a specific intelligence for this run.
132
+ const engineFlag = readOptionArg(args, '--engine');
133
+ if (engineFlag) {
134
+ const { canonicalEngineName } = require('../commands/engine');
135
+ const { RUNNER_PROFILE_NAMES } = require('../lib/runner-command');
136
+ const canonical = canonicalEngineName(engineFlag);
137
+ if (!canonical) {
138
+ console.error(`Unknown --engine "${engineFlag}". Known engines: ${RUNNER_PROFILE_NAMES.join(', ')}.`);
139
+ process.exit(1);
140
+ }
141
+ process.env.ATRIS_RUNNER_PROFILE = canonical;
142
+ }
130
143
  const runnerProfile = readOptionArg(args, '--runner-profile');
131
- if (runnerProfile) process.env.ATRIS_RUNNER_PROFILE = runnerProfile;
144
+ if (runnerProfile) {
145
+ // Fail fast at the CLI boundary: an unknown profile otherwise stays silent
146
+ // until a heartbeat spawn resolves it mid-loop (silent overnight outage).
147
+ const { RUNNER_PROFILES, RUNNER_PROFILE_NAMES } = require('../lib/runner-command');
148
+ if (!Object.prototype.hasOwnProperty.call(RUNNER_PROFILES, runnerProfile)) {
149
+ console.error(`Unknown --runner-profile "${runnerProfile}". Known profiles: ${RUNNER_PROFILE_NAMES.join(', ')}.`);
150
+ process.exit(1);
151
+ }
152
+ process.env.ATRIS_RUNNER_PROFILE = runnerProfile;
153
+ }
132
154
  const runnerBin = readOptionArg(args, '--runner-bin');
133
155
  if (runnerBin) {
134
156
  process.env.ATRIS_RUNNER_BIN = runnerBin;
@@ -144,8 +166,30 @@ function applyRunnerFlags(args) {
144
166
  process.env.ATRIS_RUNNER_MODEL = runnerModel;
145
167
  process.env.ATRIS_CLAUDE_MODEL = runnerModel;
146
168
  }
169
+ // No per-run choice and no env: the workspace's saved engine
170
+ // (.atris/engine.json, written by `atris engine <name>`) becomes the
171
+ // profile for every loop spawn. For the loop commands themselves, fall all
172
+ // the way to the house engine (atris-fast when ax is installed) so the
173
+ // default intelligence is our own. Heartbeats stay engine-agnostic without
174
+ // any loop code knowing about engines.
175
+ if (!process.env.ATRIS_RUNNER_PROFILE) {
176
+ try {
177
+ const engine = require('../commands/engine');
178
+ const saved = engine.readSavedEngine();
179
+ if (saved) {
180
+ process.env.ATRIS_RUNNER_PROFILE = saved;
181
+ } else if (RUNNER_SPAWNING_COMMANDS.includes(command)) {
182
+ const resolved = engine.resolveDefaultEngine();
183
+ if (resolved.source === 'house') process.env.ATRIS_RUNNER_PROFILE = resolved.name;
184
+ }
185
+ } catch {}
186
+ }
147
187
  }
148
188
 
189
+ // Commands whose ticks spawn a worker engine; only these pay the engine
190
+ // detection probe when nothing is configured.
191
+ const RUNNER_SPAWNING_COMMANDS = ['run', 'autopilot', 'mission', 'pulse', 'gm', 'spaceship'];
192
+
149
193
  const isBusinessSyncSafetyCommand = command === 'sync'
150
194
  && (
151
195
  commandArgs.includes('--status')
@@ -420,11 +464,13 @@ function showHelp() {
420
464
  console.log('Quick Start:');
421
465
  console.log('');
422
466
  console.log(' 1. atris Open a persistent AI computer for this workspace');
467
+ console.log(' npx atris Same command after a local project install');
423
468
  console.log(' 2. Describe what you want run, built, researched, or validated');
424
469
  console.log(' 3. Atris acts with context, memory, tools, and a review loop');
425
470
  console.log('');
426
471
  console.log('Common invocations:');
427
- console.log(' atris init');
472
+ console.log(' atris init Global install: initialize this project');
473
+ console.log(' npx atris init Local install: initialize this project');
428
474
  console.log(' atris computer');
429
475
  console.log(' atris business init "My Company"');
430
476
  console.log(' atris run');
@@ -449,7 +495,7 @@ function showHelp() {
449
495
  console.log(' plan - Create build spec with visualization');
450
496
  console.log(' do - Execute tasks');
451
497
  console.log(' review - Validate work (tests, safety checks, docs)');
452
- console.log(' run - Auto-chain plan→do→review (autonomous loop, auto-pushes)');
498
+ console.log(' run - One bounded mission pursuit: start or resume, tick, complete');
453
499
  console.log(' run logs - Browse glass run logs (phase reasoning persisted to disk)');
454
500
  console.log(' run search - Search phase reasoning across all run logs');
455
501
  console.log(' pulse - Durable overnight self-improvement heartbeat (OS cron, install/status/tick)');
@@ -483,11 +529,13 @@ function showHelp() {
483
529
  console.log('');
484
530
  console.log('Optional helpers:');
485
531
  console.log(' brainstorm - Explore ideas conversationally before planning');
486
- console.log(' autopilot - Guided loop that can clarify TODOs and run plan → do → review');
532
+ console.log(' autopilot - Keep the workspace moving: mission/member loop until you stop it');
487
533
  console.log(' improve - Run one paid RL tick (POST /api/improve, deducts credits)');
488
534
  console.log(' worktree - Isolated Git worktrees plus guarded ship/merge for parallel agents');
489
535
  console.log(' land - The landing: what is actually done vs still in the air; --reap backs up + clears overdue');
490
536
  console.log(' autoland - Approve the policy once; certified work lands itself, you keep irreversible calls');
537
+ console.log(' engine - Bring any intelligence: roster of installed coding CLIs, default engine, --engine per run, `engine test` preflight');
538
+ console.log(' sign - Co-author trailer on every commit in an atris workspace (on/off/status)');
491
539
  console.log(' visualize - Generate a Slack/deck-ready visual from a prompt');
492
540
  console.log(' youtube - Process YouTube videos with timestamped transcript-first analysis');
493
541
  console.log('');
@@ -547,7 +595,7 @@ function showHelp() {
547
595
  console.log(' soul - Show, snapshot, or fork workspace identity');
548
596
  console.log(' fleet - Inspect local fleet status');
549
597
  console.log(' agent - Select cloud agent, spawn worker requests, or run `agent doctor`');
550
- console.log(' chat - Chat with the selected Atris agent (or: atris chat scan)');
598
+ console.log(' chat - Chat with Atris 2 Fast in this workspace (--agent for cloud agent; or: atris chat scan)');
551
599
  console.log(' fast - Chat with Atris2 Fast');
552
600
  console.log(' login - Sign in or add another account');
553
601
  console.log(' logout - Sign out of current account');
@@ -557,7 +605,7 @@ function showHelp() {
557
605
  console.log(' accounts - Manage accounts (list, add, remove)');
558
606
  console.log('');
559
607
  console.log('Integrations:');
560
- console.log(' gmail - Email commands (inbox, read)');
608
+ console.log(' gmail - Email commands (inbox, read, archive)');
561
609
  console.log(' calendar - Calendar commands (today, week)');
562
610
  console.log(' twitter - Twitter commands (post)');
563
611
  console.log(' slack - Slack commands (channels)');
@@ -885,7 +933,7 @@ function showAutopilotHelp() {
885
933
  console.log(' --runner-bin PATH Runner binary for this run');
886
934
  console.log(' --runner-template CMD Runner command template for this run');
887
935
  console.log(' --runner-model MODEL Runner model for this run');
888
- console.log(' --runner-profile NAME Runner profile for this run (e.g. atris-fast)');
936
+ console.log(` --runner-profile NAME Runner profile for this run (one of: ${require('../lib/runner-command').RUNNER_PROFILE_NAMES.join(', ')})`);
889
937
  console.log('');
890
938
  console.log('Examples:');
891
939
  console.log(' atris autopilot # Suggest from existing work');
@@ -927,7 +975,7 @@ const knownCommands = ['init', 'log', 'now', 'radar', 'ctop', 'launchpad', 'stat
927
975
  'clean', 'harvest', 'verify', 'search', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'plugin', 'experiments', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube',
928
976
  'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'probe', 'worktree', 'land', 'autoland', 'aeo', 'slop', 'strings', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'xp', 'play', 'gm', 'x', 'recap', 'signup', 'clarity', 'moves',
929
977
  'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
930
- 'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'compile', 'spaceship', 'truth'];
978
+ 'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines'];
931
979
 
932
980
  // Check if command is an atris.md spec file - triggers welcome visualization
933
981
  function isSpecFile(cmd) {
@@ -1049,7 +1097,11 @@ async function interactiveEntry(userInput) {
1049
1097
 
1050
1098
  // Fresh install - offer init
1051
1099
  if (state.state === 'fresh') {
1052
- console.log('\nNo atris/ folder found. Run: atris init');
1100
+ console.log('\nNo atris/ folder found.');
1101
+ console.log('');
1102
+ console.log('Start here:');
1103
+ console.log(' atris init if Atris is installed globally');
1104
+ console.log(' npx atris init if Atris was installed in this project');
1053
1105
  return;
1054
1106
  }
1055
1107
 
@@ -1831,23 +1883,26 @@ if (command === 'init') {
1831
1883
  }
1832
1884
  if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
1833
1885
  console.log('');
1834
- console.log('Usage: atris run [options]');
1886
+ console.log('Usage: atris run ["objective"] [options]');
1835
1887
  console.log('');
1836
- console.log('Auto-chain plan do review cycles autonomously.');
1837
- console.log('Reads inbox ideas, creates tasks, builds them, validates, repeats.');
1888
+ console.log('One bounded mission pursuit: start a mission from the objective');
1889
+ console.log('(or resume the most logical runnable mission), tick it through the');
1890
+ console.log('mission runtime, complete on pass, then exit.');
1891
+ console.log('For an ongoing loop, use: atris autopilot');
1838
1892
  console.log('');
1839
1893
  console.log('Options:');
1840
- console.log(' --cycles=N Max cycles (default: 5)');
1841
- console.log(' --once Single plan→do→review cycle');
1842
- console.log(' --verbose Show configured runner output');
1843
- console.log(' --dry-run Preview without executing');
1844
- console.log(' --timeout=N Phase timeout in seconds (default: 600)');
1845
- console.log(' --runner-bin PATH Runner binary for this run');
1846
- console.log(' --runner-template CMD Runner command template for this run');
1847
- console.log(' --runner-model MODEL Runner model for this run');
1848
- console.log(' --runner-profile NAME Runner profile for this run (e.g. atris-fast)');
1849
- console.log(' --push Auto-push after each cycle (default: true)');
1850
- console.log(' --no-push Skip auto-push after each cycle');
1894
+ console.log(' --owner NAME Mission owner (default: mission-lead)');
1895
+ console.log(' --minutes N | --hours N Time budget for the pursuit');
1896
+ console.log(' --max-ticks N Override the tick budget');
1897
+ console.log(' --max-wall N Override the wall clock in seconds');
1898
+ console.log(' --cadence C Mission cadence (e.g. 15m)');
1899
+ console.log(' --no-complete Skip auto-complete after a passing run');
1900
+ console.log(' --legacy Old plan→do→review loop (claude -p cycles)');
1901
+ console.log('');
1902
+ console.log('Legacy options (with --legacy):');
1903
+ console.log(' --cycles=N --once --verbose --dry-run --timeout=N --no-push');
1904
+ console.log(' --runner-bin PATH / --runner-template CMD / --runner-model MODEL');
1905
+ console.log(` --runner-profile NAME Runner profile for this run (one of: ${require('../lib/runner-command').RUNNER_PROFILE_NAMES.join(', ')})`);
1851
1906
  console.log('');
1852
1907
  console.log('Subcommands:');
1853
1908
  console.log(' atris run logs [--tail N] [--cat FILE] [--json] Browse glass run logs');
@@ -1860,62 +1915,81 @@ if (command === 'init') {
1860
1915
  process.exit(0);
1861
1916
  }
1862
1917
 
1863
- const verbose = args.includes('--verbose') || args.includes('-v');
1864
- const dryRun = args.includes('--dry-run');
1865
- const once = args.includes('--once');
1866
- const push = !args.includes('--no-push');
1867
1918
  applyRunnerFlags(args);
1868
- const cyclesArg = args.find(a => a.startsWith('--cycles='));
1869
- const maxCycles = cyclesArg ? parseInt(cyclesArg.split('=')[1]) : 5;
1870
- const timeoutArg = args.find(a => a.startsWith('--timeout='));
1871
- const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1]) * 1000 : undefined;
1872
1919
 
1873
- require('../commands/run').runAtris({ maxCycles, verbose, dryRun, once, push, timeout })
1874
- .then(() => process.exit(0))
1875
- .catch((error) => {
1876
- console.error(`\u2717 Run failed: ${error.message || error}`);
1877
- process.exit(1);
1878
- });
1920
+ if (args.includes('--legacy')) {
1921
+ const legacyArgs = args.filter(a => a !== '--legacy');
1922
+ const verbose = legacyArgs.includes('--verbose') || legacyArgs.includes('-v');
1923
+ const dryRun = legacyArgs.includes('--dry-run');
1924
+ const once = legacyArgs.includes('--once');
1925
+ const push = !legacyArgs.includes('--no-push');
1926
+ const cyclesArg = legacyArgs.find(a => a.startsWith('--cycles='));
1927
+ const maxCycles = cyclesArg ? parseInt(cyclesArg.split('=')[1]) : 5;
1928
+ const timeoutArg = legacyArgs.find(a => a.startsWith('--timeout='));
1929
+ const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1]) * 1000 : undefined;
1930
+
1931
+ require('../commands/run').runAtris({ maxCycles, verbose, dryRun, once, push, timeout })
1932
+ .then(() => process.exit(0))
1933
+ .catch((error) => {
1934
+ console.error(`\u2717 Run failed: ${error.message || error}`);
1935
+ process.exit(1);
1936
+ });
1937
+ } else {
1938
+ require('../commands/run-front').runMissionFront(args)
1939
+ .then((code) => process.exit(code || 0))
1940
+ .catch((error) => {
1941
+ console.error(`\u2717 Run failed: ${error.message || error}`);
1942
+ process.exit(1);
1943
+ });
1944
+ }
1879
1945
  } else if (command === 'launchpad') {
1880
1946
  const code = require('../commands/launchpad').launchpadCommand(process.argv.slice(3));
1881
1947
  process.exit(code);
1882
1948
  } else if (command === 'autopilot') {
1883
1949
  const args = process.argv.slice(3);
1884
- if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
1885
- showAutopilotHelp();
1886
- process.exit(0);
1887
- }
1888
-
1889
- // Parse options
1890
- const verbose = args.includes('--verbose') || args.includes('-v');
1891
- const dryRun = args.includes('--dry-run');
1892
- const auto = args.includes('--auto');
1893
1950
  applyRunnerFlags(args);
1894
- const maxIterationsArg = args.find(a => a.startsWith('--iterations='));
1895
- const maxIterations = maxIterationsArg ? parseInt(maxIterationsArg.split('=')[1]) : undefined;
1896
- const durationArg = args.find(a => a.startsWith('--duration='));
1897
- const duration = durationArg ? durationArg.split('=')[1] : null;
1898
-
1899
- // Get description (non-flag args)
1900
- const description = args.filter((a, i) => !a.startsWith('-') && !isOptionValue(args, i, RUNNER_FLAG_NAMES)).join(' ').trim() || null;
1901
-
1902
- const options = {
1903
- ...(maxIterations !== undefined && { maxIterations }),
1904
- verbose,
1905
- dryRun,
1906
- auto,
1907
- duration
1908
- };
1909
1951
 
1910
- let promise;
1911
- promise = require('../commands/autopilot').autopilotAtris(description, options);
1952
+ if (args.includes('--legacy')) {
1953
+ const legacyArgs = args.filter(a => a !== '--legacy');
1954
+ if (legacyArgs.includes('--help') || legacyArgs.includes('-h') || legacyArgs[0] === 'help') {
1955
+ showAutopilotHelp();
1956
+ process.exit(0);
1957
+ }
1958
+
1959
+ // Parse options
1960
+ const verbose = legacyArgs.includes('--verbose') || legacyArgs.includes('-v');
1961
+ const dryRun = legacyArgs.includes('--dry-run');
1962
+ const auto = legacyArgs.includes('--auto');
1963
+ const maxIterationsArg = legacyArgs.find(a => a.startsWith('--iterations='));
1964
+ const maxIterations = maxIterationsArg ? parseInt(maxIterationsArg.split('=')[1]) : undefined;
1965
+ const durationArg = legacyArgs.find(a => a.startsWith('--duration='));
1966
+ const duration = durationArg ? durationArg.split('=')[1] : null;
1967
+
1968
+ // Get description (non-flag args)
1969
+ const description = legacyArgs.filter((a, i) => !a.startsWith('-') && !isOptionValue(legacyArgs, i, RUNNER_FLAG_NAMES)).join(' ').trim() || null;
1970
+
1971
+ const options = {
1972
+ ...(maxIterations !== undefined && { maxIterations }),
1973
+ verbose,
1974
+ dryRun,
1975
+ auto,
1976
+ duration
1977
+ };
1912
1978
 
1913
- promise
1914
- .then(() => process.exit(0))
1915
- .catch((error) => {
1916
- console.error(`✗ Autopilot failed: ${error.message || error}`);
1917
- process.exit(1);
1918
- });
1979
+ require('../commands/autopilot').autopilotAtris(description, options)
1980
+ .then(() => process.exit(0))
1981
+ .catch((error) => {
1982
+ console.error(`✗ Autopilot failed: ${error.message || error}`);
1983
+ process.exit(1);
1984
+ });
1985
+ } else {
1986
+ Promise.resolve(require('../commands/autopilot-front').autopilotFront(args))
1987
+ .then((code) => process.exit(code || 0))
1988
+ .catch((error) => {
1989
+ console.error(`✗ Autopilot failed: ${error.message || error}`);
1990
+ process.exit(1);
1991
+ });
1992
+ }
1919
1993
  } else if (command === 'brainstorm') {
1920
1994
  require('../commands/brainstorm').brainstormAtris()
1921
1995
  .then(() => process.exit(0))
@@ -2240,6 +2314,17 @@ if (command === 'init') {
2240
2314
  Promise.resolve(require('../commands/compile').compileCommand(subcommand, ...args))
2241
2315
  .then(() => process.exit(process.exitCode || 0))
2242
2316
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
2317
+ } else if (command === 'engine' || command === 'engines') {
2318
+ // Engine: bring any intelligence — roster of installed coding CLIs, default
2319
+ // engine per workspace, --engine <name> rides any loop for one run.
2320
+ try {
2321
+ const engineArgs = command === 'engines' && process.argv.length <= 3 ? [] : process.argv.slice(3);
2322
+ process.exit(require('../commands/engine').engineCommand(engineArgs) || 0);
2323
+ } catch (err) { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); }
2324
+ } else if (command === 'sign') {
2325
+ // Sign: prepare-commit-msg hook that credits Atris as co-author on commits in atris workspaces.
2326
+ try { process.exit(require('../commands/sign').signCommand(process.argv[3]) || 0); }
2327
+ catch (err) { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); }
2243
2328
  } else if (command === 'slop') {
2244
2329
  // Slop: deterministic frontend-slop detector (no LLM). Exit 1 = slop found, for CI + the autopilot gate.
2245
2330
  Promise.resolve(require('../commands/slop').slopCommand(process.argv.slice(3)))
@@ -2638,18 +2723,21 @@ async function agentAtris() {
2638
2723
 
2639
2724
 
2640
2725
  async function chatAtris() {
2641
- // Get message from command line args
2642
- const message = process.argv.slice(3).join(' ').trim();
2726
+ // Get message from command line args; --agent forces the legacy cloud-agent lane
2727
+ const rawArgs = process.argv.slice(3);
2728
+ const agentLane = rawArgs.includes('--agent');
2729
+ const message = rawArgs.filter(arg => arg !== '--agent').join(' ').trim();
2643
2730
 
2644
2731
  // Respect -h / --help before any auth/state checks
2645
2732
  if (message === '-h' || message === '--help' || message === 'help') {
2646
2733
  console.log('Usage: atris chat ["message"]');
2647
2734
  console.log('');
2648
- console.log(' Open an interactive session with the selected agent, or send a one-shot message.');
2649
- console.log(' Requires `atris login` and `atris agent` to be run first.');
2735
+ console.log(' Chat with Atris 2 Fast in this workspace: tools attached, same turn as `ax --fast`.');
2736
+ console.log(' Requires `atris login`.');
2650
2737
  console.log('');
2651
- console.log(' atris chat Interactive REPL with selected agent');
2652
- console.log(' atris chat "what now?" One-shot message');
2738
+ console.log(' atris chat Interactive chat (ax --fast --chat)');
2739
+ console.log(' atris chat "what now?" One-shot message (ax --fast)');
2740
+ console.log(' atris chat --agent [...] Legacy cloud-agent lane (needs `atris agent`)');
2653
2741
  process.exit(0);
2654
2742
  }
2655
2743
 
@@ -2665,6 +2753,22 @@ async function chatAtris() {
2665
2753
  process.exit(await runLocalFastMission(missionIntent));
2666
2754
  }
2667
2755
 
2756
+ // Workspace standard v2: `atris chat` is the same turn as `ax --fast`,
2757
+ // Atris 2 Fast with tools attached, ax owning routing (local tool loop,
2758
+ // cloud fallback when no backend listens). The pro-chat cloud-agent lane
2759
+ // survives behind --agent only; it 404s the moment a saved agent is
2760
+ // deleted server-side and cannot see the workspace.
2761
+ if (!agentLane) {
2762
+ try {
2763
+ const axPath = path.join(__dirname, '..', 'ax');
2764
+ const axArgs = message ? ['--fast', message] : ['--fast', '--chat'];
2765
+ const run = spawnSync(process.execPath, [axPath, ...axArgs], { stdio: 'inherit' });
2766
+ process.exit(run.status || 0);
2767
+ } catch {
2768
+ // ax unavailable: fall through to the agent lane.
2769
+ }
2770
+ }
2771
+
2668
2772
  printAtrisGoalBanner(process.cwd());
2669
2773
 
2670
2774
  // Check agent selected
@@ -2714,7 +2818,13 @@ async function chatOnce(config, credentials, message) {
2714
2818
  await streamProChat(endpoint, credentials.token, body);
2715
2819
  console.log('\n\n✓ Complete\n');
2716
2820
  } catch (error) {
2717
- console.error(`\n✗ Error: ${error.message || error}`);
2821
+ const detail = String(error.message || error);
2822
+ if (/404/.test(detail) && /agent not found/i.test(detail)) {
2823
+ console.error(`\n✗ Error: Agent "${config.agent_name || agentId}" no longer exists on the server.`);
2824
+ console.error(' Run "atris agent" to pick a new one, or drop --agent to use the fast workspace lane.');
2825
+ } else {
2826
+ console.error(`\n✗ Error: ${detail}`);
2827
+ }
2718
2828
  process.exit(1);
2719
2829
  }
2720
2830
  }
@@ -2914,6 +3024,20 @@ async function atrisFastChat() {
2914
3024
  process.exit(await runLocalFastMission(missionIntent));
2915
3025
  }
2916
3026
 
3027
+ // One routing brain: `atris fast` defers to ax's AX Context Standard. A
3028
+ // workspace-shaped question asked from inside a workspace needs local tools
3029
+ // (this lane is a tool-less cloud one-shot and confabulates on repo
3030
+ // questions — SwapBench 2026-07-02), so delegate the whole turn to ax.
3031
+ try {
3032
+ const axModule = require('../ax');
3033
+ if (axModule.resolveRoute(message) === 'local') {
3034
+ const run = spawnSync(process.execPath, [path.join(__dirname, '..', 'ax'), '--fast', message], { stdio: 'inherit' });
3035
+ process.exit(run.status || 0);
3036
+ }
3037
+ } catch {
3038
+ // ax unavailable: fall through to the plain cloud one-shot.
3039
+ }
3040
+
2917
3041
  printAtrisGoalBanner(process.cwd());
2918
3042
 
2919
3043
  const ensured = await ensureValidCredentials();
@@ -6,6 +6,7 @@ const os = require('os');
6
6
  const { spawnSync } = require('child_process');
7
7
 
8
8
  const autoland = require('../lib/autoland');
9
+ const { operatorReady, hasAgentJargon } = autoland;
9
10
 
10
11
  function repoRoot(cwd = process.cwd()) {
11
12
  const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' });
@@ -17,6 +18,25 @@ function projectName(root) {
17
18
  return path.basename(root);
18
19
  }
19
20
 
21
+ // The digest's "next, if you agree" section: top candidate moves from Atris
22
+ // state, each with the member best suited to own it. Moves that can't explain
23
+ // themselves are counted, not shown. Never blocks the digest.
24
+ function digestNextMoves(root) {
25
+ try {
26
+ const { nextMoves } = require('../lib/next-moves');
27
+ const { resolveFunctionalOwner } = require('../lib/functional-owner');
28
+ const all = (nextMoves(root, 5) || []).filter((move) => move && move.title);
29
+ const ready = all.filter((move) => operatorReady(move.title)).slice(0, 3).map((move) => {
30
+ let owner = null;
31
+ try { owner = resolveFunctionalOwner({ title: move.title, root })?.owner || null; } catch {}
32
+ return { title: move.title, owner };
33
+ });
34
+ return { moves: ready, unexplained: all.length - ready.length };
35
+ } catch {
36
+ return { moves: [], unexplained: 0 };
37
+ }
38
+ }
39
+
20
40
  function flag(args, name, fallback = '') {
21
41
  const idx = args.indexOf(name);
22
42
  if (idx === -1) return fallback;
@@ -24,9 +44,11 @@ function flag(args, name, fallback = '') {
24
44
  }
25
45
 
26
46
  function runOwnCli(root, cliArgs) {
27
- const bin = path.join(root, 'bin', 'atris.js');
28
- const argv = fs.existsSync(bin) ? [process.execPath, [bin, ...cliArgs]] : ['atris', [cliArgs].flat()];
29
- const result = spawnSync(argv[0], argv[1], { cwd: root, encoding: 'utf8', timeout: 300000 });
47
+ // Always spawn the bin this module shipped with. Resolving through the
48
+ // target root or a global `atris` breaks whenever the tick runs a project
49
+ // that isn't the CLI repo on a machine without a global install (CI, cron).
50
+ const bin = path.resolve(__dirname, '..', 'bin', 'atris.js');
51
+ const result = spawnSync(process.execPath, [bin, ...cliArgs], { cwd: root, encoding: 'utf8', timeout: 300000 });
30
52
  return { status: result.status, stdout: String(result.stdout || ''), stderr: String(result.stderr || '') };
31
53
  }
32
54
 
@@ -186,6 +208,7 @@ function runDigest(root, args, { forceSend = false } = {}) {
186
208
  waiting: autoland.waitingOnHuman(tasks),
187
209
  landed: landSummarySafe(root),
188
210
  project: projectName(root),
211
+ nextMoves: digestNextMoves(root),
189
212
  });
190
213
  console.log(text);
191
214
  // the full story: what each piece actually was, in its own words
@@ -195,10 +218,15 @@ function runDigest(root, args, { forceSend = false } = {}) {
195
218
  return t && (t.review?.landing?.happened || t.metadata?.landing_happened);
196
219
  });
197
220
  if (storied.length > 0) {
198
- console.log('');
221
+ let printedStoryHeader = false;
199
222
  for (const item of storied) {
200
223
  const t = byRef.get(item.ref);
201
224
  const happened = String(t.review?.landing?.happened || t.metadata?.landing_happened || '').replace(/\s+/g, ' ').slice(0, 160);
225
+ if (!operatorReady(happened)) continue;
226
+ if (!printedStoryHeader) {
227
+ console.log('');
228
+ printedStoryHeader = true;
229
+ }
202
230
  console.log(` ${item.ref} ${happened}`);
203
231
  }
204
232
  }
@@ -221,7 +249,22 @@ function runTick(root, args) {
221
249
  return 0;
222
250
  }
223
251
 
224
- // 1. land what is eligible — the policy is the standing authorization
252
+ // 1. certify what has executable proof re-run the runnable check named in
253
+ // each Review proof as a second actor. Without this the tick only lands rows
254
+ // some always-on mission happened to certify, and everything else waits on a
255
+ // human who never needed to look. Denied lanes and check-less proofs still wait.
256
+ if (policy.drain_reviews !== false) {
257
+ const certify = runOwnCli(root, ['task', 'certify-verified', '--json']);
258
+ try {
259
+ const parsed = JSON.parse(certify.stdout);
260
+ receipt.reviews_certified = parsed.certified ?? 0;
261
+ if (parsed.ok !== true) receipt.certify_error = 'certify-verified failed';
262
+ } catch {
263
+ receipt.certify_error = certify.stderr.slice(0, 200) || 'certify-verified output unreadable';
264
+ }
265
+ }
266
+
267
+ // 2. land what is eligible — the policy is the standing authorization
225
268
  const cliArgs = ['task', 'auto-accept-certified', '--json', '--limit', '12'];
226
269
  if (policy.strict_verify !== false) cliArgs.push('--strict-verify');
227
270
  const accept = runOwnCli(root, cliArgs);
@@ -232,7 +275,23 @@ function runTick(root, args) {
232
275
  receipt.accept_error = accept.stderr.slice(0, 200) || 'auto-accept output unreadable';
233
276
  }
234
277
 
235
- // 2. alarm on anything waiting on a human past the line
278
+ // 2b. tell the operator the moment something lands: one text, one landing
279
+ // sentence per piece (the day-one PM sentence written at finish time),
280
+ // capped at three with the rest counted. Off with live_updates: false.
281
+ const tasksForLive = readProjection(root);
282
+ if (receipt.landed.length > 0 && policy.imessage_to && policy.live_updates !== false) {
283
+ const text = autoland.composeLiveUpdate({
284
+ landedRefs: receipt.landed,
285
+ tasks: tasksForLive,
286
+ project: projectName(root),
287
+ });
288
+ if (text) {
289
+ const sent = autoland.sendImessage(root, policy.imessage_to, text);
290
+ receipt.live_update_sent = sent.ok;
291
+ }
292
+ }
293
+
294
+ // 3. alarm on anything waiting on a human past the line
236
295
  const state = autoland.readState(root);
237
296
  const tasks = readProjection(root);
238
297
  const waiting = autoland.waitingOnHuman(tasks);
@@ -247,7 +306,7 @@ function runTick(root, args) {
247
306
  }
248
307
  }
249
308
 
250
- // 3. daily digest at the configured hour
309
+ // 4. daily digest at the configured hour
251
310
  const today = new Date().toISOString().slice(0, 10);
252
311
  const digestHour = Number(policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR);
253
312
  if (new Date().getHours() === digestHour && state.last_digest_date !== today) {
@@ -256,6 +315,7 @@ function runTick(root, args) {
256
315
  waiting,
257
316
  landed: landSummarySafe(root),
258
317
  project: projectName(root),
318
+ nextMoves: digestNextMoves(root),
259
319
  });
260
320
  if (policy.imessage_to) {
261
321
  const sent = autoland.sendImessage(root, policy.imessage_to, text);
@@ -264,7 +324,7 @@ function runTick(root, args) {
264
324
  receipt.digest_text = text;
265
325
  state.last_digest_date = today;
266
326
 
267
- // 4. once a day, keep the receipt shelf lean: compress old run receipts
327
+ // 5. once a day, keep the receipt shelf lean: compress old run receipts
268
328
  // into the manifest and drop unreferenced clutter, newest 200 kept.
269
329
  const prune = runOwnCli(root, ['mission', 'prune-runs', '--apply', '--days', '14', '--keep-newest', '200', '--json']);
270
330
  try {
@@ -278,7 +338,7 @@ function runTick(root, args) {
278
338
 
279
339
  if (json) console.log(JSON.stringify(receipt));
280
340
  else {
281
- console.log(`autoland tick: ${receipt.landed.length} landed${receipt.landed.length ? ` (${receipt.landed.join(', ')})` : ''}, ${receipt.alarms} alarms, digest ${receipt.digest_sent ? 'sent' : 'not due'}`);
341
+ console.log(`autoland tick: ${receipt.reviews_certified ?? 0} reviews certified, ${receipt.landed.length} landed${receipt.landed.length ? ` (${receipt.landed.join(', ')})` : ''}, ${receipt.alarms} alarms, digest ${receipt.digest_sent ? 'sent' : 'not due'}`);
282
342
  }
283
343
  return 0;
284
344
  }
@@ -297,15 +357,21 @@ function showHelp() {
297
357
  console.log(' on you past 24h');
298
358
  console.log(' atris autoland off back to approving every item');
299
359
  console.log(' atris autoland digest [--send] the daily message, now');
300
- console.log(' atris autoland tick one heartbeat (what the hourly cron runs)');
360
+ console.log(' atris autoland tick [--json] one heartbeat (what the hourly cron runs)');
361
+ console.log('');
362
+ console.log('help is always read-only: atris autoland tick --help never lands work.');
301
363
  console.log('');
302
364
  return 0;
303
365
  }
304
366
 
367
+ function wantsHelp(args = []) {
368
+ return args.some((arg) => arg === 'help' || arg === '--help' || arg === '-h');
369
+ }
370
+
305
371
  function autolandCommand(args = []) {
306
372
  const [sub, ...rest] = args;
373
+ if (wantsHelp(args)) return showHelp();
307
374
  if (!sub || sub.startsWith('--')) return showStatus(repoRoot(), args);
308
- if (sub === 'help' || sub === '--help' || sub === '-h') return showHelp();
309
375
  const root = repoRoot();
310
376
  if (sub === 'on') return turnOn(root, rest);
311
377
  if (sub === 'off') return turnOff(root);
@@ -316,4 +382,4 @@ function autolandCommand(args = []) {
316
382
  return 1;
317
383
  }
318
384
 
319
- module.exports = { autolandCommand };
385
+ module.exports = { autolandCommand, operatorReady, hasAgentJargon };