atris 3.15.56 → 3.16.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/AGENTS.md +2 -2
- package/GETTING_STARTED.md +1 -1
- package/PERSONA.md +4 -4
- package/README.md +11 -11
- package/atris/skills/copy-editor/SKILL.md +30 -4
- package/atris/skills/improve/SKILL.md +18 -20
- package/atris/wiki/concepts/agent-activation-contract.md +5 -3
- package/atris/wiki/concepts/workspace-initialization-contract.md +4 -4
- package/atris/wiki/index.md +1 -0
- package/ax +522 -73
- package/bin/atris.js +32 -31
- package/commands/align.js +0 -14
- package/commands/apps.js +102 -1
- package/commands/autopilot.js +197 -22
- package/commands/brain.js +219 -34
- package/commands/brainstorm.js +0 -829
- package/commands/computer.js +45 -83
- package/commands/improve.js +501 -0
- package/commands/integrations.js +228 -0
- package/commands/lesson.js +44 -0
- package/commands/member.js +4498 -226
- package/commands/mission.js +302 -27
- package/commands/now.js +89 -1
- package/commands/radar.js +181 -56
- package/commands/skill.js +37 -6
- package/commands/soul.js +0 -4
- package/commands/task.js +5582 -517
- package/commands/terminal.js +14 -10
- package/commands/wiki.js +87 -1
- package/commands/workflow.js +288 -73
- package/commands/worktree.js +52 -15
- package/commands/xp.js +41 -65
- package/lib/auto-accept-certified.js +294 -0
- package/lib/file-ops.js +0 -184
- package/lib/member-alive.js +232 -0
- package/lib/policy-lessons.js +280 -0
- package/lib/receipt-evidence.js +64 -0
- package/lib/state-detection.js +34 -0
- package/lib/task-db.js +568 -16
- package/lib/task-proof.js +43 -0
- package/package.json +1 -1
- package/utils/auth.js +13 -4
- package/commands/research.js +0 -52
- package/lib/section-merge.js +0 -196
package/commands/computer.js
CHANGED
|
@@ -146,12 +146,6 @@ function formatWorkerName(worker) {
|
|
|
146
146
|
return active === 'openai' ? 'OpenAI' : 'Claude';
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
function formatBillingMode(worker) {
|
|
150
|
-
return activeWorker(worker) === 'openai'
|
|
151
|
-
? 'Atris credits'
|
|
152
|
-
: 'Claude subscription lane';
|
|
153
|
-
}
|
|
154
|
-
|
|
155
149
|
function extractAttachedWorkspaceMismatch(...values) {
|
|
156
150
|
const text = values
|
|
157
151
|
.filter((value) => value !== null && value !== undefined)
|
|
@@ -412,12 +406,6 @@ async function printCloudSessionStatus(token, ctx, worker, model) {
|
|
|
412
406
|
if (d.endpoint) console.log(` Endpoint: ${d.endpoint}`);
|
|
413
407
|
}
|
|
414
408
|
|
|
415
|
-
function formatDropdownLine(choice, selected) {
|
|
416
|
-
const pointer = selected ? '>' : ' ';
|
|
417
|
-
const label = selected ? ui.bold(choice.label) : choice.label;
|
|
418
|
-
return `${pointer} ${label} ${ui.dim(choice.detail || '')}`.trimEnd();
|
|
419
|
-
}
|
|
420
|
-
|
|
421
409
|
function questionAsync(rl, question) {
|
|
422
410
|
return new Promise((resolve) => rl.question(question, resolve));
|
|
423
411
|
}
|
|
@@ -445,38 +433,6 @@ async function selectFromDropdown(title, choices) {
|
|
|
445
433
|
return choices[0];
|
|
446
434
|
}
|
|
447
435
|
|
|
448
|
-
function describeLocalClaudeAuth() {
|
|
449
|
-
if (process.env.ANTHROPIC_API_KEY) {
|
|
450
|
-
return 'Local auth: ANTHROPIC_API_KEY set on this Mac';
|
|
451
|
-
}
|
|
452
|
-
const hasClaude = spawnSync('which', ['claude'], { encoding: 'utf8', timeout: 1000 }).status === 0;
|
|
453
|
-
if (!hasClaude) {
|
|
454
|
-
return 'Local auth: Claude CLI not found; use Cloud workspace or install Claude Code';
|
|
455
|
-
}
|
|
456
|
-
const status = spawnSync('claude', ['auth', 'status', '--json'], {
|
|
457
|
-
encoding: 'utf8',
|
|
458
|
-
timeout: 1500,
|
|
459
|
-
stdio: 'pipe',
|
|
460
|
-
});
|
|
461
|
-
if (status.error && status.error.code === 'ETIMEDOUT') {
|
|
462
|
-
return 'Local auth: Claude CLI installed, auth check timed out; Cloud subscription does not carry over';
|
|
463
|
-
}
|
|
464
|
-
const raw = String(status.stdout || status.stderr || '').trim();
|
|
465
|
-
try {
|
|
466
|
-
const parsed = JSON.parse(raw);
|
|
467
|
-
if (parsed.loggedIn || parsed.status === 'logged_in' || parsed.authMethod) {
|
|
468
|
-
const plan = parsed.subscriptionType || parsed.plan || parsed.authMethod || 'connected';
|
|
469
|
-
return `Local auth: Claude logged in on this Mac (${plan})`;
|
|
470
|
-
}
|
|
471
|
-
} catch {
|
|
472
|
-
// Fall through to text checks.
|
|
473
|
-
}
|
|
474
|
-
if (/logged\s*in|subscription|max|pro/i.test(raw)) {
|
|
475
|
-
return 'Local auth: Claude appears logged in on this Mac';
|
|
476
|
-
}
|
|
477
|
-
return 'Local auth: not confirmed; run `claude login` on this Mac or choose Cloud workspace';
|
|
478
|
-
}
|
|
479
|
-
|
|
480
436
|
async function chooseComputerSurface(hasBusinessBinding, hasLocalHarness) {
|
|
481
437
|
if (!useInteractiveTerminalUi()) {
|
|
482
438
|
return hasBusinessBinding ? 'cloud' : 'local';
|
|
@@ -723,22 +679,6 @@ function findAtrisCodeTerminal() {
|
|
|
723
679
|
return null;
|
|
724
680
|
}
|
|
725
681
|
|
|
726
|
-
function findAtrisCodePython(terminalPath) {
|
|
727
|
-
const envPython = process.env.ATRIS_CODE_PYTHON;
|
|
728
|
-
if (envPython && fs.existsSync(envPython)) return envPython;
|
|
729
|
-
if (!terminalPath) return 'python3';
|
|
730
|
-
|
|
731
|
-
const projectRoot = path.dirname(path.dirname(terminalPath));
|
|
732
|
-
const candidates = [
|
|
733
|
-
path.join(projectRoot, 'venv', 'bin', 'python3'),
|
|
734
|
-
path.join(projectRoot, '.venv', 'bin', 'python3'),
|
|
735
|
-
];
|
|
736
|
-
for (const p of candidates) {
|
|
737
|
-
if (fs.existsSync(p)) return p;
|
|
738
|
-
}
|
|
739
|
-
return 'python3';
|
|
740
|
-
}
|
|
741
|
-
|
|
742
682
|
function computerLocalLegacy(extraArgs = []) {
|
|
743
683
|
printModeBanner('LOCAL', process.cwd(), [
|
|
744
684
|
'Current folder is the workspace.',
|
|
@@ -2550,34 +2490,56 @@ async function streamBusinessChatResult(token, ctx, executionId, rl = null, opti
|
|
|
2550
2490
|
|
|
2551
2491
|
errors = 0;
|
|
2552
2492
|
let done = false;
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
sawVisibleOutput
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2493
|
+
const emitEvents = (items, { showTools = true } = {}) => {
|
|
2494
|
+
let batchDone = false;
|
|
2495
|
+
for (const event of (items || [])) {
|
|
2496
|
+
if ((event.type === 'assistant_text' || event.type === 'text') && event.content) {
|
|
2497
|
+
sawVisibleOutput = true;
|
|
2498
|
+
process.stdout.write(event.content);
|
|
2499
|
+
} else if (event.type === 'result' && event.result && !sawVisibleOutput) {
|
|
2500
|
+
sawVisibleOutput = true;
|
|
2501
|
+
process.stdout.write(String(event.result));
|
|
2502
|
+
} else if (showTools && !options.quiet && event.type === 'tool_use' && event.tool) {
|
|
2503
|
+
const arg = event.input?.file_path || event.input?.path || event.input?.pattern || event.input?.command || '';
|
|
2504
|
+
if (arg) {
|
|
2505
|
+
console.log(`\n [${event.tool}] ${String(arg).slice(0, 120)}`);
|
|
2506
|
+
} else {
|
|
2507
|
+
console.log(`\n [${event.tool}]`);
|
|
2508
|
+
}
|
|
2509
|
+
} else if (event.type === 'error') {
|
|
2510
|
+
if (event.error) console.error(`\n${event.error}`);
|
|
2511
|
+
terminalStatus = 'error';
|
|
2512
|
+
batchDone = true;
|
|
2513
|
+
break;
|
|
2514
|
+
} else if (event.type === 'complete') {
|
|
2515
|
+
terminalStatus = 'completed';
|
|
2516
|
+
batchDone = true;
|
|
2517
|
+
break;
|
|
2567
2518
|
}
|
|
2568
|
-
} else if (event.type === 'error') {
|
|
2569
|
-
if (event.error) console.error(`\n${event.error}`);
|
|
2570
|
-
terminalStatus = 'error';
|
|
2571
|
-
done = true;
|
|
2572
|
-
break;
|
|
2573
|
-
} else if (event.type === 'complete') {
|
|
2574
|
-
terminalStatus = 'completed';
|
|
2575
|
-
done = true;
|
|
2576
|
-
break;
|
|
2577
2519
|
}
|
|
2520
|
+
return batchDone;
|
|
2521
|
+
};
|
|
2522
|
+
|
|
2523
|
+
const batch = events.data?.events || [];
|
|
2524
|
+
done = emitEvents(batch);
|
|
2525
|
+
const nextIndex = events.data?.next_index;
|
|
2526
|
+
if (Number.isInteger(nextIndex) && nextIndex >= fromIndex) {
|
|
2527
|
+
fromIndex = nextIndex;
|
|
2528
|
+
} else {
|
|
2529
|
+
fromIndex += batch.length;
|
|
2578
2530
|
}
|
|
2579
2531
|
|
|
2580
2532
|
if (done || ['completed', 'error', 'failed', 'cancelled'].includes(events.data?.status)) {
|
|
2533
|
+
if (!sawVisibleOutput && events.data?.status === 'completed') {
|
|
2534
|
+
const fullEvents = await apiRequestJson(
|
|
2535
|
+
`/business/${ctx.businessId}/chat/events?execution_id=${executionId}&workspace_id=${ctx.workspaceId}&from_index=0`,
|
|
2536
|
+
{ method: 'GET', token, timeoutMs: 60000 }
|
|
2537
|
+
);
|
|
2538
|
+
if (fullEvents.ok) {
|
|
2539
|
+
emitEvents(fullEvents.data?.events || [], { showTools: false });
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2581
2543
|
if (!process.stdout.write('\n')) {
|
|
2582
2544
|
// no-op: keep line handling stable
|
|
2583
2545
|
}
|
|
@@ -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
|
+
};
|