atris 3.15.57 → 3.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/AGENTS.md +2 -2
  2. package/GETTING_STARTED.md +1 -1
  3. package/PERSONA.md +4 -4
  4. package/README.md +12 -11
  5. package/atris/skills/copy-editor/SKILL.md +30 -4
  6. package/atris/skills/improve/SKILL.md +18 -20
  7. package/atris/wiki/concepts/agent-activation-contract.md +5 -3
  8. package/atris/wiki/concepts/workspace-initialization-contract.md +4 -4
  9. package/atris/wiki/index.md +1 -0
  10. package/ax +522 -73
  11. package/bin/atris.js +78 -44
  12. package/commands/align.js +0 -14
  13. package/commands/apps.js +102 -1
  14. package/commands/autopilot.js +628 -31
  15. package/commands/brain.js +219 -34
  16. package/commands/brainstorm.js +0 -829
  17. package/commands/compile.js +569 -0
  18. package/commands/computer.js +0 -60
  19. package/commands/improve.js +501 -0
  20. package/commands/integrations.js +233 -71
  21. package/commands/lesson.js +44 -0
  22. package/commands/member.js +4498 -226
  23. package/commands/mission.js +302 -27
  24. package/commands/now.js +89 -1
  25. package/commands/probe.js +366 -0
  26. package/commands/radar.js +181 -56
  27. package/commands/recap.js +203 -0
  28. package/commands/skill.js +6 -2
  29. package/commands/soul.js +0 -4
  30. package/commands/task.js +5587 -499
  31. package/commands/terminal.js +14 -10
  32. package/commands/wiki.js +87 -1
  33. package/commands/workflow.js +288 -73
  34. package/commands/worktree.js +52 -15
  35. package/commands/xp.js +6 -65
  36. package/lib/auto-accept-certified.js +294 -0
  37. package/lib/file-ops.js +0 -184
  38. package/lib/member-alive.js +232 -0
  39. package/lib/policy-lessons.js +280 -0
  40. package/lib/receipt-evidence.js +64 -0
  41. package/lib/state-detection.js +75 -1
  42. package/lib/task-db.js +568 -16
  43. package/lib/task-proof.js +43 -0
  44. package/package.json +1 -1
  45. package/utils/auth.js +13 -4
  46. package/commands/research.js +0 -52
  47. package/lib/section-merge.js +0 -196
