atris 3.31.0 → 3.32.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/README.md CHANGED
@@ -81,7 +81,7 @@ atris
81
81
 
82
82
  `atris init` scaffolds the workspace, including `atris/wiki/`. `atris` loads context and hands the workflow off to `atris/atris.md`.
83
83
 
84
- If you're still shaping the idea, use `atris brainstorm`. If you want Atris to keep cycling, use `atris run` or `atris autopilot`. If you want project memory checked for stale pages and missing context, use `atris loop`. `atris activate` surfaces wiki state from `atris/wiki/STATUS.md` when it exists.
84
+ If you're still shaping the idea, use `atris brainstorm`. If you want one bounded mission pursued to proof, use `atris run "<objective>"`. If you want Atris to keep picking the next mission or member until you say stop, use `atris autopilot`. If you want project memory checked for stale pages and missing context, use `atris loop`. `atris activate` surfaces wiki state from `atris/wiki/STATUS.md` when it exists.
85
85
 
86
86
  Core loop: `plan` -> `do` -> `review`
87
87
 
@@ -186,14 +186,14 @@ atris business record atris/reports/2026-04-12-operator-recap.md --outcome mixed
186
186
  | `atris plan` | Create the plan/spec |
187
187
  | `atris do` | Execute work |
188
188
  | `atris review` | Validate work and capture learnings |
189
- | `atris run` | Auto-chain `plan -> do -> review` |
189
+ | `atris run` | One bounded mission pursuit: start or resume, tick, complete (`--legacy` for the old plan/do/review chain) |
190
190
  | `atris run logs` | Browse glass run logs (phase reasoning persisted to disk) |
191
191
  | `atris run prune-logs` | Prune old run logs, keeping only the most recent N |
192
192
  | `atris run search` | Search phase reasoning across all run logs by keyword |
193
193
  | `atris run stats` | Show run log stats: phase counts, avg durations |
194
194
  | `atris run export` | Export all run logs as a JSON bundle |
195
195
  | `atris run diff` | Compare two run logs side by side |
196
- | `atris autopilot` | Guided loop with approvals |
196
+ | `atris autopilot` | Keep the workspace moving: mission/member legs until `atris autopilot stop` (`--legacy` for the old approval loop) |
197
197
  | `atris pulse` | Install or run the durable overnight heartbeat |
198
198
  | `atris log` | Add inbox items to today's journal |
199
199
  | `atris now` | Show the current operating truth |
package/atris/CLAUDE.md CHANGED
@@ -90,6 +90,14 @@ release/publish work; clean old merged worktrees with `atris worktree cleanup`.
90
90
 
91
91
  If the task produces durable project knowledge, update `atris/wiki/` or run the local wiki flow (`atris ingest`, `atris query`, `atris lint`).
92
92
 
93
+ ## How to Report
94
+
95
+ The human approves work by reading, so how you report IS the product.
96
+
97
+ - **Results are capabilities.** State what someone can do now that they couldn't before, then what it means for them or the business. Tests are one word ("verified"). Shape: "We did X, so you can now Y." Detail stays under the hood; the reader asks if they want more.
98
+ - **Three results, air between them, rest on ask.** A report fits one screen with no scrolling.
99
+ - **Stake first, then the move.** "Agents burn tokens hand-rolling parsers: add one shared view." Flags, ids, and identifiers belong in the body, never the headline.
100
+
93
101
  ## Rules (Non‑Negotiable)
94
102
 
95
103
  - Plan = ASCII visualization + approval gate. Do not execute during planning.
package/atris.md CHANGED
@@ -80,6 +80,14 @@ The same discipline for words. Output stays sharp no matter how bloated the cont
80
80
  - **Cut filler.** Drop "it's worth noting", "in order to", "leverage", "seamless", "robust", "delve", stacked hedges, and em dashes. `atris slop` flags the prose tells (em-dash, hype-copy) too.
81
81
  - **Bound verbosity by information, not context.** Say the load-bearing thing and stop. Length tracks what the reader needs to act, nothing more.
82
82
  - **Match the register.** The operator wants the next move; a spec wants the contract; a journal wants one line. Jargon is a lever only when shared: use the reader's precise terms, define a new one once.
