atris 3.16.0 → 3.17.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 +33 -7
- package/atris/skills/atris/SKILL.md +15 -2
- package/atris/skills/atris-feedback/SKILL.md +7 -0
- package/atris/skills/design/SKILL.md +29 -2
- package/atris/skills/engines/SKILL.md +44 -0
- package/atris/skills/flow/SKILL.md +1 -1
- package/atris/skills/wake/SKILL.md +37 -0
- package/atris/skills/youtube/SKILL.md +13 -39
- package/atris/team/validator/MEMBER.md +1 -0
- package/atris/wiki/concepts/agent-activation-contract.md +3 -3
- package/atris/wiki/concepts/workspace-initialization-contract.md +3 -3
- package/atris/wiki/index.md +1 -0
- package/atris.md +43 -19
- package/bin/atris.js +446 -43
- package/commands/agent-spawn.js +480 -0
- package/commands/analytics.js +6 -3
- package/commands/apps.js +11 -0
- package/commands/autopilot.js +466 -20
- package/commands/brain.js +74 -7
- package/commands/brainstorm.js +9 -58
- package/commands/clean.js +1 -4
- package/commands/compile.js +574 -0
- package/commands/console.js +8 -3
- package/commands/deck.js +135 -0
- package/commands/init.js +22 -11
- package/commands/lesson.js +76 -0
- package/commands/member.js +252 -48
- package/commands/mission.js +405 -13
- package/commands/now.js +4 -2
- package/commands/probe.js +444 -0
- package/commands/pulse.js +504 -0
- package/commands/radar.js +1 -0
- package/commands/recap.js +233 -0
- package/commands/run.js +615 -22
- package/commands/skill.js +6 -2
- package/commands/slop.js +173 -0
- package/commands/spaceship.js +39 -0
- package/commands/sync.js +0 -2
- package/commands/task.js +458 -43
- package/commands/verify.js +7 -3
- package/lib/activity-stream.js +166 -0
- package/lib/auto-accept-certified.js +23 -1
- package/lib/context-gatherer.js +170 -0
- package/lib/escape-regexp.js +13 -0
- package/lib/file-ops.js +6 -3
- package/lib/journal.js +1 -1
- package/lib/lesson-contradiction.js +113 -0
- package/lib/policy-lessons.js +3 -2
- package/lib/pulse.js +401 -0
- package/lib/runner-command.js +156 -0
- package/lib/slides-deck.js +236 -0
- package/lib/state-detection.js +40 -3
- package/lib/task-db.js +101 -4
- package/lib/task-proof.js +1 -1
- package/lib/todo-fallback.js +2 -1
- package/lib/todo-sections.js +33 -0
- package/package.json +1 -2
- package/utils/api.js +14 -2
- package/atris/atrisDev.md +0 -717
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atris Compile — learn like AI, run like code.
|
|
3
|
+
*
|
|
4
|
+
* The compile loop: execution records → compile-to-deterministic-code →
|
|
5
|
+
* statistical backtest → gated promote. The LLM is only in the build path;
|
|
6
|
+
* the promoted artifact runs token-free.
|
|
7
|
+
*
|
|
8
|
+
* atris compile record <name> --input <json|@file> --output <json|@file>
|
|
9
|
+
* atris compile build <name> (uses the shared runner command)
|
|
10
|
+
* atris compile backtest <name>
|
|
11
|
+
* atris compile promote <name>
|
|
12
|
+
* atris compile exec <name> --input <json|@file> [--record]
|
|
13
|
+
* atris compile status
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { execSync } = require('child_process');
|
|
21
|
+
const {
|
|
22
|
+
buildRunnerAvailabilityCommand,
|
|
23
|
+
buildRunnerCommand,
|
|
24
|
+
resolveClaudeRunnerBin,
|
|
25
|
+
} = require('../lib/runner-command');
|
|
26
|
+
|
|
27
|
+
const DEFAULT_THRESHOLD = 0.99;
|
|
28
|
+
const SAMPLE_RECORDS_FOR_BUILD = 25;
|
|
29
|
+
const LOW_RECORD_WARNING = 20;
|
|
30
|
+
|
|
31
|
+
// ── paths ──────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
function processDir(root, name) {
|
|
34
|
+
return path.join(root, 'atris', 'processes', name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function recordsPath(root, name) {
|
|
38
|
+
return path.join(root, '.atris', 'state', 'processes', name, 'records.jsonl');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function manifestPath(root, name) {
|
|
42
|
+
return path.join(processDir(root, name), 'process.json');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function runnerPath(root, name) {
|
|
46
|
+
return path.join(processDir(root, name), 'run.js');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function specPath(root, name) {
|
|
50
|
+
return path.join(processDir(root, name), 'spec.md');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function validName(name) {
|
|
54
|
+
return typeof name === 'string' && /^[a-z0-9][a-z0-9-_]*$/.test(name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── manifest ───────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function readManifest(root, name) {
|
|
60
|
+
const p = manifestPath(root, name);
|
|
61
|
+
if (!fs.existsSync(p)) return null;
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writeManifest(root, name, manifest) {
|
|
70
|
+
fs.mkdirSync(processDir(root, name), { recursive: true });
|
|
71
|
+
const target = manifestPath(root, name);
|
|
72
|
+
const tmp = `${target}.tmp`;
|
|
73
|
+
fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2) + '\n');
|
|
74
|
+
fs.renameSync(tmp, target);
|
|
75
|
+
return manifest;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function defaultManifest(name) {
|
|
79
|
+
return {
|
|
80
|
+
name,
|
|
81
|
+
version: 0,
|
|
82
|
+
status: 'draft', // draft | active | drifted
|
|
83
|
+
threshold: DEFAULT_THRESHOLD,
|
|
84
|
+
compiledAt: null,
|
|
85
|
+
backtest: null, // { version, total, passed, accuracy, at, failures }
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── records ────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function parseValueArg(raw) {
|
|
92
|
+
if (raw === undefined || raw === null) return undefined;
|
|
93
|
+
let text = String(raw);
|
|
94
|
+
if (text.startsWith('@')) {
|
|
95
|
+
text = fs.readFileSync(text.slice(1), 'utf8');
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(text);
|
|
99
|
+
} catch {
|
|
100
|
+
return text; // plain string payloads are fine
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function appendRecord(root, name, record) {
|
|
105
|
+
const p = recordsPath(root, name);
|
|
106
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
107
|
+
const entry = { ts: new Date().toISOString(), ...record };
|
|
108
|
+
fs.appendFileSync(p, JSON.stringify(entry) + '\n');
|
|
109
|
+
return entry;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readRecords(root, name) {
|
|
113
|
+
const p = recordsPath(root, name);
|
|
114
|
+
if (!fs.existsSync(p)) return [];
|
|
115
|
+
return fs.readFileSync(p, 'utf8')
|
|
116
|
+
.split('\n')
|
|
117
|
+
.filter((line) => line.trim())
|
|
118
|
+
.map((line) => {
|
|
119
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
120
|
+
})
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function listProcesses(root) {
|
|
125
|
+
const names = new Set();
|
|
126
|
+
const docsDir = path.join(root, 'atris', 'processes');
|
|
127
|
+
const stateDir = path.join(root, '.atris', 'state', 'processes');
|
|
128
|
+
for (const dir of [docsDir, stateDir]) {
|
|
129
|
+
if (!fs.existsSync(dir)) continue;
|
|
130
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
131
|
+
if (entry.isDirectory()) names.add(entry.name);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return [...names].sort();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── compare ────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
function deepEqual(a, b) {
|
|
140
|
+
if (a === b) return true;
|
|
141
|
+
if (typeof a !== typeof b) return false;
|
|
142
|
+
if (a === null || b === null) return false;
|
|
143
|
+
if (typeof a !== 'object') {
|
|
144
|
+
return Number.isNaN(a) && Number.isNaN(b);
|
|
145
|
+
}
|
|
146
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
147
|
+
const keysA = Object.keys(a);
|
|
148
|
+
const keysB = Object.keys(b);
|
|
149
|
+
if (keysA.length !== keysB.length) return false;
|
|
150
|
+
return keysA.every((k) => deepEqual(a[k], b[k]));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── runner loading / execution ─────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
function loadRunner(root, name) {
|
|
156
|
+
const p = runnerPath(root, name);
|
|
157
|
+
if (!fs.existsSync(p)) {
|
|
158
|
+
throw new Error(`no compiled artifact at ${path.relative(root, p)} — run "atris compile build ${name}" first`);
|
|
159
|
+
}
|
|
160
|
+
const resolved = require.resolve(p);
|
|
161
|
+
delete require.cache[resolved]; // always load the latest compiled version
|
|
162
|
+
const mod = require(resolved);
|
|
163
|
+
if (!mod || typeof mod.run !== 'function') {
|
|
164
|
+
throw new Error(`${path.relative(root, p)} must export { run(input) }`);
|
|
165
|
+
}
|
|
166
|
+
return mod;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── backtest ───────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
async function runBacktest(root, name, options = {}) {
|
|
172
|
+
const records = readRecords(root, name);
|
|
173
|
+
if (records.length === 0) {
|
|
174
|
+
throw new Error(`no execution records for "${name}" — add some with "atris compile record ${name} --input ... --output ..."`);
|
|
175
|
+
}
|
|
176
|
+
const runner = loadRunner(root, name);
|
|
177
|
+
const manifest = readManifest(root, name) || defaultManifest(name);
|
|
178
|
+
const threshold = options.threshold !== undefined ? options.threshold : manifest.threshold;
|
|
179
|
+
|
|
180
|
+
let passed = 0;
|
|
181
|
+
const failures = [];
|
|
182
|
+
for (let i = 0; i < records.length; i++) {
|
|
183
|
+
const record = records[i];
|
|
184
|
+
const expected = record.expected !== undefined ? record.expected : record.output;
|
|
185
|
+
try {
|
|
186
|
+
const actual = await runner.run(record.input);
|
|
187
|
+
if (deepEqual(actual, expected)) {
|
|
188
|
+
passed++;
|
|
189
|
+
} else {
|
|
190
|
+
failures.push({ index: i, input: record.input, expected, actual });
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
failures.push({ index: i, input: record.input, expected, error: err.message });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const total = records.length;
|
|
198
|
+
const accuracy = passed / total;
|
|
199
|
+
const result = {
|
|
200
|
+
version: manifest.version,
|
|
201
|
+
total,
|
|
202
|
+
passed,
|
|
203
|
+
accuracy,
|
|
204
|
+
threshold,
|
|
205
|
+
at: new Date().toISOString(),
|
|
206
|
+
failures: failures.slice(0, 10),
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
manifest.backtest = result;
|
|
210
|
+
// a one-off --threshold applies to this run only; the promote gate stays as set
|
|
211
|
+
// drift detection: an active process falling below its standing gate is drifted
|
|
212
|
+
if (manifest.status === 'active' && accuracy < manifest.threshold) {
|
|
213
|
+
manifest.status = 'drifted';
|
|
214
|
+
}
|
|
215
|
+
writeManifest(root, name, manifest);
|
|
216
|
+
|
|
217
|
+
return { result, manifest, failureCount: failures.length };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── promote gate ───────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
function promoteProcess(root, name, options = {}) {
|
|
223
|
+
const manifest = readManifest(root, name);
|
|
224
|
+
if (!manifest) {
|
|
225
|
+
throw new Error(`no manifest for "${name}" — compile and backtest it first`);
|
|
226
|
+
}
|
|
227
|
+
if (options.threshold !== undefined) {
|
|
228
|
+
// the only way to change the standing gate: deliberate, at promote time
|
|
229
|
+
manifest.threshold = options.threshold;
|
|
230
|
+
}
|
|
231
|
+
const bt = manifest.backtest;
|
|
232
|
+
if (!bt) {
|
|
233
|
+
throw new Error(`"${name}" has no backtest — run "atris compile backtest ${name}" first`);
|
|
234
|
+
}
|
|
235
|
+
if (bt.version !== manifest.version) {
|
|
236
|
+
throw new Error(`backtest is for version ${bt.version} but current version is ${manifest.version} — re-run the backtest`);
|
|
237
|
+
}
|
|
238
|
+
if (bt.accuracy < manifest.threshold) {
|
|
239
|
+
throw new Error(`backtest accuracy ${(bt.accuracy * 100).toFixed(2)}% is below the ${(manifest.threshold * 100).toFixed(2)}% gate (${bt.passed}/${bt.total}) — not promoting`);
|
|
240
|
+
}
|
|
241
|
+
manifest.status = 'active';
|
|
242
|
+
manifest.promotedAt = new Date().toISOString();
|
|
243
|
+
writeManifest(root, name, manifest);
|
|
244
|
+
return manifest;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── build (the only step that spends tokens) ───────────────────────
|
|
248
|
+
|
|
249
|
+
function buildCompilePrompt(root, name, records, notes) {
|
|
250
|
+
const runRel = path.relative(root, runnerPath(root, name));
|
|
251
|
+
const specRel = path.relative(root, specPath(root, name));
|
|
252
|
+
const hasSpec = fs.existsSync(specPath(root, name));
|
|
253
|
+
const sample = records.slice(-SAMPLE_RECORDS_FOR_BUILD);
|
|
254
|
+
|
|
255
|
+
return `You are the Atris process compiler. Compile the recurring process "${name}" into deterministic code.
|
|
256
|
+
|
|
257
|
+
${hasSpec ? `Read the process spec first: ${specRel}` : `There is no spec file yet. Infer the process from the execution records below, then write what you inferred to ${specRel} (short markdown: purpose, input shape, output shape, rules).`}
|
|
258
|
+
${notes ? `\nOperator notes for this build:\n${notes}\n` : ''}
|
|
259
|
+
Execution records (most recent ${sample.length} of ${records.length}; input is what the process received, expected/output is the correct result):
|
|
260
|
+
${JSON.stringify(sample, null, 2)}
|
|
261
|
+
|
|
262
|
+
Write the compiled artifact to ${runRel} with this exact contract:
|
|
263
|
+
- CommonJS module exporting { run }: \`module.exports = { run };\`
|
|
264
|
+
- \`run(input)\` takes one input value and returns the output (sync return or promise both fine)
|
|
265
|
+
- deterministic: no network calls, no Date.now()/new Date() in outputs, no randomness, no environment reads
|
|
266
|
+
- Node.js built-ins only — zero npm dependencies
|
|
267
|
+
- handle malformed input by throwing a clear Error, never by guessing
|
|
268
|
+
|
|
269
|
+
The artifact will be verified by replaying every execution record through run() and requiring near-perfect accuracy, so generalize the rules — do not hardcode a lookup table of the sample records.
|
|
270
|
+
|
|
271
|
+
Reply [COMPILE_COMPLETE] when ${runRel} is written.`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function executeBuild(root, name, options = {}) {
|
|
275
|
+
const { verbose = false, timeout = 600000, notes = '', cmdOverride } = options;
|
|
276
|
+
const records = readRecords(root, name);
|
|
277
|
+
if (records.length === 0) {
|
|
278
|
+
throw new Error(`no execution records for "${name}" — record real runs first, then compile`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!cmdOverride) {
|
|
282
|
+
try {
|
|
283
|
+
execSync(buildRunnerAvailabilityCommand(), { stdio: 'pipe' });
|
|
284
|
+
} catch {
|
|
285
|
+
throw new Error(`${resolveClaudeRunnerBin()} CLI not found. Set ATRIS_RUNNER_BIN (or legacy ATRIS_CLAUDE_BIN), or install the configured runner first.`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
fs.mkdirSync(processDir(root, name), { recursive: true });
|
|
290
|
+
const prompt = buildCompilePrompt(root, name, records, notes);
|
|
291
|
+
const tmpFile = path.join(root, '.compile-prompt.tmp');
|
|
292
|
+
fs.writeFileSync(tmpFile, prompt);
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const cmd = cmdOverride || buildRunnerCommand({ promptFile: tmpFile, allowedTools: 'Read,Write,Edit,Glob,Grep' });
|
|
296
|
+
const env = { ...process.env };
|
|
297
|
+
delete env.CLAUDECODE;
|
|
298
|
+
execSync(cmd, {
|
|
299
|
+
cwd: root,
|
|
300
|
+
encoding: 'utf8',
|
|
301
|
+
timeout,
|
|
302
|
+
stdio: verbose ? 'inherit' : 'pipe',
|
|
303
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
304
|
+
env,
|
|
305
|
+
});
|
|
306
|
+
} finally {
|
|
307
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!fs.existsSync(runnerPath(root, name))) {
|
|
311
|
+
throw new Error(`compile finished but ${path.relative(root, runnerPath(root, name))} was not written`);
|
|
312
|
+
}
|
|
313
|
+
loadRunner(root, name); // validates the contract
|
|
314
|
+
|
|
315
|
+
const manifest = readManifest(root, name) || defaultManifest(name);
|
|
316
|
+
manifest.version += 1;
|
|
317
|
+
manifest.compiledAt = new Date().toISOString();
|
|
318
|
+
manifest.status = 'draft'; // every rebuild must re-earn promotion through the gate
|
|
319
|
+
manifest.backtest = null; // new version must re-earn its backtest
|
|
320
|
+
writeManifest(root, name, manifest);
|
|
321
|
+
return manifest;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── arg parsing ────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
function parseFlags(args) {
|
|
327
|
+
const flags = {};
|
|
328
|
+
const positional = [];
|
|
329
|
+
for (let i = 0; i < args.length; i++) {
|
|
330
|
+
const arg = args[i];
|
|
331
|
+
if (!arg.startsWith('--')) {
|
|
332
|
+
positional.push(arg);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const eq = arg.indexOf('=');
|
|
336
|
+
if (eq !== -1) {
|
|
337
|
+
flags[arg.slice(2, eq)] = arg.slice(eq + 1);
|
|
338
|
+
} else if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
339
|
+
flags[arg.slice(2)] = args[++i];
|
|
340
|
+
} else {
|
|
341
|
+
flags[arg.slice(2)] = true;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return { flags, positional };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── formatting ─────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
function formatAccuracy(accuracy) {
|
|
350
|
+
return `${(accuracy * 100).toFixed(2)}%`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function printBacktest(name, result, failureCount, manifest) {
|
|
354
|
+
console.log(`backtest ${name} v${result.version}: ${result.passed}/${result.total} passed (${formatAccuracy(result.accuracy)}), gate ${formatAccuracy(result.threshold)}`);
|
|
355
|
+
if (result.total < LOW_RECORD_WARNING) {
|
|
356
|
+
console.log(`note: only ${result.total} record${result.total === 1 ? '' : 's'} — accuracy means more with ${LOW_RECORD_WARNING}+`);
|
|
357
|
+
}
|
|
358
|
+
if (failureCount > 0) {
|
|
359
|
+
console.log(`${failureCount} mismatch${failureCount === 1 ? '' : 'es'}. first ${Math.min(failureCount, 3)}:`);
|
|
360
|
+
for (const f of result.failures.slice(0, 3)) {
|
|
361
|
+
if (f.error) {
|
|
362
|
+
console.log(` record ${f.index}: threw "${f.error}"`);
|
|
363
|
+
} else {
|
|
364
|
+
console.log(` record ${f.index}: expected ${JSON.stringify(f.expected)} got ${JSON.stringify(f.actual)}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (manifest.status === 'drifted') {
|
|
369
|
+
console.log(`drift detected: active process fell below its gate. recompile with "atris compile build ${name}"`);
|
|
370
|
+
} else if (result.accuracy >= result.threshold && manifest.status !== 'active') {
|
|
371
|
+
console.log(`gate passed. promote with "atris compile promote ${name}"`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ── command dispatcher ─────────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
function showCompileHelp() {
|
|
378
|
+
console.log('');
|
|
379
|
+
console.log('Usage: atris compile <subcommand> [args]');
|
|
380
|
+
console.log('');
|
|
381
|
+
console.log('Compile a recurring process into deterministic code, verified by');
|
|
382
|
+
console.log('backtesting against real execution records. LLM only at build time;');
|
|
383
|
+
console.log('the promoted artifact runs token-free.');
|
|
384
|
+
console.log('');
|
|
385
|
+
console.log(' record <name> --input <json|@file> --output <json|@file> [--expected ...]');
|
|
386
|
+
console.log(' append an execution record');
|
|
387
|
+
console.log(' build <name> [--notes "..."] [--verbose]');
|
|
388
|
+
console.log(' compile records + spec into atris/processes/<name>/run.js');
|
|
389
|
+
console.log(' backtest <name> [--threshold 0.99] [--json]');
|
|
390
|
+
console.log(' replay every record through run.js, score accuracy');
|
|
391
|
+
console.log(' (--threshold judges this run only; the gate is unchanged)');
|
|
392
|
+
console.log(' promote <name> [--gate 0.99]');
|
|
393
|
+
console.log(' mark active (gate: backtest accuracy >= threshold);');
|
|
394
|
+
console.log(' --gate is the only way to change the standing threshold');
|
|
395
|
+
console.log(' exec <name> --input <json|@file> [--record]');
|
|
396
|
+
console.log(' run the compiled process on new input');
|
|
397
|
+
console.log(' status [<name>] [--json] list processes, versions, accuracy, drift');
|
|
398
|
+
console.log('');
|
|
399
|
+
console.log('Loop: record real runs -> build -> backtest -> promote -> exec --record');
|
|
400
|
+
console.log('When backtest accuracy drops below the gate, the process is marked drifted');
|
|
401
|
+
console.log('and a recompile is suggested — self-healing against process drift.');
|
|
402
|
+
console.log('');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function compileCommand(subcommand, ...rawArgs) {
|
|
406
|
+
const root = process.cwd();
|
|
407
|
+
const { flags, positional } = parseFlags(rawArgs);
|
|
408
|
+
|
|
409
|
+
if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
410
|
+
showCompileHelp();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// `atris compile <name>` with an existing process name = build shortcut
|
|
415
|
+
let cmd = subcommand;
|
|
416
|
+
let name = positional[0];
|
|
417
|
+
const subcommands = ['record', 'build', 'backtest', 'promote', 'exec', 'status'];
|
|
418
|
+
if (!subcommands.includes(subcommand)) {
|
|
419
|
+
if (validName(subcommand) && listProcesses(root).includes(subcommand)) {
|
|
420
|
+
cmd = 'build';
|
|
421
|
+
name = subcommand;
|
|
422
|
+
} else {
|
|
423
|
+
console.error(`unknown subcommand "${subcommand}". try: atris compile help`);
|
|
424
|
+
process.exitCode = 1;
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (cmd === 'status') {
|
|
430
|
+
const names = name ? [name] : listProcesses(root);
|
|
431
|
+
const rows = names.map((n) => {
|
|
432
|
+
const manifest = readManifest(root, n) || defaultManifest(n);
|
|
433
|
+
const records = readRecords(root, n).length;
|
|
434
|
+
return {
|
|
435
|
+
name: n,
|
|
436
|
+
version: manifest.version,
|
|
437
|
+
status: manifest.status,
|
|
438
|
+
records,
|
|
439
|
+
accuracy: manifest.backtest ? manifest.backtest.accuracy : null,
|
|
440
|
+
compiledAt: manifest.compiledAt,
|
|
441
|
+
};
|
|
442
|
+
});
|
|
443
|
+
if (flags.json) {
|
|
444
|
+
console.log(JSON.stringify({ ok: true, processes: rows }, null, 2));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (rows.length === 0) {
|
|
448
|
+
console.log('no compiled processes yet. start with: atris compile record <name> --input ... --output ...');
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
for (const row of rows) {
|
|
452
|
+
const acc = row.accuracy === null ? 'no backtest' : formatAccuracy(row.accuracy);
|
|
453
|
+
console.log(`${row.name} v${row.version} ${row.status} ${row.records} record${row.records === 1 ? '' : 's'} ${acc}`);
|
|
454
|
+
}
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (!validName(name)) {
|
|
459
|
+
console.error('process name required (lowercase letters, digits, dashes). example: atris compile record invoice-triage --input ... --output ...');
|
|
460
|
+
process.exitCode = 1;
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (cmd === 'record') {
|
|
465
|
+
const input = parseValueArg(flags.input);
|
|
466
|
+
const output = parseValueArg(flags.output);
|
|
467
|
+
if (input === undefined || output === undefined) {
|
|
468
|
+
console.error('record needs --input and --output (inline json, a plain string, or @file)');
|
|
469
|
+
process.exitCode = 1;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const record = { input, output };
|
|
473
|
+
const expected = parseValueArg(flags.expected);
|
|
474
|
+
if (expected !== undefined) record.expected = expected;
|
|
475
|
+
if (flags.source) record.source = flags.source;
|
|
476
|
+
appendRecord(root, name, record);
|
|
477
|
+
const total = readRecords(root, name).length;
|
|
478
|
+
console.log(`recorded ${name} run ${total} -> ${path.relative(root, recordsPath(root, name))}`);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (cmd === 'build') {
|
|
483
|
+
const records = readRecords(root, name);
|
|
484
|
+
console.log(`compiling ${name} from ${records.length} execution record${records.length === 1 ? '' : 's'}…`);
|
|
485
|
+
const manifest = executeBuild(root, name, {
|
|
486
|
+
verbose: Boolean(flags.verbose),
|
|
487
|
+
notes: typeof flags.notes === 'string' ? flags.notes : '',
|
|
488
|
+
timeout: flags.timeout ? Number(flags.timeout) * 1000 : undefined,
|
|
489
|
+
});
|
|
490
|
+
console.log(`compiled ${name} v${manifest.version} -> ${path.relative(root, runnerPath(root, name))}`);
|
|
491
|
+
console.log(`next: atris compile backtest ${name}`);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (cmd === 'backtest') {
|
|
496
|
+
const options = {};
|
|
497
|
+
if (flags.threshold !== undefined) {
|
|
498
|
+
const t = Number(flags.threshold);
|
|
499
|
+
if (!(t > 0 && t <= 1)) {
|
|
500
|
+
console.error('--threshold must be between 0 and 1 (e.g. 0.99)');
|
|
501
|
+
process.exitCode = 1;
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
options.threshold = t;
|
|
505
|
+
}
|
|
506
|
+
const { result, manifest, failureCount } = await runBacktest(root, name, options);
|
|
507
|
+
if (flags.json) {
|
|
508
|
+
console.log(JSON.stringify({ ok: true, name, ...result, status: manifest.status }, null, 2));
|
|
509
|
+
} else {
|
|
510
|
+
printBacktest(name, result, failureCount, manifest);
|
|
511
|
+
}
|
|
512
|
+
if (result.accuracy < result.threshold) process.exitCode = 1;
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (cmd === 'promote') {
|
|
517
|
+
const options = {};
|
|
518
|
+
if (flags.gate !== undefined) {
|
|
519
|
+
const t = Number(flags.gate);
|
|
520
|
+
if (!(t > 0 && t <= 1)) {
|
|
521
|
+
console.error('--gate must be between 0 and 1 (e.g. 0.99)');
|
|
522
|
+
process.exitCode = 1;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
options.threshold = t;
|
|
526
|
+
}
|
|
527
|
+
const manifest = promoteProcess(root, name, options);
|
|
528
|
+
console.log(`${name} v${manifest.version} promoted to active (${formatAccuracy(manifest.backtest.accuracy)} over ${manifest.backtest.total} records, gate ${formatAccuracy(manifest.threshold)})`);
|
|
529
|
+
console.log(`run it token-free: atris compile exec ${name} --input '<json>' --record`);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (cmd === 'exec') {
|
|
534
|
+
const input = parseValueArg(flags.input);
|
|
535
|
+
if (input === undefined) {
|
|
536
|
+
console.error('exec needs --input (inline json, a plain string, or @file)');
|
|
537
|
+
process.exitCode = 1;
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const manifest = readManifest(root, name);
|
|
541
|
+
if (manifest && manifest.status === 'drifted') {
|
|
542
|
+
console.error(`warning: ${name} is marked drifted — outputs may be stale. recompile with "atris compile build ${name}"`);
|
|
543
|
+
}
|
|
544
|
+
const runner = loadRunner(root, name);
|
|
545
|
+
const output = await runner.run(input);
|
|
546
|
+
console.log(JSON.stringify(output, null, 2));
|
|
547
|
+
if (flags.record) {
|
|
548
|
+
appendRecord(root, name, { input, output, source: 'exec' });
|
|
549
|
+
}
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
module.exports = {
|
|
555
|
+
compileCommand,
|
|
556
|
+
// exported for tests
|
|
557
|
+
appendRecord,
|
|
558
|
+
readRecords,
|
|
559
|
+
readManifest,
|
|
560
|
+
writeManifest,
|
|
561
|
+
defaultManifest,
|
|
562
|
+
deepEqual,
|
|
563
|
+
runBacktest,
|
|
564
|
+
promoteProcess,
|
|
565
|
+
executeBuild,
|
|
566
|
+
buildCompilePrompt,
|
|
567
|
+
parseValueArg,
|
|
568
|
+
parseFlags,
|
|
569
|
+
listProcesses,
|
|
570
|
+
recordsPath,
|
|
571
|
+
runnerPath,
|
|
572
|
+
manifestPath,
|
|
573
|
+
DEFAULT_THRESHOLD,
|
|
574
|
+
};
|
package/commands/console.js
CHANGED
|
@@ -3,6 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { spawn, spawnSync } = require('child_process');
|
|
5
5
|
const readline = require('readline');
|
|
6
|
+
const { resolveClaudeRunnerBin } = require('../lib/runner-command');
|
|
6
7
|
|
|
7
8
|
// ── Context Gathering ──────────────────────────────────────────────
|
|
8
9
|
|
|
@@ -206,7 +207,10 @@ function renderSkillsBar(ctx) {
|
|
|
206
207
|
// ── Backend Detection & Auth ───────────────────────────────────────
|
|
207
208
|
|
|
208
209
|
function detectBackend(requested) {
|
|
209
|
-
const
|
|
210
|
+
const claudeBin = resolveClaudeRunnerBin();
|
|
211
|
+
const hasClaude = claudeBin.includes(path.sep)
|
|
212
|
+
? fs.existsSync(claudeBin)
|
|
213
|
+
: spawnSync('which', [claudeBin], { stdio: 'pipe' }).status === 0;
|
|
210
214
|
const hasCodex = spawnSync('which', ['codex'], { stdio: 'pipe' }).status === 0;
|
|
211
215
|
|
|
212
216
|
if (requested) {
|
|
@@ -277,20 +281,21 @@ function checkAuth(backend) {
|
|
|
277
281
|
// ── Launch ──────────────────────────────────────────────────────────
|
|
278
282
|
|
|
279
283
|
function launchClaude(systemPrompt, extraArgs) {
|
|
284
|
+
const runnerBin = resolveClaudeRunnerBin();
|
|
280
285
|
const args = [
|
|
281
286
|
'--dangerously-skip-permissions',
|
|
282
287
|
'--append-system-prompt', systemPrompt,
|
|
283
288
|
...extraArgs,
|
|
284
289
|
];
|
|
285
290
|
|
|
286
|
-
const child = spawnSync(
|
|
291
|
+
const child = spawnSync(runnerBin, args, {
|
|
287
292
|
cwd: process.cwd(),
|
|
288
293
|
stdio: 'inherit',
|
|
289
294
|
env: { ...process.env, CLAUDECODE: undefined },
|
|
290
295
|
});
|
|
291
296
|
|
|
292
297
|
if (child.error) {
|
|
293
|
-
console.error(`✗ Failed to start
|
|
298
|
+
console.error(`✗ Failed to start ${runnerBin}: ${child.error.message}`);
|
|
294
299
|
process.exit(1);
|
|
295
300
|
}
|
|
296
301
|
process.exit(child.status ?? 0);
|