@@ -0,0 +1,501 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * atris improve — run one paid RL improvement tick on the workspace.
5
+ *
6
+ * Calls POST /api/improve on the backend, which plans one task, builds it,
7
+ * runs the verify command, scores it, and deducts Atris credits per
8
+ * successful tick. Returns what shipped + reward and writes a per-tick
9
+ * scorecard row to .atris/state/scorecards.jsonl (the receipt the brain
10
+ * ledger already counts).
11
+ *
12
+ * This is the CLI entrypoint for the headline paid capability. The member
13
+ * loop and the /improve skill both call it. If the backend is unreachable
14
+ * or the user is not logged in, it falls back to a local autopilot tick
15
+ * (same loop, local inference) instead of erroring silently.
16
+ *
17
+ * The orchestrator (runImprove) takes injected deps so the network, auth,
18
+ * fallback, and scorecard writes can all be faked in tests.
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const os = require('os');
23
+ const path = require('path');
24
+ const { spawnSync } = require('child_process');
25
+ const { apiRequestJson, getApiBaseUrl } = require('../utils/api');
26
+ const { loadCredentials } = require('../utils/auth');
27
+
28
+ /**
29
+ * Expand a leading `~` to the real home directory for LOCAL filesystem
30
+ * writes. The path sent to the backend is left untouched — a remote tick
31
+ * may target a server-side `~/...` workspace the server expands itself — but
32
+ * local scorecard/journal writes must never create a literal `~` directory.
33
+ * (Surfaced by a live plan tick whose `~/arena/...` arg wrote junk locally.)
34
+ */
35
+ function expandHome(p) {
36
+ if (typeof p === 'string' && (p === '~' || p.startsWith('~/'))) {
37
+ return path.join(os.homedir(), p.slice(1));
38
+ }
39
+ return p;
40
+ }
41
+
42
+ const SCORECARD_SCHEMA = 'atris.improve_tick.v1';
43
+ const DEFAULT_TIMEOUT_MS = 300000;
44
+ const VALID_MODES = new Set(['full', 'plan', 'delegate']);
45
+
46
+ /**
47
+ * Resolve the improve endpoint path relative to the configured API base.
48
+ * The backend mounts the router at /api/improve. The CLI's default base
49
+ * (https://api.atris.ai/api) already includes /api, so we append /improve;
50
+ * a bare base (e.g. http://localhost:8000) needs the full /api/improve.
51
+ */
52
+ function improveApiPath(baseUrl) {
53
+ const base = String(baseUrl || '').replace(/\/+$/, '');
54
+ return base.endsWith('/api') ? '/improve' : '/api/improve';
55
+ }
56
+
57
+ function parseImproveArgs(argv = []) {
58
+ const args = Array.isArray(argv) ? [...argv] : [];
59
+ const opts = {
60
+ mode: 'full',
61
+ model: null,
62
+ member: null,
63
+ dryRun: false,
64
+ json: false,
65
+ fallback: true,
66
+ workspace: process.cwd(),
67
+ timeoutMs: DEFAULT_TIMEOUT_MS,
68
+ help: false,
69
+ history: false,
70
+ };
71
+
72
+ for (let i = 0; i < args.length; i++) {
73
+ const a = args[i];
74
+ if (a === '--help' || a === '-h') { opts.help = true; continue; }
75
+ if (a === 'history' || a === '--history') { opts.history = true; continue; }
76
+ if (a === '--json') { opts.json = true; continue; }
77
+ if (a === '--no-fallback') { opts.fallback = false; continue; }
78
+ if (a === '--dry-run' || a === 'dry-run' || a === 'dry_run') { opts.dryRun = true; continue; }
79
+ if (a === '--mode') { const v = args[++i]; if (v) opts.mode = v; continue; }
80
+ if (a === '--model') { const v = args[++i]; if (v) opts.model = v; continue; }
81
+ if (a === '--member') { const v = args[++i]; if (v) opts.member = v; continue; }
82
+ if (a === '--workspace') { const v = args[++i]; if (v) opts.workspace = v; continue; }
83
+ if (a === '--timeout') { const v = Number(args[++i]); if (v > 0) opts.timeoutMs = Math.round(v * 1000); continue; }
84
+ if (a.startsWith('--timeout=')) { const v = Number(a.split('=')[1]); if (v > 0) opts.timeoutMs = Math.round(v * 1000); continue; }
85
+ // positional mode (plan|full|delegate)
86
+ if (!a.startsWith('-') && VALID_MODES.has(a)) { opts.mode = a; continue; }
87
+ }
88
+
89
+ if (!VALID_MODES.has(opts.mode)) opts.mode = 'full';
90
+ return opts;
91
+ }
92
+
93
+ function buildImprovePayload(opts = {}) {
94
+ const body = {
95
+ workspace: opts.workspace || process.cwd(),
96
+ mode: opts.mode || 'full',
97
+ };
98
+ if (opts.model) body.model = opts.model;
99
+ if (opts.dryRun) body.dry_run = true;
100
+ if (opts.businessId) body.business_id = opts.businessId;
101
+ return body;
102
+ }
103
+
104
+ /**
105
+ * Normalize the /api/improve response into a stable summary, reading every
106
+ * field defensively. The full-mode ImproveResponse omits credits_deducted
107
+ * (credits are still billed server-side), so credits may be null even on a
108
+ * successful, charged tick — callers must not assume it is present.
109
+ */
110
+ function summarizeImproveResponse(data = {}) {
111
+ const d = data && typeof data === 'object' ? data : {};
112
+ const verify = d.verify_passed != null ? d.verify_passed
113
+ : (d.verify_pass != null ? d.verify_pass : null);
114
+ const credits = d.credits_deducted != null ? d.credits_deducted
115
+ : (d.credits_charged != null ? d.credits_charged : null);
116
+ const files = Array.isArray(d.files_written) ? d.files_written
117
+ : Array.isArray(d.files_changed) ? d.files_changed
118
+ : Array.isArray(d.files) ? d.files : [];
119
+ const reward = typeof d.reward === 'number' ? d.reward
120
+ : (d.reward != null && !Number.isNaN(Number(d.reward)) ? Number(d.reward) : null);
121
+ return {
122
+ shipped: d.what_shipped || d.summary || d.task_description || d.task || null,
123
+ reward,
124
+ verify,
125
+ credits,
126
+ files,
127
+ model: d.model_used || d.model || null,
128
+ taskId: d.task_id || d.taskId || null,
129
+ elapsedMs: typeof d.elapsed_ms === 'number' ? d.elapsed_ms : null,
130
+ scorecardWritten: d.scorecard_written === true,
131
+ error: d.error || null,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Decide whether to fall back to a local autopilot tick. Fallback only when
137
+ * the backend is genuinely unavailable: no auth, or unreachable (status 0).
138
+ * A real HTTP error (insufficient credits 402, server error 5xx) is reported
139
+ * honestly — we never silently run local work and bill nothing on what was a
140
+ * real, answerable failure.
141
+ */
142
+ function shouldFallbackLocal({ creds, apiResult } = {}) {
143
+ if (!creds || !creds.token) return { fallback: true, reason: 'no_auth' };
144
+ if (!apiResult) return { fallback: false, reason: 'no_result' };
145
+ if (apiResult.ok) return { fallback: false, reason: 'api_ok' };
146
+ if (apiResult.status === 0) return { fallback: true, reason: 'unreachable' };
147
+ return { fallback: false, reason: `api_error_${apiResult.status}` };
148
+ }
149
+
150
+ function buildScorecardRow(summary = {}, meta = {}) {
151
+ return {
152
+ schema: SCORECARD_SCHEMA,
153
+ ts: meta.ts || new Date().toISOString(),
154
+ source: meta.source || 'api',
155
+ member: meta.member || null,
156
+ mode: meta.mode || 'full',
157
+ reward: summary.reward != null ? summary.reward : null,
158
+ verify_passed: summary.verify != null ? summary.verify : null,
159
+ credits_deducted: summary.credits != null ? summary.credits : null,
160
+ what_shipped: summary.shipped || null,
161
+ files_written: Array.isArray(summary.files) ? summary.files : [],
162
+ model_used: summary.model || null,
163
+ task_id: summary.taskId || null,
164
+ elapsed_ms: summary.elapsedMs != null ? summary.elapsedMs : null,
165
+ };
166
+ }
167
+
168
+ function appendScorecardRow(workspace, row) {
169
+ const dir = path.join(expandHome(workspace), '.atris', 'state');
170
+ fs.mkdirSync(dir, { recursive: true });
171
+ const file = path.join(dir, 'scorecards.jsonl');
172
+ fs.appendFileSync(file, `${JSON.stringify(row)}\n`, 'utf8');
173
+ return file;
174
+ }
175
+
176
+ /**
177
+ * Read the improve-tick scorecard rows the loop writes. This is the
178
+ * substrate that makes the loop recursive: each tick appends a receipt,
179
+ * and the next tick (or a human) reads the accumulated rows to see whether
180
+ * the loop is actually compounding.
181
+ */
182
+ function readTickHistory(workspace) {
183
+ const file = path.join(expandHome(workspace), '.atris', 'state', 'scorecards.jsonl');
184
+ if (!fs.existsSync(file)) return [];
185
+ const rows = [];
186
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
187
+ const trimmed = line.trim();
188
+ if (!trimmed) continue;
189
+ try {
190
+ const row = JSON.parse(trimmed);
191
+ if (row && row.schema === SCORECARD_SCHEMA) rows.push(row);
192
+ } catch {
193
+ // skip non-JSON / foreign rows
194
+ }
195
+ }
196
+ return rows;
197
+ }
198
+
199
+ /**
200
+ * Summarize the tick history into the compounding signal: how many ticks
201
+ * shipped, the reward trend, total credits spent, and the verify pass rate.
202
+ * This is the answer to "is the self-improvement loop getting better?".
203
+ */
204
+ function summarizeTickHistory(rows = []) {
205
+ const ticks = Array.isArray(rows) ? rows : [];
206
+ const verified = ticks.filter((r) => r.verify_passed === true);
207
+ const rewards = ticks.map((r) => (typeof r.reward === 'number' ? r.reward : 0));
208
+ const totalReward = rewards.reduce((a, b) => a + b, 0);
209
+ const totalCredits = ticks.reduce((a, r) => a + (typeof r.credits_deducted === 'number' ? r.credits_deducted : 0), 0);
210
+ const rewardTrend = ticks.map((r) => (typeof r.reward === 'number' ? r.reward : null));
211
+ return {
212
+ ticks: ticks.length,
213
+ shipped: verified.length,
214
+ passRate: ticks.length ? verified.length / ticks.length : 0,
215
+ totalReward,
216
+ avgReward: ticks.length ? totalReward / ticks.length : 0,
217
+ totalCredits,
218
+ rewardTrend,
219
+ first: ticks[0] || null,
220
+ latest: ticks[ticks.length - 1] || null,
221
+ };
222
+ }
223
+
224
+ function formatTickHistory(summary = {}) {
225
+ const lines = [];
226
+ lines.push('improve loop — tick history');
227
+ lines.push('');
228
+ lines.push(` ticks: ${summary.ticks}`);
229
+ lines.push(` shipped: ${summary.shipped}/${summary.ticks} (verify pass ${Math.round((summary.passRate || 0) * 100)}%)`);
230
+ lines.push(` reward: total ${summary.totalReward}, avg ${(summary.avgReward || 0).toFixed(1)}`);
231
+ lines.push(` credits: ${summary.totalCredits} deducted`);
232
+ if (summary.rewardTrend && summary.rewardTrend.length) {
233
+ lines.push(` trend: ${summary.rewardTrend.map((r) => (r == null ? '·' : r)).join(' → ')}`);
234
+ }
235
+ if (!summary.ticks) lines.push(' (no ticks yet — run `atris improve`)');
236
+ return lines.join('\n');
237
+ }
238
+
239
+ // Local calendar day — journal receipts are local workspace files, never UTC
240
+ // (see lessons.md: now-front-door-uses-local-date).
241
+ function localDateKey(d = new Date()) {
242
+ const y = d.getFullYear();
243
+ const m = String(d.getMonth() + 1).padStart(2, '0');
244
+ const day = String(d.getDate()).padStart(2, '0');
245
+ return `${y}-${m}-${day}`;
246
+ }
247
+
248
+ function localHourMinute(d = new Date()) {
249
+ return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
250
+ }
251
+
252
+ /**
253
+ * Append a human-readable tick entry to today's journal under ## Notes.
254
+ * The skill contract says every tick lands in the journal; the JSONL
255
+ * scorecard is the machine receipt, this is the operator-readable trail.
256
+ */
257
+ function appendTickToJournal(workspace, summary = {}, opts = {}) {
258
+ const dateKey = opts.dateKey || localDateKey();
259
+ const time = opts.time || localHourMinute();
260
+ const year = dateKey.slice(0, 4);
261
+ const logFile = path.join(expandHome(workspace), 'atris', 'logs', year, `${dateKey}.md`);
262
+ fs.mkdirSync(path.dirname(logFile), { recursive: true });
263
+
264
+ const source = opts.source || 'api';
265
+ const verify = summary.verify === true ? 'pass' : summary.verify === false ? 'fail' : 'unknown';
266
+ const credits = summary.credits != null ? summary.credits : 'server-side';
267
+ const owner = opts.member ? ` · member: ${opts.member}` : '';
268
+ const block = [
269
+ `### Improve Tick — ${time}`,
270
+ `- shipped: ${summary.shipped || '(no description)'}`,
271
+ `- verify: ${verify} · reward: ${summary.reward != null ? summary.reward : '?'} · credits: ${credits} · source: ${source}${owner}`,
272
+ '',
273
+ ].join('\n');
274
+
275
+ let content = fs.existsSync(logFile) ? fs.readFileSync(logFile, 'utf8') : `# ${dateKey}\n\n## Notes\n`;
276
+ if (content.includes('## Notes')) {
277
+ content = content.replace('## Notes\n', `## Notes\n\n${block}`);
278
+ } else {
279
+ content = `${content.replace(/\n*$/, '')}\n\n## Notes\n\n${block}`;
280
+ }
281
+ fs.writeFileSync(logFile, content, 'utf8');
282
+ return logFile;
283
+ }
284
+
285
+ function resolveAtrisBin() {
286
+ const local = path.join(__dirname, '..', 'bin', 'atris.js');
287
+ if (fs.existsSync(local)) return local;
288
+ return process.env.ATRIS_BIN || 'atris';
289
+ }
290
+
291
+ function runLocalFallback(opts = {}) {
292
+ const bin = resolveAtrisBin();
293
+ const isScript = bin.endsWith('.js');
294
+ const cmd = isScript ? process.execPath : bin;
295
+ const argv = (isScript ? [bin] : []).concat(['autopilot', '--auto', '--iterations=1']);
296
+ const r = spawnSync(cmd, argv, {
297
+ cwd: opts.workspace || process.cwd(),
298
+ encoding: 'utf8',
299
+ env: process.env,
300
+ stdio: opts.json ? ['ignore', 'pipe', 'pipe'] : 'inherit',
301
+ timeout: Math.max(60, Number(opts.timeoutSec) || 600) * 1000,
302
+ });
303
+ return {
304
+ ok: r.status === 0,
305
+ status: r.status == null ? 1 : r.status,
306
+ stdout: r.stdout || '',
307
+ stderr: r.stderr || '',
308
+ };
309
+ }
310
+
311
+ /**
312
+ * Run one improvement tick. Dependency-injected so tests can fake the
313
+ * network (apiRequestJson), auth (loadCredentials), the local fallback,
314
+ * and the scorecard sink.
315
+ *
316
+ * Returns a structured result:
317
+ * { ok, source: 'api'|'local'|'none', reason, summary?, scorecardPath?, local?, apiResult?, error? }
318
+ */
319
+ async function runImprove(opts = {}, deps = {}) {
320
+ const apiFn = deps.apiRequestJson || apiRequestJson;
321
+ const loadCreds = deps.loadCredentials || loadCredentials;
322
+ const localFn = deps.runLocalFallback || runLocalFallback;
323
+ const writeRow = deps.appendScorecardRow || appendScorecardRow;
324
+ const writeJournal = deps.appendTickToJournal || appendTickToJournal;
325
+ const baseFn = deps.getApiBaseUrl || getApiBaseUrl;
326
+ const now = deps.now || (() => new Date().toISOString());
327
+ const log = deps.log || (() => {});
328
+
329
+ const workspace = opts.workspace || process.cwd();
330
+ const timeoutSec = Math.round((opts.timeoutMs || DEFAULT_TIMEOUT_MS) / 1000);
331
+ const startedAt = now();
332
+ const creds = loadCreds();
333
+
334
+ // No auth → local fallback (or report if fallback disabled).
335
+ if (!creds || !creds.token) {
336
+ if (!opts.fallback) {
337
+ return {
338
+ ok: false, source: 'none', reason: 'no_auth',
339
+ error: 'Not logged in and --no-fallback set. Run: atris login',
340
+ startedAt, finishedAt: now(),
341
+ };
342
+ }
343
+ log('not logged in — falling back to a local autopilot tick');
344
+ const local = localFn({ workspace, json: opts.json, timeoutSec });
345
+ return { ok: local.ok, source: 'local', reason: 'no_auth', local, startedAt, finishedAt: now() };
346
+ }
347
+
348
+ // Attempt the paid API tick.
349
+ const apiPath = improveApiPath(baseFn());
350
+ const body = buildImprovePayload({ ...opts, workspace });
351
+ const apiResult = await apiFn(apiPath, {
352
+ method: 'POST',
353
+ token: creds.token,
354
+ body,
355
+ timeoutMs: opts.timeoutMs || DEFAULT_TIMEOUT_MS,
356
+ });
357
+
358
+ if (apiResult && apiResult.ok) {
359
+ const summary = summarizeImproveResponse(apiResult.data);
360
+ const finishedAt = now();
361
+ // Only a real, shipping tick earns a receipt. Plan/delegate/dry-run ship
362
+ // nothing, and an error inside an ok envelope (e.g. "workspace not found")
363
+ // is not a shipped change — none of these should write a scorecard/journal.
364
+ const shipped = (opts.mode || 'full') === 'full' && !opts.dryRun && !summary.error;
365
+ if (!shipped) {
366
+ return { ok: true, source: 'api', summary, scorecardPath: null, journalPath: null, receipt: 'skipped', startedAt, finishedAt };
367
+ }
368
+ const row = buildScorecardRow(summary, { source: 'api', mode: opts.mode || 'full', ts: finishedAt, member: opts.member });
369
+ let scorecardPath = null;
370
+ try {
371
+ scorecardPath = writeRow(workspace, row);
372
+ } catch (e) {
373
+ log(`scorecard write failed: ${e.message}`);
374
+ }
375
+ let journalPath = null;
376
+ try {
377
+ journalPath = writeJournal(workspace, summary, { source: 'api', member: opts.member });
378
+ } catch (e) {
379
+ log(`journal write failed: ${e.message}`);
380
+ }
381
+ return { ok: true, source: 'api', summary, scorecardPath, journalPath, receipt: 'written', row, startedAt, finishedAt };
382
+ }
383
+
384
+ const decide = shouldFallbackLocal({ creds, apiResult });
385
+ if (decide.fallback && opts.fallback) {
386
+ log(`backend ${decide.reason} — falling back to a local autopilot tick`);
387
+ const local = localFn({ workspace, json: opts.json, timeoutSec });
388
+ return { ok: local.ok, source: 'local', reason: decide.reason, local, apiResult, startedAt, finishedAt: now() };
389
+ }
390
+
391
+ // Real, answerable failure (e.g. insufficient credits, server error). Report it.
392
+ return {
393
+ ok: false, source: 'api', reason: decide.reason,
394
+ error: (apiResult && (apiResult.error || `HTTP ${apiResult.status}`)) || 'request failed',
395
+ apiResult, startedAt, finishedAt: now(),
396
+ };
397
+ }
398
+
399
+ function formatImproveReport(result = {}) {
400
+ const lines = [];
401
+ if (result.source === 'api' && result.ok) {
402
+ const s = result.summary || {};
403
+ lines.push('improved.');
404
+ lines.push('');
405
+ lines.push(` task: ${s.shipped || '(no description returned)'}`);
406
+ lines.push(` verify: ${s.verify === true ? 'pass' : s.verify === false ? 'FAIL' : 'unknown'}`);
407
+ lines.push(` reward: ${s.reward != null ? s.reward : '?'}`);
408
+ lines.push(` credits: ${s.credits != null ? s.credits : 'billed server-side (not echoed)'}`);
409
+ if (s.files && s.files.length) lines.push(` files: ${s.files.join(', ')}`);
410
+ if (s.model) lines.push(` model: ${s.model}`);
411
+ if (s.elapsedMs != null) lines.push(` time: ${(s.elapsedMs / 1000).toFixed(0)}s`);
412
+ if (result.scorecardPath) {
413
+ lines.push('');
414
+ lines.push(` scorecard: ${path.relative(process.cwd(), result.scorecardPath)}`);
415
+ }
416
+ return lines.join('\n');
417
+ }
418
+ if (result.source === 'local') {
419
+ lines.push(result.ok ? 'improved (local fallback).' : 'local fallback tick failed.');
420
+ lines.push(` reason: backend ${result.reason} — ran a local autopilot tick instead`);
421
+ if (result.local && !result.ok && result.local.stderr) {
422
+ lines.push(` error: ${result.local.stderr.trim().split('\n').slice(-1)[0]}`);
423
+ }
424
+ return lines.join('\n');
425
+ }
426
+ lines.push('improve tick did not run.');
427
+ lines.push(` reason: ${result.reason || 'unknown'}`);
428
+ if (result.error) lines.push(` error: ${result.error}`);
429
+ return lines.join('\n');
430
+ }
431
+
432
+ function showHelp() {
433
+ console.log(`atris improve — run one paid RL improvement tick
434
+
435
+ Usage:
436
+ atris improve [mode] [options]
437
+
438
+ Modes (positional or --mode):
439
+ full plan + build + verify + score (default)
440
+ plan return the plan only, no changes
441
+ delegate queue the tick for a local Claude Code session
442
+ history show the tick history (reward trend, credits, pass rate)
443
+
444
+ Options:
445
+ --member <name> attribute the tick to a member (the loop's owner)
446
+ --model <id> override the model (e.g. claude-sonnet-4-6)
447
+ --dry-run plan and run but do not commit
448
+ --no-fallback do not fall back to a local tick if the backend is down
449
+ --workspace <p> workspace path (default: cwd)
450
+ --timeout <sec> request timeout in seconds (default: 300)
451
+ --json machine-readable output (for the member loop)
452
+ -h, --help this help
453
+
454
+ Calls POST /api/improve, which ships one verifiable change and deducts
455
+ Atris credits per successful tick. Writes a per-tick scorecard to
456
+ .atris/state/scorecards.jsonl. Falls back to a local autopilot tick when
457
+ the backend is unreachable or you are not logged in.`);
458
+ }
459
+
460
+ async function run(argv = []) {
461
+ const opts = parseImproveArgs(argv);
462
+ if (opts.help) { showHelp(); return 0; }
463
+
464
+ if (opts.history) {
465
+ const summary = summarizeTickHistory(readTickHistory(opts.workspace));
466
+ if (opts.json) console.log(JSON.stringify(summary));
467
+ else console.log(formatTickHistory(summary));
468
+ return 0;
469
+ }
470
+
471
+ const result = await runImprove(opts, {
472
+ log: opts.json ? () => {} : (m) => console.error(` ${m}`),
473
+ });
474
+
475
+ if (opts.json) {
476
+ console.log(JSON.stringify(result));
477
+ } else {
478
+ console.log(formatImproveReport(result));
479
+ }
480
+ return result.ok ? 0 : 1;
481
+ }
482
+
483
+ module.exports = {
484
+ run,
485
+ runImprove,
486
+ parseImproveArgs,
487
+ buildImprovePayload,
488
+ summarizeImproveResponse,
489
+ shouldFallbackLocal,
490
+ buildScorecardRow,
491
+ appendScorecardRow,
492
+ appendTickToJournal,
493
+ expandHome,
494
+ readTickHistory,
495
+ summarizeTickHistory,
496
+ formatTickHistory,
497
+ improveApiPath,
498
+ formatImproveReport,
499
+ runLocalFallback,
500
+ SCORECARD_SCHEMA,
501
+ };