83
+ - **Specific AND workable.** Vocabulary carries both: precise enough that another agent can act on it, plain enough that the operator gets it on a phone. A sentence only one of those audiences can use is half-written.
84
+ - **Every queue item earns its surface.** Anything an operator might see (task title, mission objective, roadmap item, tick summary) carries its own why in the same sentence: what it costs to skip, or what it buys. "Agents burn tokens hand-rolling state parsers: add one shared inspect view" surfaces; "novel goal-chain demo" does not. Digests refuse raw titles that can't explain themselves.
85
+ - **Day-one PM test.** Every sentence that reaches an operator must make sense to a PM who joined the company this morning: no internal codenames, no problem-titles standing in for results. If they'd have to ask "what does that mean?", the sentence is unfinished. Detail stays one ask away, always.
86
+ - **Results are capabilities, not test counts.** When work lands, the sentence states what someone can do now that they couldn't before, then what it means for them or the business. Tests are one word ("verified"); the meaning is the sentence. Copy these shapes:
87
+ - "One word now runs the work: `atris autopilot` picks the next mission and keeps going until you say stop. Autonomy stopped being a setup and became a feature."
88
+ - "Missions survive everything: one engine starts the work, another picks it up cold, a third lands it. Work is no longer tied to a chat window, a session, or a vendor."
89
+ - "The system reports in plain English: one daily message with what landed, what waits on you, and who should own what's next. One person can supervise many projects."
90
+ - **Fit the screen.** An operator-facing report shows three results, air between them, and holds the rest on ask. No scrolling: reading it is one glance, and the reader asks for more if they want more.
83
91
 
84
92
  `expected`: this is how an Atris agent writes and builds. Shipping slop or rambling is a failure smell, same as drift or a stale task.
85
93
 
package/ax CHANGED
@@ -306,6 +306,20 @@ function formatRunProfile(profile, options = {}) {
306
306
  return rows.map(([label, value]) => formatAuxRow(label, value, options)).join('\n');
307
307
  }
308
308
 
