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.
@@ -0,0 +1,273 @@
1
+ // atris autopilot: point it at the workspace and it keeps going.
2
+ // Each leg picks the most logical work — a moving mission first, then a
3
+ // member choosing useful work, then a self-chosen mission — and drives it
4
+ // through the mission runtime. It loops until stopped:
5
+ // atris autopilot stop (from any terminal) or Ctrl-C here.
6
+ // The old suggest→justify→execute loop lives on behind `atris autopilot --legacy`.
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { spawn } = require('child_process');
10
+ const { pickRunnableMission, runBudgetSeconds } = require('./run-front');
11
+
12
+ const CLI_PATH = path.join(__dirname, '..', 'bin', 'atris.js');
13
+ const DEFAULT_LEG_WALL_SECONDS = 3600;
14
+ const LEG_TICKS = 12;
15
+ const FAST_FAIL_SECONDS = 30;
16
+ const MAX_FAST_FAILS = 5;
17
+
18
+ function statePath(root) { return path.join(root, '.atris', 'state', 'autopilot.json'); }
19
+ function stopPath(root) { return path.join(root, '.atris', 'state', 'autopilot.stop'); }
20
+
21
+ function readState(root) {
22
+ try { return JSON.parse(fs.readFileSync(statePath(root), 'utf8')); } catch { return null; }
23
+ }
24
+
25
+ function writeState(root, state) {
26
+ fs.mkdirSync(path.dirname(statePath(root)), { recursive: true });
27
+ fs.writeFileSync(statePath(root), `${JSON.stringify(state, null, 2)}\n`);
28
+ }
29
+
30
+ function clearState(root) {
31
+ try { fs.unlinkSync(statePath(root)); } catch {}
32
+ }
33
+
34
+ function stopRequested(root) { return fs.existsSync(stopPath(root)); }
35
+ function requestStop(root) {
36
+ fs.mkdirSync(path.dirname(stopPath(root)), { recursive: true });
37
+ fs.writeFileSync(stopPath(root), `${new Date().toISOString()}\n`);
38
+ }
39
+ function clearStop(root) { try { fs.unlinkSync(stopPath(root)); } catch {} }
40
+
41
+ function pidAlive(pid) {
42
+ if (!Number.isFinite(pid) || pid <= 0) return false;
43
+ try { process.kill(pid, 0); return true; } catch { return false; }
44
+ }
45
+
46
+ function readValueFlag(args, name) {
47
+ const index = args.indexOf(name);
48
+ return index >= 0 && index + 1 < args.length ? args[index + 1] : null;
49
+ }
50
+
51
+ function positiveNumber(value) {
52
+ const parsed = Number(value);
53
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
54
+ }
55
+
56
+ // Most logical member: the one who owns the newest mission, else the first
57
+ // member on the team. Null when the workspace has no members yet.
58
+ function pickMember(root, preferredOwner = '') {
59
+ let members = [];
60
+ try {
61
+ const { findAllMembers } = require('./member');
62
+ members = findAllMembers(path.join(root, 'atris', 'team')) || [];
63
+ } catch {
64
+ return null;
65
+ }
66
+ if (!members.length) return null;
67
+ const owner = String(preferredOwner || '').trim().toLowerCase();
68
+ if (owner) {
69
+ const owned = members.find((member) => String(member?.name || '').trim().toLowerCase() === owner);
70
+ if (owned) return owned.name;
71
+ }
72
+ return members[0]?.name || null;
73
+ }
74
+
75
+ function newestMissionOwner(root) {
76
+ let map;
77
+ try { map = require('./mission').loadMissionMap(root); } catch { return ''; }
78
+ const newest = Array.from(map.values())
79
+ .sort((a, b) => String(b?.updated_at || b?.created_at || '').localeCompare(String(a?.updated_at || a?.created_at || '')));
80
+ return newest[0]?.owner || '';
81
+ }
82
+
83
+ function legPlan(root, legWallSeconds) {
84
+ const mission = pickRunnableMission(root);
85
+ if (mission) {
86
+ return {
87
+ kind: 'mission',
88
+ label: `mission ${mission.id}: ${mission.objective}`,
89
+ args: ['mission', 'run', mission.id, '--max-ticks', String(LEG_TICKS), '--max-wall', String(legWallSeconds), '--complete-on-pass'],
90
+ };
91
+ }
92
+ const member = pickMember(root, newestMissionOwner(root));
93
+ if (member) {
94
+ // --runner atris2: member run defaults to codex_goal, which waits for a
95
+ // live Codex session; a headless leg would tick it without doing work.
96
+ return {
97
+ kind: 'member',
98
+ label: `member ${member} chooses useful work`,
99
+ args: ['member', 'run', member, '--runner', 'atris2', '--minutes', String(Math.max(5, Math.round(legWallSeconds / 60)))],
100
+ };
101
+ }
102
+ return {
103
+ kind: 'auto-mission',
104
+ label: 'self-chosen mission from Atris state',
105
+ args: [
106
+ 'mission', 'run',
107
+ 'Choose one bounded useful task from Atris state, complete it, and show plain proof.',
108
+ '--owner', 'mission-lead',
109
+ '--runner', 'atris2',
110
+ '--max-ticks', String(LEG_TICKS),
111
+ '--max-wall', String(legWallSeconds),
112
+ ],
113
+ };
114
+ }
115
+
116
+ function driveLeg(root, legArgs, current) {
117
+ return new Promise((resolve) => {
118
+ const child = spawn(process.execPath, [CLI_PATH, ...legArgs], { cwd: root, stdio: 'inherit' });
119
+ current.child = child;
120
+ // Stop fast: a stop file written by `atris autopilot stop` in another
121
+ // terminal terminates the running leg, not just the loop between legs.
122
+ const poll = setInterval(() => {
123
+ if (stopRequested(root)) { try { child.kill('SIGTERM'); } catch {} }
124
+ }, 2000);
125
+ if (poll.unref) poll.unref();
126
+ const finish = (code) => {
127
+ clearInterval(poll);
128
+ current.child = null;
129
+ resolve(Number.isFinite(code) ? code : 1);
130
+ };
131
+ child.on('error', () => finish(1));
132
+ child.on('close', finish);
133
+ });
134
+ }
135
+
136
+ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
137
+
138
+ function autopilotStop(root = process.cwd()) {
139
+ requestStop(root);
140
+ const state = readState(root);
141
+ if (state && pidAlive(state.pid)) {
142
+ try { process.kill(state.pid, 'SIGTERM'); } catch {}
143
+ console.log(`Stopping autopilot (pid ${state.pid}).`);
144
+ } else {
145
+ console.log('No live autopilot process; stop marker written so the next start clears it.');
146
+ }
147
+ return 0;
148
+ }
149
+
150
+ function autopilotStatus(root = process.cwd()) {
151
+ const state = readState(root);
152
+ if (!state) { console.log('Autopilot is not running.'); return 0; }
153
+ const alive = pidAlive(state.pid);
154
+ console.log(`Autopilot ${alive ? 'running' : 'not running (stale state)'} — pid ${state.pid}, started ${state.started_at}, legs ${state.legs || 0}`);
155
+ if (state.current_leg) console.log(`Current leg: ${state.current_leg}`);
156
+ if (!alive) clearState(root);
157
+ return 0;
158
+ }
159
+
160
+ function showFrontHelp() {
161
+ console.log('');
162
+ console.log('Usage: atris autopilot [options]');
163
+ console.log('');
164
+ console.log('Keeps the workspace moving: picks the most logical mission or member,');
165
+ console.log('drives it through the mission runtime, then picks the next one.');
166
+ console.log('Runs until you stop it.');
167
+ console.log('');
168
+ console.log('Options:');
169
+ console.log(' --minutes N | --hours N Total budget (default: unlimited)');
170
+ console.log(' --leg-wall N Seconds per leg (default: 3600)');
171
+ console.log(' --once Run a single leg, then exit');
172
+ console.log(' --legacy Old suggest→justify→execute loop');
173
+ console.log('');
174
+ console.log('Control:');
175
+ console.log(' atris autopilot stop Stop fast, from any terminal');
176
+ console.log(' atris autopilot status Show what the loop is doing');
177
+ console.log('');
178
+ }
179
+
180
+ async function autopilotFront(args = []) {
181
+ const root = process.cwd();
182
+ if (args[0] === 'stop') return autopilotStop(root);
183
+ if (args[0] === 'status') return autopilotStatus(root);
184
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') { showFrontHelp(); return 0; }
185
+
186
+ const existing = readState(root);
187
+ if (existing && pidAlive(existing.pid) && existing.pid !== process.pid) {
188
+ console.log(`Autopilot already running (pid ${existing.pid}). Stop it first: atris autopilot stop`);
189
+ return 1;
190
+ }
191
+
192
+ clearStop(root);
193
+ const budgetSeconds = runBudgetSeconds(args);
194
+ const legWallDefault = positiveNumber(readValueFlag(args, '--leg-wall')) || DEFAULT_LEG_WALL_SECONDS;
195
+ const once = args.includes('--once');
196
+ const startedAt = Date.now();
197
+ const state = { pid: process.pid, started_at: new Date().toISOString(), legs: 0 };
198
+ writeState(root, state);
199
+
200
+ const current = { child: null };
201
+ // `autopilot stop` SIGTERMs this pid directly, so the farewell must print
202
+ // here — the signal always beats the loop's own stop-file check.
203
+ const onSignal = () => {
204
+ if (current.child) { try { current.child.kill('SIGTERM'); } catch {} }
205
+ const reason = stopRequested(root) ? 'stop requested' : 'interrupted';
206
+ console.log(`\nAutopilot off (${reason}). Legs run: ${state.legs}.`);
207
+ clearState(root);
208
+ clearStop(root);
209
+ process.exit(130);
210
+ };
211
+ process.on('SIGINT', onSignal);
212
+ process.on('SIGTERM', onSignal);
213
+
214
+ console.log('Autopilot on. Stop anytime: Ctrl-C here, or `atris autopilot stop` anywhere.');
215
+
216
+ let fastFails = 0;
217
+ let stopReason = '';
218
+ while (true) {
219
+ if (stopRequested(root)) { stopReason = 'stop requested'; break; }
220
+ const elapsed = Math.round((Date.now() - startedAt) / 1000);
221
+ if (budgetSeconds && elapsed >= budgetSeconds) { stopReason = 'budget spent'; break; }
222
+ const legWall = budgetSeconds ? Math.min(legWallDefault, budgetSeconds - elapsed) : legWallDefault;
223
+
224
+ const plan = legPlan(root, Math.max(60, legWall));
225
+ state.legs += 1;
226
+ state.current_leg = plan.label;
227
+ state.updated_at = new Date().toISOString();
228
+ writeState(root, state);
229
+ console.log(`\nautopilot leg ${state.legs}: ${plan.label}`);
230
+
231
+ const legStarted = Date.now();
232
+ const code = await driveLeg(root, plan.args, current);
233
+ state.last_exit = code;
234
+ writeState(root, state);
235
+
236
+ if (once) { stopReason = 'single leg (--once)'; break; }
237
+ if (stopRequested(root)) { stopReason = 'stop requested'; break; }
238
+
239
+ // A leg that finishes in seconds means no real work happened — a crash,
240
+ // or a degenerate mission whose ticks record instantly (exit 0). Either
241
+ // way, back off and stop instead of hot-looping on it.
242
+ const legSeconds = (Date.now() - legStarted) / 1000;
243
+ if (legSeconds < FAST_FAIL_SECONDS) {
244
+ fastFails += 1;
245
+ if (fastFails >= MAX_FAST_FAILS) {
246
+ stopReason = `${MAX_FAST_FAILS} fast legs in a row (${code === 0 ? 'no progress' : 'failures'})`;
247
+ break;
248
+ }
249
+ await sleep(code === 0 ? 5000 : 15000);
250
+ } else {
251
+ fastFails = 0;
252
+ await sleep(2000);
253
+ }
254
+ }
255
+
256
+ clearStop(root);
257
+ clearState(root);
258
+ console.log(`\nAutopilot off (${stopReason}). Legs run: ${state.legs}.`);
259
+ return stopReason.includes('fast legs') ? 1 : 0;
260
+ }
261
+
262
+ module.exports = {
263
+ autopilotFront,
264
+ autopilotStop,
265
+ autopilotStatus,
266
+ legPlan,
267
+ pickMember,
268
+ stopRequested,
269
+ requestStop,
270
+ clearStop,
271
+ readState,
272
+ writeState,
273
+ };
@@ -0,0 +1,299 @@
1
+ 'use strict';
2
+
3
+ // atris engine: bring any intelligence.
4
+ // Every installed headless coding CLI is a swappable worker behind one
5
+ // contract (bounded prompt in, verified proof out, engines never
6
+ // self-certify). This command shows the roster, flips the default, and the
7
+ // same names ride --engine on mission run / autopilot / run.
8
+ //
9
+ // atris engine roster + current default
10
+ // atris engine cursor make cursor the default engine here
11
+ // atris engine reset back to the house default (atris-fast)
12
+ //
13
+ // The default persists to .atris/engine.json. Per-run flags and
14
+ // ATRIS_RUNNER_PROFILE always beat the file.
15
+
16
+ const fs = require('fs');
17
+ const os = require('os');
18
+ const path = require('path');
19
+ const { spawnSync } = require('child_process');
20
+ const {
21
+ RUNNER_PROFILE_DEFS,
22
+ RUNNER_PROFILE_ALIASES,
23
+ RUNNER_PROFILE_NAMES,
24
+ buildRunnerCommand,
25
+ } = require('../lib/runner-command');
26
+
27
+ const HOUSE_ENGINE = 'atris-fast';
28
+
29
+ function engineFile(root = process.cwd()) {
30
+ return path.join(root, '.atris', 'engine.json');
31
+ }
32
+
33
+ function binInstalled(bin) {
34
+ const safe = String(bin || '').replace(/[^A-Za-z0-9_.-]/g, '');
35
+ if (!safe) return false;
36
+ const probe = spawnSync('sh', ['-c', `command -v ${safe}`], { encoding: 'utf8' });
37
+ return probe.status === 0 && Boolean(String(probe.stdout || '').trim());
38
+ }
39
+
40
+ function canonicalEngineName(name) {
41
+ const trimmed = String(name || '').trim();
42
+ if (!trimmed) return '';
43
+ if (RUNNER_PROFILE_DEFS[trimmed]) return trimmed;
44
+ if (RUNNER_PROFILE_ALIASES[trimmed]) return RUNNER_PROFILE_ALIASES[trimmed];
45
+ return '';
46
+ }
47
+
48
+ function readSavedEngine(root = process.cwd()) {
49
+ try {
50
+ const saved = JSON.parse(fs.readFileSync(engineFile(root), 'utf8'));
51
+ return canonicalEngineName(saved.default);
52
+ } catch {
53
+ return '';
54
+ }
55
+ }
56
+
57
+ // The default engine for this workspace, in precedence order:
58
+ // env (per-run flags land here) -> .atris/engine.json -> house default.
59
+ // The house default is our own intelligence when it is installed.
60
+ function resolveDefaultEngine(root = process.cwd()) {
61
+ const env = canonicalEngineName(process.env.ATRIS_RUNNER_PROFILE);
62
+ if (env) return { name: env, source: 'env' };
63
+ const saved = readSavedEngine(root);
64
+ if (saved) return { name: saved, source: 'saved' };
65
+ if (binInstalled(RUNNER_PROFILE_DEFS[HOUSE_ENGINE].bin)) return { name: HOUSE_ENGINE, source: 'house' };
66
+ const fallback = RUNNER_PROFILE_NAMES.find((name) => binInstalled(RUNNER_PROFILE_DEFS[name].bin));
67
+ return fallback ? { name: fallback, source: 'detected' } : { name: HOUSE_ENGINE, source: 'none' };
68
+ }
69
+
70
+ function roster(root = process.cwd()) {
71
+ const current = resolveDefaultEngine(root);
72
+ return RUNNER_PROFILE_NAMES.map((name) => ({
73
+ name,
74
+ bin: RUNNER_PROFILE_DEFS[name].bin,
75
+ installed: binInstalled(RUNNER_PROFILE_DEFS[name].bin),
76
+ default: name === current.name,
77
+ }));
78
+ }
79
+
80
+ function setEngine(name, root = process.cwd()) {
81
+ const canonical = canonicalEngineName(name);
82
+ if (!canonical) {
83
+ throw new Error(`Unknown engine "${name}". Known engines: ${RUNNER_PROFILE_NAMES.join(', ')}`);
84
+ }
85
+ const file = engineFile(root);
86
+ fs.mkdirSync(path.dirname(file), { recursive: true });
87
+ fs.writeFileSync(file, `${JSON.stringify({ default: canonical, set_at: new Date().toISOString() }, null, 2)}\n`);
88
+ return canonical;
89
+ }
90
+
91
+ function resetEngine(root = process.cwd()) {
92
+ try { fs.unlinkSync(engineFile(root)); return true; } catch { return false; }
93
+ }
94
+
95
+ function printRoster(root) {
96
+ const list = roster(root);
97
+ const found = list.filter((e) => e.installed).length;
98
+ const current = resolveDefaultEngine(root);
99
+ console.log('');
100
+ console.log(` engines — ${found} intelligence${found === 1 ? '' : 's'} found`);
101
+ console.log('');
102
+ for (const engine of list) {
103
+ const mark = engine.default ? '→' : ' ';
104
+ const state = engine.installed ? 'ready' : 'not installed';
105
+ console.log(` ${mark} ${engine.name.padEnd(12)} ${state}`);
106
+ }
107
+ console.log('');
108
+ console.log(` default: ${current.name}${current.source === 'saved' ? ' (set here)' : current.source === 'env' ? ' (this session)' : ''}`);
109
+ console.log(` switch: atris engine <name> · one run: --engine <name> on mission run / autopilot / run`);
110
+ console.log('');
111
+ }
112
+
113
+ // Preflight: run one engine CLI headless with a reply-OK prompt and report
114
+ // pass/fail. A dead login, missing binary, or hung spawn is a one-command
115
+ // diagnosis instead of a failed overnight flight. `name` is canonical.
116
+ const PROBE_PROMPT = 'Reply with exactly the two characters: OK';
117
+ // Real engines think before replying: cursor measured at ~68s for a one-word
118
+ // answer on 2026-07-02. 30s produced a false FAIL on a healthy engine.
119
+ const PROBE_DEFAULT_TIMEOUT_MS = 120000;
120
+
121
+ function probeEngine(name, { timeout = PROBE_DEFAULT_TIMEOUT_MS } = {}) {
122
+ const def = RUNNER_PROFILE_DEFS[name];
123
+ if (!binInstalled(def.bin)) {
124
+ return {
125
+ engine: name,
126
+ bin: def.bin,
127
+ pass: false,
128
+ reason: 'not-installed',
129
+ message: `${def.bin} CLI not installed`,
130
+ stdout: '',
131
+ stderr: '',
132
+ durationMs: 0,
133
+ };
134
+ }
135
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-engine-probe-'));
136
+ const promptFile = path.join(tmpDir, 'prompt.txt');
137
+ fs.writeFileSync(promptFile, `${PROBE_PROMPT}\n`);
138
+ let cmd;
139
+ const prevProfile = process.env.ATRIS_RUNNER_PROFILE;
140
+ process.env.ATRIS_RUNNER_PROFILE = name;
141
+ try {
142
+ cmd = buildRunnerCommand({ promptFile });
143
+ } finally {
144
+ if (prevProfile === undefined) delete process.env.ATRIS_RUNNER_PROFILE;
145
+ else process.env.ATRIS_RUNNER_PROFILE = prevProfile;
146
+ }
147
+ const start = Date.now();
148
+ let res;
149
+ try {
150
+ res = spawnSync('sh', ['-c', cmd], { encoding: 'utf8', timeout });
151
+ } catch (err) {
152
+ fs.rmSync(tmpDir, { recursive: true, force: true });
153
+ return {
154
+ engine: name,
155
+ bin: def.bin,
156
+ pass: false,
157
+ reason: 'spawn-error',
158
+ message: String(err && err.message ? err.message : err),
159
+ stdout: '',
160
+ stderr: '',
161
+ durationMs: Date.now() - start,
162
+ };
163
+ }
164
+ const durationMs = Date.now() - start;
165
+ fs.rmSync(tmpDir, { recursive: true, force: true });
166
+ const stdout = res.stdout || '';
167
+ const stderr = res.stderr || '';
168
+ const timedOut = res.status === null && Boolean(res.signal) && String(res.signal).toLowerCase().includes('term');
169
+ const combined = `${stdout}\n${stderr}`.trim();
170
+ const ok = res.status === 0 && /OK/i.test(combined);
171
+ let reason;
172
+ if (ok) reason = 'ok';
173
+ else if (timedOut) reason = 'timeout';
174
+ else if (res.status !== 0 && res.status !== null) reason = 'bad-exit';
175
+ else if (!/OK/i.test(combined)) reason = 'no-ok';
176
+ else reason = 'unknown';
177
+ return {
178
+ engine: name,
179
+ bin: def.bin,
180
+ pass: ok,
181
+ reason,
182
+ message: ok
183
+ ? 'responded OK'
184
+ : (timedOut ? `no reply within ${timeout}ms` : `did not reply OK (exit ${res.status}, signal ${res.signal})`),
185
+ stdout,
186
+ stderr,
187
+ durationMs,
188
+ };
189
+ }
190
+
191
+ function runEngineTest(targets, { json, root } = {}) {
192
+ let enginesToTest;
193
+ if (targets && targets.length) {
194
+ enginesToTest = targets.map((n) => {
195
+ const c = canonicalEngineName(n);
196
+ if (!c) {
197
+ throw new Error(`Unknown engine "${n}". Known engines: ${RUNNER_PROFILE_NAMES.join(', ')}`);
198
+ }
199
+ return c;
200
+ });
201
+ } else {
202
+ enginesToTest = RUNNER_PROFILE_NAMES.filter((n) => binInstalled(RUNNER_PROFILE_DEFS[n].bin));
203
+ if (!enginesToTest.length) {
204
+ if (json) {
205
+ console.log(JSON.stringify({ ok: false, results: [], summary: { pass: 0, fail: 0 } }, null, 2));
206
+ } else {
207
+ console.error('\n no installed engines to test\n');
208
+ }
209
+ return 1;
210
+ }
211
+ }
212
+ const results = enginesToTest.map((name) => probeEngine(name));
213
+ const failures = results.filter((r) => !r.pass);
214
+ const passed = results.length - failures.length;
215
+ if (json) {
216
+ console.log(JSON.stringify({
217
+ ok: failures.length === 0,
218
+ results,
219
+ summary: { pass: passed, fail: failures.length },
220
+ }, null, 2));
221
+ } else {
222
+ console.log('');
223
+ for (const r of results) {
224
+ const mark = r.pass ? '✓' : '✗';
225
+ const line = r.pass
226
+ ? ` ${mark} ${r.engine.padEnd(12)} pass — ${r.message} (${r.durationMs}ms)`
227
+ : ` ${mark} ${r.engine.padEnd(12)} FAIL — ${r.message}`;
228
+ if (r.pass) console.log(line);
229
+ else console.error(line);
230
+ }
231
+ console.log('');
232
+ if (failures.length) {
233
+ console.error(` ${failures.length} engine${failures.length === 1 ? '' : 's'} failed: ${failures.map((f) => f.engine).join(', ')}`);
234
+ console.error(` fix the login/binary, then re-run: atris engine test${targets && targets.length ? ' ' + targets.join(' ') : ''}`);
235
+ } else {
236
+ console.log(` all engines responded — clear for flight`);
237
+ }
238
+ console.log('');
239
+ }
240
+ return failures.length ? 1 : 0;
241
+ }
242
+
243
+ function engineCommand(args = []) {
244
+ const json = args.includes('--json');
245
+ const positional = args.filter((a) => !a.startsWith('--'));
246
+ const sub = (positional[0] || '').trim();
247
+ const root = process.cwd();
248
+
249
+ if (sub === 'test') {
250
+ return runEngineTest(positional.slice(1), { json, root });
251
+ }
252
+
253
+ if (!sub || sub === 'list' || sub === 'status') {
254
+ if (json) {
255
+ const current = resolveDefaultEngine(root);
256
+ console.log(JSON.stringify({ engines: roster(root), default: current.name, source: current.source }, null, 2));
257
+ return 0;
258
+ }
259
+ printRoster(root);
260
+ return 0;
261
+ }
262
+
263
+ if (sub === 'reset' || sub === 'off') {
264
+ const removed = resetEngine(root);
265
+ console.log(removed
266
+ ? `\n default engine reset — back to ${resolveDefaultEngine(root).name}\n`
267
+ : `\n nothing to reset — no engine was set here\n`);
268
+ return 0;
269
+ }
270
+
271
+ if (sub === 'help') {
272
+ console.log('\n atris engine roster + current default\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 reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
273
+ return 0;
274
+ }
275
+
276
+ // atris engine <name> — flip the default.
277
+ const canonical = setEngine(sub, root);
278
+ const def = RUNNER_PROFILE_DEFS[canonical];
279
+ const installed = binInstalled(def.bin);
280
+ console.log('');
281
+ console.log(` default engine: ${canonical}`);
282
+ if (!installed) console.log(` heads up: its CLI (${def.bin}) is not installed here yet — runs will fail until it is.`);
283
+ console.log(` every mission run / autopilot / run tick now rides it. one-off: --engine <name>. undo: atris engine reset`);
284
+ console.log('');
285
+ return 0;
286
+ }
287
+
288
+ module.exports = {
289
+ engineCommand,
290
+ resolveDefaultEngine,
291
+ canonicalEngineName,
292
+ readSavedEngine,
293
+ setEngine,
294
+ resetEngine,
295
+ roster,
296
+ probeEngine,
297
+ runEngineTest,
298
+ HOUSE_ENGINE,
299
+ };
package/commands/init.js CHANGED
@@ -660,6 +660,20 @@ Do not rely on chat context. Put the task, file pointers, and proof on disk.
660
660
  Do not write new operating doctrine here first; add it to Atris policy, skills,
661
661
  wiki, or \`atris/atris.md\`, then regenerate this adapter if needed.
662
662
 
663
+ ## How to Report
664
+
665
+ The human approves work by reading, so how you report IS the product. Rules:
666
+
667
+ - **Results are capabilities.** State what someone can do now that they
668
+ couldn't before, then what it means for them or the business. Tests are one
669
+ word ("verified"); the meaning is the sentence. Shape: "We did X, so you can
670
+ now Y." Detail stays under the hood; the reader asks if they want more.
671
+ - **Three results, air between them, rest on ask.** A report fits one screen
672
+ with no scrolling. Reading it is one glance.
673
+ - **Stake first, then the move.** "Agents burn tokens hand-rolling parsers:
674
+ add one shared view." Plain words; flags, ids, and identifiers belong in the
675
+ body, never the headline.
676
+
663
677
  Native goals and task approval are separate gates:
664
678
 
665
679
  \`\`\`text
@@ -980,6 +994,13 @@ Read atris/MAP.md. Begin iteration 1.`;
980
994
  console.log('✓ Created .claude/settings.json (auto-loads Atris on startup)');
981
995
  }
982
996
 
997
+ // Co-author trailer: commits in this workspace credit Atris, same as Claude/Cursor do
998
+ try {
999
+ const { installSignHook } = require('./sign');
1000
+ const { already } = installSignHook();
1001
+ if (!already) console.log('✓ Installed co-author hook (commits credit Atris — remove with `atris sign off`)');
1002
+ } catch {} // not a git repo — skip quietly
1003
+
983
1004
  // Update root CLAUDE.md with Atris block (prepend with markers)
984
1005
  const rootClaudeMd = path.join(process.cwd(), 'CLAUDE.md');
985
1006
  const atrisBlock = `<!-- ATRIS:START - Auto-generated, do not edit -->