309
+ // Auto-routed local turns must never die on a missing local backend — a
310
+ // consumer machine without the launchd service gets a degraded cloud chat
311
+ // turn, not ECONNREFUSED. Probed once per process (REPL lines reuse it).
312
+ // Explicit --local skips the downgrade so devs see the real error. Interim
313
+ // guard until the cloud relay executes workspace tools in-process and
314
+ // consumers stop needing a local backend at all.
315
+ let _localBackendUp = null;
316
+ async function localBackendReachable() {
317
+ if (_localBackendUp !== null) return _localBackendUp;
318
+ const res = await requestJson(ATRIS2_HEALTH_PATH, { timeoutMs: 1200, route: 'local' });
319
+ _localBackendUp = Boolean(res.ok || Number(res.status || 0) > 0);
320
+ return _localBackendUp;
321
+ }
322
+
309
323
  async function buildRuntimeHealth(options = {}) {
310
324
  const route = options.route === 'local' || options.forceLocal ? 'local' : 'cloud';
311
325
  const healthRes = options.healthRes
@@ -621,9 +635,49 @@ function githubWorkspaceIntent(message) {
621
635
  return /\b(push|commit|commits?|branch|branches|checkout|merge|rebase|tag|release|pr|pull request|pull-request|repo change|code change|small change)\b/i.test(text);
622
636
  }
623
637
 
638
+ // Walk up from cwd looking for a workspace marker (.git or atris/ or atris.md).
639
+ // The process.cwd() answer is cached — resolveRoute runs per chat line — but an
640
+ // explicit startDir (callers pass their own cwd) is always checked fresh.
641
+ let _workspaceCwd = null;
642
+ function insideWorkspaceCwd(startDir) {
643
+ const explicit = typeof startDir === 'string' && startDir.length > 0;
644
+ if (!explicit && _workspaceCwd !== null) return _workspaceCwd;
645
+ let dir = explicit ? startDir : process.cwd();
646
+ let found = false;
647
+ for (let i = 0; i < 24; i += 1) {
648
+ if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'atris.md')) || fs.existsSync(path.join(dir, 'atris'))) {
649
+ found = true;
650
+ break;
651
+ }
652
+ const parent = path.dirname(dir);
653
+ if (parent === dir) break;
654
+ dir = parent;
655
+ }
656
+ if (!explicit) _workspaceCwd = found;
657
+ return found;
658
+ }
659
+
624
660
  function resolveRoute(message, options = {}) {
625
661
  if (options.route === 'local' || options.forceLocal) return 'local';
626
662
  if (options.route === 'cloud' || options.forceCloud) return 'cloud';
663
+ // Workspace standard v2 (2026-07-02, supersedes intent routing): inside a
664
+ // workspace, tools are ALWAYS attached and the model decides whether to use
665
+ // them — the Claude Code / Codex shape. v1 guessed intent with regexes and
666
+ // lost three times in one day ("in scripts/fast-coach/tick.mjs, what does
667
+ // SLOW_MS control?", "what did Atris ship last night?", "find it lol" — all
668
+ // tool-less misses that told the user to paste files they were sitting on).
669
+ // Measured cost of always-tools: pure math 3s vs 2s; and "plan my morning"
670
+ // through the tool lane reads the real task board instead of guessing.
671
+ // Non-workspace cwds stay pure cloud (the privacy line); explicit flags
672
+ // always win. One carve-out: GitHub-connector phrasing prefers the cloud
673
+ // GitHub API lane unless it names local-checkout work.
674
+ const text = String(message || '');
675
+ // Local-checkout work includes every git mutation verb: cloud chat can only
676
+ // read the GitHub connector, so "do a quick github push" through the cloud
677
+ // lane dead-ends in a write-blocked upsell (live miss 2026-07-02). Inside a
678
+ // workspace the local tool loop just runs git.
679
+ if (/\bgithub\b/i.test(text) && !/\b(commits?|branch(?:es)?|checkout|merge|rebase|changes?|diff|push(?:es|ed|ing)?|pull(?:s|ed|ing)?|fetch|clone|stash|tags?)\b/i.test(text)) return 'cloud';
680
+ if (insideWorkspaceCwd(options.cwd)) return 'local';
627
681
  return 'cloud';
628
682
  }
629
683
 
@@ -1871,6 +1925,19 @@ function codeFastWorkspaceNotice(result) {
1871
1925
  function handleEvent(event, state, output) {
1872
1926
  if (!event || typeof event !== 'object') return;
1873
1927
  state.events.push(event);
1928
+ // AX_TIMING=1: phase scoreboard to stderr: one line per SSE event with ms
1929
+ // since turn start and since the previous event, so latency work can see
1930
+ // WHERE a slow turn spends its time (model TTFT vs tool exec vs loop count).
1931
+ if (process.env.AX_TIMING === '1') {
1932
+ const now = Date.now();
1933
+ if (!state._t0) state._t0 = now;
1934
+ const kind = event.type || event.status || 'event';
1935
+ const detail = event.type === 'assistant_blocks' && Array.isArray(event.blocks)
1936
+ ? event.blocks.map((b) => (b && b.type === 'tool_use' ? `tool:${b.tool}` : b && b.type)).join(',')
1937
+ : event.type === 'tool_results' ? `results:${(event.results || []).length}` : '';
1938
+ process.stderr.write(`[ax-timing] +${String(now - state._t0).padStart(6)}ms (gap ${String(now - (state._tPrev || now)).padStart(5)}ms) ${kind}${detail ? ' ' + detail : ''}\n`);
1939
+ state._tPrev = now;
1940
+ }
1874
1941
 
1875
1942
  if (event.type === 'system_init') {
1876
1943
  state.runtime = event;
@@ -1968,7 +2035,11 @@ function handleEvent(event, state, output) {
1968
2035
  }
1969
2036
 
1970
2037
  async function postTurn(message, options = {}) {
1971
- const route = options.business ? 'local' : resolveRoute(message, options);
2038
+ let route = options.business ? 'local' : resolveRoute(message, options);
2039
+ if (route === 'local' && !options.business && options.route !== 'local' && !options.forceLocal
2040
+ && !(await localBackendReachable())) {
2041
+ route = 'cloud';
2042
+ }
1972
2043
  const local = route !== 'cloud';
1973
2044
  const endpointRoute = options.business ? 'cloud' : route;
1974
2045
  const token = authToken();
@@ -1987,7 +2058,11 @@ async function postTurn(message, options = {}) {
1987
2058
  // Relayed business turns wait on EC2 terminal calls (up to 60s each) with no
1988
2059
  // SSE traffic in between, so the socket-idle timeout needs more headroom.
1989
2060
  const baseTimeoutMs = payload.model === 'atris:max' ? 300000 : payload.model === 'atris:pro' ? 180000 : 60000;
1990
- const timeoutMs = options.business ? Math.max(baseTimeoutMs, 180000) : baseTimeoutMs;
2061
+ let timeoutMs = options.business ? Math.max(baseTimeoutMs, 180000) : baseTimeoutMs;
2062
+ // Local workspace tool loops (max_turns 8/14) legitimately run past the fast
2063
+ // lane's 60s chat wall — SwapBench 2026-07-02: three tool-loop tasks died at
2064
+ // ~60s as "Atris cloud did not respond". Same headroom as business relays.
2065
+ if (local) timeoutMs = Math.max(timeoutMs, 180000);
1991
2066
  const turnUrl = new URL(backendUrl({ route: endpointRoute }));
1992
2067
  const transport = turnUrl.protocol === 'https:' ? https : http;
1993
2068
  const state = {
@@ -2357,7 +2432,15 @@ async function chat(options = {}) {
2357
2432
  const pendingTaskPreview = latestPendingTaskPreview(history);
2358
2433
  const route = pendingTaskPreview ? 'cloud' : options.route;
2359
2434
  maybeWriteAtrisGoalBanner(output, cwd);
2360
- const turnRoute = options.business ? 'cloud' : resolveRoute(trimmed, { route });
2435
+ let turnRoute = options.business ? 'cloud' : resolveRoute(trimmed, { route, cwd });
2436
+ // Same downgrade as postTurn: an auto-routed local turn with no local
2437
+ // backend listening becomes a cloud turn instead of a blocked preflight
2438
+ // that dumps localhost start instructions at a consumer. Explicit --local
2439
+ // keeps the real error.
2440
+ if (turnRoute === 'local' && route !== 'local' && !options.forceLocal) {
2441
+ const probeLocal = options.localBackendProbe || localBackendReachable;
2442
+ if (!(await probeLocal())) turnRoute = 'cloud';
2443
+ }
2361
2444
  if (!(await ensureRuntimeReady(turnRoute))) return false;
2362
2445
  const turnFunction = options.turnFunction || turnFunctionForMode(mode);
2363
2446
  const turnId = options.turnId || createTurnId();
@@ -3311,7 +3394,14 @@ async function main() {
3311
3394
  console.log(formatDoneLine(result.durationMs, creditsFromState(result)));
3312
3395
  } catch (error) {
3313
3396
  console.error(`x ${error.message}`);
3314
- printBackendHint({ route: route === 'auto' ? undefined : route });
3397
+ // Only name a lane when the user picked one. An auto-routed turn that
3398
+ // failed locally must not claim "Atris cloud did not respond" (seen live
3399
+ // 2026-07-02 after a local workspace-root rejection).
3400
+ if (route === 'auto') {
3401
+ console.error('\nRetry, or run ax --doctor to check the runtime.');
3402
+ } else {
3403
+ printBackendHint({ route });
3404
+ }
3315
3405
  process.exit(1);
3316
3406
  }
3317
3407
  }
package/bin/atris.js CHANGED
@@ -128,7 +128,16 @@ function isOptionValue(args, index, optionNames) {
128
128
 
129
129
  function applyRunnerFlags(args) {
130
130
  const runnerProfile = readOptionArg(args, '--runner-profile');
131
- if (runnerProfile) process.env.ATRIS_RUNNER_PROFILE = runnerProfile;
131
+ if (runnerProfile) {
132
+ // Fail fast at the CLI boundary: an unknown profile otherwise stays silent
133
+ // until a heartbeat spawn resolves it mid-loop (silent overnight outage).
134
+ const { RUNNER_PROFILES, RUNNER_PROFILE_NAMES } = require('../lib/runner-command');
135
+ if (!Object.prototype.hasOwnProperty.call(RUNNER_PROFILES, runnerProfile)) {
136
+ console.error(`Unknown --runner-profile "${runnerProfile}". Known profiles: ${RUNNER_PROFILE_NAMES.join(', ')}.`);
137
+ process.exit(1);
138
+ }
139
+ process.env.ATRIS_RUNNER_PROFILE = runnerProfile;
140
+ }
132
141
  const runnerBin = readOptionArg(args, '--runner-bin');
133
142
  if (runnerBin) {
134
143
  process.env.ATRIS_RUNNER_BIN = runnerBin;
@@ -449,7 +458,7 @@ function showHelp() {
449
458
  console.log(' plan - Create build spec with visualization');
450
459
  console.log(' do - Execute tasks');
451
460
  console.log(' review - Validate work (tests, safety checks, docs)');
452
- console.log(' run - Auto-chain plan→do→review (autonomous loop, auto-pushes)');
461
+ console.log(' run - One bounded mission pursuit: start or resume, tick, complete');
453
462
  console.log(' run logs - Browse glass run logs (phase reasoning persisted to disk)');
454
463
  console.log(' run search - Search phase reasoning across all run logs');
455
464
  console.log(' pulse - Durable overnight self-improvement heartbeat (OS cron, install/status/tick)');
@@ -483,11 +492,12 @@ function showHelp() {
483
492
  console.log('');
484
493
  console.log('Optional helpers:');
485
494
  console.log(' brainstorm - Explore ideas conversationally before planning');
486
- console.log(' autopilot - Guided loop that can clarify TODOs and run plan → do → review');
495
+ console.log(' autopilot - Keep the workspace moving: mission/member loop until you stop it');
487
496
  console.log(' improve - Run one paid RL tick (POST /api/improve, deducts credits)');
488
497
  console.log(' worktree - Isolated Git worktrees plus guarded ship/merge for parallel agents');
489
498
  console.log(' land - The landing: what is actually done vs still in the air; --reap backs up + clears overdue');
490
499
  console.log(' autoland - Approve the policy once; certified work lands itself, you keep irreversible calls');
500
+ console.log(' sign - Co-author trailer on every commit in an atris workspace (on/off/status)');
491
501
  console.log(' visualize - Generate a Slack/deck-ready visual from a prompt');
492
502
  console.log(' youtube - Process YouTube videos with timestamped transcript-first analysis');
493
503
  console.log('');
@@ -547,7 +557,7 @@ function showHelp() {
547
557
  console.log(' soul - Show, snapshot, or fork workspace identity');
548
558
  console.log(' fleet - Inspect local fleet status');
549
559
  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)');
560
+ console.log(' chat - Chat with Atris 2 Fast in this workspace (--agent for cloud agent; or: atris chat scan)');
551
561
  console.log(' fast - Chat with Atris2 Fast');
552
562
  console.log(' login - Sign in or add another account');
553
563
  console.log(' logout - Sign out of current account');
@@ -885,7 +895,7 @@ function showAutopilotHelp() {
885
895
  console.log(' --runner-bin PATH Runner binary for this run');
886
896
  console.log(' --runner-template CMD Runner command template for this run');
887
897
  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)');
898
+ console.log(` --runner-profile NAME Runner profile for this run (one of: ${require('../lib/runner-command').RUNNER_PROFILE_NAMES.join(', ')})`);
889
899
  console.log('');
890
900
  console.log('Examples:');
891
901
  console.log(' atris autopilot # Suggest from existing work');
@@ -927,7 +937,7 @@ const knownCommands = ['init', 'log', 'now', 'radar', 'ctop', 'launchpad', 'stat
927
937
  '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
938
  '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
939
  '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'];
940
+ 'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'compile', 'spaceship', 'truth', 'sign'];
931
941
 
932
942
  // Check if command is an atris.md spec file - triggers welcome visualization
933
943
  function isSpecFile(cmd) {
@@ -1831,23 +1841,26 @@ if (command === 'init') {
1831
1841
  }
1832
1842
  if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
1833
1843
  console.log('');
1834
- console.log('Usage: atris run [options]');
1844
+ console.log('Usage: atris run ["objective"] [options]');
1835
1845
  console.log('');
1836
- console.log('Auto-chain plan do review cycles autonomously.');
1837
- console.log('Reads inbox ideas, creates tasks, builds them, validates, repeats.');
1846
+ console.log('One bounded mission pursuit: start a mission from the objective');
1847
+ console.log('(or resume the most logical runnable mission), tick it through the');
1848
+ console.log('mission runtime, complete on pass, then exit.');
1849
+ console.log('For an ongoing loop, use: atris autopilot');
1838
1850
  console.log('');
1839
1851
  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');
1852
+ console.log(' --owner NAME Mission owner (default: mission-lead)');
1853
+ console.log(' --minutes N | --hours N Time budget for the pursuit');
1854
+ console.log(' --max-ticks N Override the tick budget');
1855
+ console.log(' --max-wall N Override the wall clock in seconds');
1856
+ console.log(' --cadence C Mission cadence (e.g. 15m)');
1857
+ console.log(' --no-complete Skip auto-complete after a passing run');
1858
+ console.log(' --legacy Old plan→do→review loop (claude -p cycles)');
1859
+ console.log('');
1860
+ console.log('Legacy options (with --legacy):');
1861
+ console.log(' --cycles=N --once --verbose --dry-run --timeout=N --no-push');
1862
+ console.log(' --runner-bin PATH / --runner-template CMD / --runner-model MODEL');
1863
+ console.log(` --runner-profile NAME Runner profile for this run (one of: ${require('../lib/runner-command').RUNNER_PROFILE_NAMES.join(', ')})`);
1851
1864
  console.log('');
1852
1865
  console.log('Subcommands:');
1853
1866
  console.log(' atris run logs [--tail N] [--cat FILE] [--json] Browse glass run logs');
@@ -1860,62 +1873,81 @@ if (command === 'init') {
1860
1873
  process.exit(0);
1861
1874
  }
1862
1875
 
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
1876
  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
1877
 
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
- });
1878
+ if (args.includes('--legacy')) {
1879
+ const legacyArgs = args.filter(a => a !== '--legacy');
1880
+ const verbose = legacyArgs.includes('--verbose') || legacyArgs.includes('-v');
1881
+ const dryRun = legacyArgs.includes('--dry-run');
1882
+ const once = legacyArgs.includes('--once');
1883
+ const push = !legacyArgs.includes('--no-push');
1884
+ const cyclesArg = legacyArgs.find(a => a.startsWith('--cycles='));
1885
+ const maxCycles = cyclesArg ? parseInt(cyclesArg.split('=')[1]) : 5;
1886
+ const timeoutArg = legacyArgs.find(a => a.startsWith('--timeout='));
1887
+ const timeout = timeoutArg ? parseInt(timeoutArg.split('=')[1]) * 1000 : undefined;
1888
+
1889
+ require('../commands/run').runAtris({ maxCycles, verbose, dryRun, once, push, timeout })
1890
+ .then(() => process.exit(0))
1891
+ .catch((error) => {
1892
+ console.error(`\u2717 Run failed: ${error.message || error}`);
1893
+ process.exit(1);
1894
+ });
1895
+ } else {
1896
+ require('../commands/run-front').runMissionFront(args)
1897
+ .then((code) => process.exit(code || 0))
1898
+ .catch((error) => {
1899
+ console.error(`\u2717 Run failed: ${error.message || error}`);
1900
+ process.exit(1);
1901
+ });
1902
+ }
1879
1903
  } else if (command === 'launchpad') {
1880
1904
  const code = require('../commands/launchpad').launchpadCommand(process.argv.slice(3));
1881
1905
  process.exit(code);
1882
1906
  } else if (command === 'autopilot') {
1883
1907
  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
1908
  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
1909
 
1910
- let promise;
1911
- promise = require('../commands/autopilot').autopilotAtris(description, options);
1910
+ if (args.includes('--legacy')) {
1911
+ const legacyArgs = args.filter(a => a !== '--legacy');
1912
+ if (legacyArgs.includes('--help') || legacyArgs.includes('-h') || legacyArgs[0] === 'help') {
1913
+ showAutopilotHelp();
1914
+ process.exit(0);
1915
+ }
1912
1916
 
1913
- promise
1914
- .then(() => process.exit(0))
1915
- .catch((error) => {
1916
- console.error(`✗ Autopilot failed: ${error.message || error}`);
1917
- process.exit(1);
1918
- });
1917
+ // Parse options
1918
+ const verbose = legacyArgs.includes('--verbose') || legacyArgs.includes('-v');
1919
+ const dryRun = legacyArgs.includes('--dry-run');
1920
+ const auto = legacyArgs.includes('--auto');
1921
+ const maxIterationsArg = legacyArgs.find(a => a.startsWith('--iterations='));
1922
+ const maxIterations = maxIterationsArg ? parseInt(maxIterationsArg.split('=')[1]) : undefined;
1923
+ const durationArg = legacyArgs.find(a => a.startsWith('--duration='));
1924
+ const duration = durationArg ? durationArg.split('=')[1] : null;
1925
+
1926
+ // Get description (non-flag args)
1927
+ const description = legacyArgs.filter((a, i) => !a.startsWith('-') && !isOptionValue(legacyArgs, i, RUNNER_FLAG_NAMES)).join(' ').trim() || null;
1928
+
1929
+ const options = {
1930
+ ...(maxIterations !== undefined && { maxIterations }),
1931
+ verbose,
1932
+ dryRun,
1933
+ auto,
1934
+ duration
1935
+ };
1936
+
1937
+ require('../commands/autopilot').autopilotAtris(description, options)
1938
+ .then(() => process.exit(0))
1939
+ .catch((error) => {
1940
+ console.error(`✗ Autopilot failed: ${error.message || error}`);
1941
+ process.exit(1);
1942
+ });
1943
+ } else {
1944
+ Promise.resolve(require('../commands/autopilot-front').autopilotFront(args))
1945
+ .then((code) => process.exit(code || 0))
1946
+ .catch((error) => {
1947
+ console.error(`✗ Autopilot failed: ${error.message || error}`);
1948
+ process.exit(1);
1949
+ });
1950
+ }
1919
1951
  } else if (command === 'brainstorm') {
1920
1952
  require('../commands/brainstorm').brainstormAtris()
1921
1953
  .then(() => process.exit(0))
@@ -2240,6 +2272,10 @@ if (command === 'init') {
2240
2272
  Promise.resolve(require('../commands/compile').compileCommand(subcommand, ...args))
2241
2273
  .then(() => process.exit(process.exitCode || 0))
2242
2274
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
2275
+ } else if (command === 'sign') {
2276
+ // Sign: prepare-commit-msg hook that credits Atris as co-author on commits in atris workspaces.
2277
+ try { process.exit(require('../commands/sign').signCommand(process.argv[3]) || 0); }
2278
+ catch (err) { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); }
2243
2279
  } else if (command === 'slop') {
2244
2280
  // Slop: deterministic frontend-slop detector (no LLM). Exit 1 = slop found, for CI + the autopilot gate.
2245
2281
  Promise.resolve(require('../commands/slop').slopCommand(process.argv.slice(3)))
@@ -2638,18 +2674,21 @@ async function agentAtris() {
2638
2674
 
2639
2675
 
2640
2676
  async function chatAtris() {
2641
- // Get message from command line args
2642
- const message = process.argv.slice(3).join(' ').trim();
2677
+ // Get message from command line args; --agent forces the legacy cloud-agent lane
2678
+ const rawArgs = process.argv.slice(3);
2679
+ const agentLane = rawArgs.includes('--agent');
2680
+ const message = rawArgs.filter(arg => arg !== '--agent').join(' ').trim();
2643
2681
 
2644
2682
  // Respect -h / --help before any auth/state checks
2645
2683
  if (message === '-h' || message === '--help' || message === 'help') {
2646
2684
  console.log('Usage: atris chat ["message"]');
2647
2685
  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.');
2686
+ console.log(' Chat with Atris 2 Fast in this workspace: tools attached, same turn as `ax --fast`.');
2687
+ console.log(' Requires `atris login`.');
2650
2688
  console.log('');
2651
- console.log(' atris chat Interactive REPL with selected agent');
2652
- console.log(' atris chat "what now?" One-shot message');
2689
+ console.log(' atris chat Interactive chat (ax --fast --chat)');
2690
+ console.log(' atris chat "what now?" One-shot message (ax --fast)');
2691
+ console.log(' atris chat --agent [...] Legacy cloud-agent lane (needs `atris agent`)');
2653
2692
  process.exit(0);
2654
2693
  }
2655
2694
 
@@ -2665,6 +2704,22 @@ async function chatAtris() {
2665
2704
  process.exit(await runLocalFastMission(missionIntent));
2666
2705
  }
2667
2706
 
2707
+ // Workspace standard v2: `atris chat` is the same turn as `ax --fast`,
2708
+ // Atris 2 Fast with tools attached, ax owning routing (local tool loop,
2709
+ // cloud fallback when no backend listens). The pro-chat cloud-agent lane
2710
+ // survives behind --agent only; it 404s the moment a saved agent is
2711
+ // deleted server-side and cannot see the workspace.
2712
+ if (!agentLane) {
2713
+ try {
2714
+ const axPath = path.join(__dirname, '..', 'ax');
2715
+ const axArgs = message ? ['--fast', message] : ['--fast', '--chat'];
2716
+ const run = spawnSync(process.execPath, [axPath, ...axArgs], { stdio: 'inherit' });
2717
+ process.exit(run.status || 0);
2718
+ } catch {
2719
+ // ax unavailable: fall through to the agent lane.
2720
+ }
2721
+ }
2722
+
2668
2723
  printAtrisGoalBanner(process.cwd());
2669
2724
 
2670
2725
  // Check agent selected
@@ -2714,7 +2769,13 @@ async function chatOnce(config, credentials, message) {
2714
2769
  await streamProChat(endpoint, credentials.token, body);
2715
2770
  console.log('\n\n✓ Complete\n');
2716
2771
  } catch (error) {
2717
- console.error(`\n✗ Error: ${error.message || error}`);
2772
+ const detail = String(error.message || error);
2773
+ if (/404/.test(detail) && /agent not found/i.test(detail)) {
2774
+ console.error(`\n✗ Error: Agent "${config.agent_name || agentId}" no longer exists on the server.`);
2775
+ console.error(' Run "atris agent" to pick a new one, or drop --agent to use the fast workspace lane.');
2776
+ } else {
2777
+ console.error(`\n✗ Error: ${detail}`);
2778
+ }
2718
2779
  process.exit(1);
2719
2780
  }
2720
2781
  }
@@ -2914,6 +2975,20 @@ async function atrisFastChat() {
2914
2975
  process.exit(await runLocalFastMission(missionIntent));
2915
2976
  }
2916
2977
 
2978
+ // One routing brain: `atris fast` defers to ax's AX Context Standard. A
2979
+ // workspace-shaped question asked from inside a workspace needs local tools
2980
+ // (this lane is a tool-less cloud one-shot and confabulates on repo
2981
+ // questions — SwapBench 2026-07-02), so delegate the whole turn to ax.
2982
+ try {
2983
+ const axModule = require('../ax');
2984
+ if (axModule.resolveRoute(message) === 'local') {
2985
+ const run = spawnSync(process.execPath, [path.join(__dirname, '..', 'ax'), '--fast', message], { stdio: 'inherit' });
2986
+ process.exit(run.status || 0);
2987
+ }
2988
+ } catch {
2989
+ // ax unavailable: fall through to the plain cloud one-shot.
2990
+ }
2991
+
2917
2992
  printAtrisGoalBanner(process.cwd());
2918
2993
 
2919
2994
  const ensured = await ensureValidCredentials();