atris 3.35.0 → 3.36.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 +37 -0
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +22 -4
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +2 -0
- package/ax +147 -19
- package/bin/atris.js +565 -265
- package/commands/activate.js +194 -88
- package/commands/agents.js +166 -0
- package/commands/autoland.js +459 -107
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/engine.js +993 -32
- package/commands/experiments.js +28 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/gm.js +23 -0
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +72 -44
- package/commands/interview.js +67 -1
- package/commands/land.js +152 -52
- package/commands/lifecycle.js +39 -3
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +511 -34
- package/commands/mission.js +3029 -339
- package/commands/next.js +137 -0
- package/commands/now.js +220 -25
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/stream.js +861 -0
- package/commands/study.js +693 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +1346 -117
- package/commands/team.js +73 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +234 -13
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +331 -34
- package/lib/autoland.js +319 -54
- package/lib/ax-auto-lane.js +79 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2219 -67
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/next-moves.js +327 -10
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +10 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +108 -29
- package/lib/task-proof.js +23 -1
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { loadCredentials } = require('../utils/auth');
|
|
4
|
+
const { apiRequestJson } = require('../utils/api');
|
|
5
|
+
const { loadBusinesses, saveBusinesses, businessMatchesSlug } = require('./business');
|
|
6
|
+
const { computeLocalHashes, isIgnoredSyncPath } = require('../lib/manifest');
|
|
7
|
+
const { assertSafeWorkspaceRoot } = require('../lib/workspace-safety');
|
|
8
|
+
const { isMassDeletePlan } = require('./push');
|
|
9
|
+
|
|
10
|
+
const DEFAULT_DEPS = {
|
|
11
|
+
loadCredentials,
|
|
12
|
+
apiRequestJson,
|
|
13
|
+
loadBusinesses,
|
|
14
|
+
saveBusinesses,
|
|
15
|
+
computeLocalHashes,
|
|
16
|
+
isMassDeletePlan,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function parseCloudCleanArgs(args = []) {
|
|
20
|
+
const positional = args.filter((arg) => arg && !arg.startsWith('-'));
|
|
21
|
+
const help = args.includes('--help') || args.includes('-h') || positional[0] === 'help';
|
|
22
|
+
const subcommand = positional[0] || (help ? 'help' : 'clean');
|
|
23
|
+
const slug = positional[1] || null;
|
|
24
|
+
const dryRun = args.includes('--dry-run');
|
|
25
|
+
const yes = args.includes('--yes') || args.includes('-y');
|
|
26
|
+
const deleteAll = args.includes('--delete-all');
|
|
27
|
+
return { subcommand, slug, dryRun, yes, deleteAll, help };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readCloudCleanSlug(cwd, args) {
|
|
31
|
+
const parsed = parseCloudCleanArgs(args);
|
|
32
|
+
if (parsed.slug) return parsed.slug;
|
|
33
|
+
const bizFile = path.join(cwd, '.atris', 'business.json');
|
|
34
|
+
if (fs.existsSync(bizFile)) {
|
|
35
|
+
try {
|
|
36
|
+
const biz = JSON.parse(fs.readFileSync(bizFile, 'utf8'));
|
|
37
|
+
return biz.slug || biz.name || null;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function resolveBusinessWorkspace(token, slug, deps = {}) {
|
|
46
|
+
const d = { ...DEFAULT_DEPS, ...deps };
|
|
47
|
+
const businesses = d.loadBusinesses();
|
|
48
|
+
const listResult = await d.apiRequestJson('/business/', { method: 'GET', token });
|
|
49
|
+
if (listResult.ok) {
|
|
50
|
+
const match = (listResult.data || []).find((b) => businessMatchesSlug(b, slug, { includeName: true }));
|
|
51
|
+
if (!match) return null;
|
|
52
|
+
const { id: businessId, workspace_id: workspaceId, name: businessName, slug: resolvedSlug } = match;
|
|
53
|
+
businesses[slug] = {
|
|
54
|
+
business_id: businessId,
|
|
55
|
+
workspace_id: workspaceId,
|
|
56
|
+
name: businessName,
|
|
57
|
+
slug: resolvedSlug,
|
|
58
|
+
added_at: new Date().toISOString(),
|
|
59
|
+
};
|
|
60
|
+
d.saveBusinesses(businesses);
|
|
61
|
+
return { businessId, workspaceId, businessName, resolvedSlug };
|
|
62
|
+
}
|
|
63
|
+
if (businesses[slug]) {
|
|
64
|
+
return {
|
|
65
|
+
businessId: businesses[slug].business_id,
|
|
66
|
+
workspaceId: businesses[slug].workspace_id,
|
|
67
|
+
businessName: businesses[slug].name || slug,
|
|
68
|
+
resolvedSlug: businesses[slug].slug || slug,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeCloudPath(p) {
|
|
75
|
+
const s = String(p || '').replace(/\\/g, '/');
|
|
76
|
+
return s.startsWith('/') ? s : `/${s}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function collectCloudOrphans(cwd, slug, deps = {}) {
|
|
80
|
+
const d = { ...DEFAULT_DEPS, ...deps };
|
|
81
|
+
const credentials = d.loadCredentials();
|
|
82
|
+
if (!credentials || !credentials.token) {
|
|
83
|
+
throw new Error('Not logged in. Run: atris login');
|
|
84
|
+
}
|
|
85
|
+
assertSafeWorkspaceRoot(cwd, { op: 'cloud clean' });
|
|
86
|
+
const resolvedSlug = slug || readCloudCleanSlug(cwd, []);
|
|
87
|
+
if (!resolvedSlug) {
|
|
88
|
+
throw new Error('No business workspace found. Run inside a business workspace or pass a business slug.');
|
|
89
|
+
}
|
|
90
|
+
const biz = await resolveBusinessWorkspace(credentials.token, resolvedSlug, d);
|
|
91
|
+
if (!biz) {
|
|
92
|
+
throw new Error(`Business "${resolvedSlug}" not found.`);
|
|
93
|
+
}
|
|
94
|
+
const localFiles = d.computeLocalHashes(cwd);
|
|
95
|
+
const result = await d.apiRequestJson(
|
|
96
|
+
`/business/${biz.businessId}/workspaces/${biz.workspaceId}/snapshot?include_content=false`,
|
|
97
|
+
{ method: 'GET', token: credentials.token, timeoutMs: 120000 }
|
|
98
|
+
);
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
throw new Error(`could not list cloud files: ${result.error || result.status}`);
|
|
101
|
+
}
|
|
102
|
+
const cloudFiles = (result.data && Array.isArray(result.data.files)) ? result.data.files : [];
|
|
103
|
+
const cloudPaths = new Set();
|
|
104
|
+
for (const f of cloudFiles) {
|
|
105
|
+
if (!f || f.binary) continue;
|
|
106
|
+
const normalized = normalizeCloudPath(f.path);
|
|
107
|
+
if (!normalized) continue;
|
|
108
|
+
if (isIgnoredSyncPath(normalized)) continue;
|
|
109
|
+
if (path.basename(normalized).startsWith('.')) continue;
|
|
110
|
+
cloudPaths.add(normalized);
|
|
111
|
+
}
|
|
112
|
+
const orphanPaths = [];
|
|
113
|
+
for (const p of cloudPaths) {
|
|
114
|
+
if (!localFiles[p]) {
|
|
115
|
+
orphanPaths.push(p);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
orphanPaths.sort();
|
|
119
|
+
return {
|
|
120
|
+
orphanPaths,
|
|
121
|
+
localCount: Object.keys(localFiles).length,
|
|
122
|
+
cloudCount: cloudPaths.size,
|
|
123
|
+
...biz,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function renderCloudCleanSummary(orphanPaths, { dryRun = false, deleted = null, failed = null, localCount = 0 } = {}) {
|
|
128
|
+
const count = orphanPaths.length;
|
|
129
|
+
if (count === 0) {
|
|
130
|
+
return 'No cloud orphans found.\n';
|
|
131
|
+
}
|
|
132
|
+
const fileLabel = count === 1 ? 'file' : 'files';
|
|
133
|
+
const label = dryRun ? 'cloud clean (dry run):' : 'cloud clean:';
|
|
134
|
+
const lines = [
|
|
135
|
+
`${label} ${count} cloud ${fileLabel} not present locally:`,
|
|
136
|
+
...orphanPaths.map((p) => ` - ${p.replace(/^\//, '')}`),
|
|
137
|
+
'',
|
|
138
|
+
];
|
|
139
|
+
if (dryRun) {
|
|
140
|
+
lines.push('pass --yes to delete these cloud files.');
|
|
141
|
+
} else if (deleted !== null) {
|
|
142
|
+
const parts = [];
|
|
143
|
+
if (deleted > 0) parts.push(`${deleted} deleted`);
|
|
144
|
+
if (failed > 0) parts.push(`${failed} failed`);
|
|
145
|
+
if (parts.length === 0) parts.push(`${count} deleted`);
|
|
146
|
+
lines.push(`${parts.join(', ')}. local files: ${localCount}`);
|
|
147
|
+
} else {
|
|
148
|
+
lines.push('pass --yes to delete these cloud files.');
|
|
149
|
+
}
|
|
150
|
+
return lines.join('\n') + '\n';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function cloudClean(args = [], cwd = process.cwd(), deps = {}) {
|
|
154
|
+
const d = { ...DEFAULT_DEPS, ...deps };
|
|
155
|
+
const options = parseCloudCleanArgs(args);
|
|
156
|
+
if (options.help || options.subcommand === 'help') {
|
|
157
|
+
return {
|
|
158
|
+
ok: true,
|
|
159
|
+
output: [
|
|
160
|
+
'Usage: atris cloud clean [business] [--dry-run] [--yes] [--delete-all]',
|
|
161
|
+
'',
|
|
162
|
+
'List or delete cloud files that are not present locally.',
|
|
163
|
+
'',
|
|
164
|
+
' --dry-run Preview which cloud files would be deleted',
|
|
165
|
+
' --yes Delete the listed cloud files',
|
|
166
|
+
' --delete-all Confirm a mass delete (required with --yes for large cleanups)',
|
|
167
|
+
'',
|
|
168
|
+
].join('\n') + '\n',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (options.subcommand !== 'clean') {
|
|
172
|
+
throw new Error(`Unknown cloud subcommand: ${options.subcommand}. Try: atris cloud clean --help`);
|
|
173
|
+
}
|
|
174
|
+
const slug = options.slug || readCloudCleanSlug(cwd, args);
|
|
175
|
+
if (!slug) {
|
|
176
|
+
throw new Error('No business workspace found. Run inside a business workspace or pass a business slug.');
|
|
177
|
+
}
|
|
178
|
+
const { orphanPaths, localCount, businessId, workspaceId } = await collectCloudOrphans(cwd, slug, d);
|
|
179
|
+
if (orphanPaths.length === 0) {
|
|
180
|
+
return { ok: true, output: 'No cloud orphans found.\n' };
|
|
181
|
+
}
|
|
182
|
+
if (!options.yes) {
|
|
183
|
+
return {
|
|
184
|
+
ok: true,
|
|
185
|
+
output: renderCloudCleanSummary(orphanPaths, { dryRun: options.dryRun, localCount }),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (isMassDeletePlan({ deletedPaths: orphanPaths, filesToPush: [], unchangedCount: localCount }) && !options.deleteAll) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`mass delete guard: ${orphanPaths.length} cloud files would be deleted with only ${localCount} local files present.\n` +
|
|
191
|
+
'this looks like a wrong source root or empty local copy. pass --delete-all with --yes to confirm.'
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
let deleted = 0;
|
|
195
|
+
let failed = 0;
|
|
196
|
+
const credentials = d.loadCredentials();
|
|
197
|
+
for (let i = 0; i < orphanPaths.length; i++) {
|
|
198
|
+
const filePath = orphanPaths[i];
|
|
199
|
+
if (i > 0) {
|
|
200
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
201
|
+
}
|
|
202
|
+
let result = await d.apiRequestJson(
|
|
203
|
+
`/business/${businessId}/workspaces/${workspaceId}/file?path=${encodeURIComponent(filePath)}`,
|
|
204
|
+
{ method: 'DELETE', token: credentials.token }
|
|
205
|
+
);
|
|
206
|
+
if (result.status === 429) {
|
|
207
|
+
await new Promise((r) => setTimeout(r, 20000));
|
|
208
|
+
result = await d.apiRequestJson(
|
|
209
|
+
`/business/${businessId}/workspaces/${workspaceId}/file?path=${encodeURIComponent(filePath)}`,
|
|
210
|
+
{ method: 'DELETE', token: credentials.token }
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (result.ok || result.status === 404) {
|
|
214
|
+
deleted++;
|
|
215
|
+
} else {
|
|
216
|
+
failed++;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
ok: failed === 0,
|
|
221
|
+
output: renderCloudCleanSummary(orphanPaths, { dryRun: false, deleted, failed, localCount }),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function cloudAtris() {
|
|
226
|
+
const args = process.argv.slice(3);
|
|
227
|
+
cloudClean(args, process.cwd())
|
|
228
|
+
.then((result) => {
|
|
229
|
+
process.stdout.write(result.output || '');
|
|
230
|
+
process.exit(result.ok ? 0 : 1);
|
|
231
|
+
})
|
|
232
|
+
.catch((err) => {
|
|
233
|
+
console.error(`\n✗ Error: ${err.message || err}`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = {
|
|
239
|
+
cloudAtris,
|
|
240
|
+
cloudClean,
|
|
241
|
+
collectCloudOrphans,
|
|
242
|
+
parseCloudCleanArgs,
|
|
243
|
+
readCloudCleanSlug,
|
|
244
|
+
renderCloudCleanSummary,
|
|
245
|
+
};
|
package/commands/compile.js
CHANGED
|
@@ -17,12 +17,15 @@
|
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
|
+
const crypto = require('crypto');
|
|
20
21
|
const { execSync } = require('child_process');
|
|
21
22
|
const {
|
|
22
23
|
buildRunnerAvailabilityCommand,
|
|
23
24
|
buildRunnerCommand,
|
|
24
25
|
resolveClaudeRunnerBin,
|
|
25
26
|
} = require('../lib/runner-command');
|
|
27
|
+
const { apiRequestJson, getApiBaseUrl } = require('../utils/api');
|
|
28
|
+
const { loadCredentials } = require('../utils/auth');
|
|
26
29
|
|
|
27
30
|
const DEFAULT_THRESHOLD = 0.99;
|
|
28
31
|
const SAMPLE_RECORDS_FOR_BUILD = 25;
|
|
@@ -38,6 +41,10 @@ function recordsPath(root, name) {
|
|
|
38
41
|
return path.join(root, '.atris', 'state', 'processes', name, 'records.jsonl');
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
function pushCursorPath(root, name) {
|
|
45
|
+
return path.join(root, '.atris', 'state', 'processes', name, 'push-cursor.json');
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
function manifestPath(root, name) {
|
|
42
49
|
return path.join(processDir(root, name), 'process.json');
|
|
43
50
|
}
|
|
@@ -134,6 +141,172 @@ function listProcesses(root) {
|
|
|
134
141
|
return [...names].sort();
|
|
135
142
|
}
|
|
136
143
|
|
|
144
|
+
function readPushCursor(root, name) {
|
|
145
|
+
const p = pushCursorPath(root, name);
|
|
146
|
+
if (!fs.existsSync(p)) return null;
|
|
147
|
+
try {
|
|
148
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
149
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function writePushCursor(root, name, cursor) {
|
|
156
|
+
const p = pushCursorPath(root, name);
|
|
157
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
158
|
+
const tmp = `${p}.tmp`;
|
|
159
|
+
fs.writeFileSync(tmp, JSON.stringify(cursor, null, 2) + '\n');
|
|
160
|
+
fs.renameSync(tmp, p);
|
|
161
|
+
return cursor;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function hashString(value) {
|
|
165
|
+
return crypto.createHash('sha256').update(String(value)).digest('hex');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function pushContextKey(ctx) {
|
|
169
|
+
return hashString(`${ctx.apiBase}\0${ctx.agentId}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function recordKey(name, index) {
|
|
173
|
+
return `compile-record-v1:${hashString(`${name}\0${index}`)}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function normalizePushCursorState(raw) {
|
|
177
|
+
if (raw && typeof raw === 'object' && raw.contexts && typeof raw.contexts === 'object' && !Array.isArray(raw.contexts)) {
|
|
178
|
+
return { version: 1, contexts: { ...raw.contexts } };
|
|
179
|
+
}
|
|
180
|
+
if (
|
|
181
|
+
raw
|
|
182
|
+
&& typeof raw === 'object'
|
|
183
|
+
&& typeof raw.agent_id === 'string'
|
|
184
|
+
&& typeof raw.api_base === 'string'
|
|
185
|
+
) {
|
|
186
|
+
return {
|
|
187
|
+
version: 1,
|
|
188
|
+
contexts: {
|
|
189
|
+
[pushContextKey({ agentId: raw.agent_id, apiBase: raw.api_base })]: raw,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return { version: 1, contexts: {} };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function cursorForContext(cursorState, ctx) {
|
|
197
|
+
const state = normalizePushCursorState(cursorState);
|
|
198
|
+
const cursor = state.contexts[pushContextKey(ctx)];
|
|
199
|
+
if (!cursor || cursor.agent_id !== ctx.agentId || cursor.api_base !== ctx.apiBase) return null;
|
|
200
|
+
return cursor;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function writePushCursorForContext(root, name, cursorState, ctx, nextRecordIndex) {
|
|
204
|
+
const state = normalizePushCursorState(cursorState);
|
|
205
|
+
state.contexts[pushContextKey(ctx)] = {
|
|
206
|
+
agent_id: ctx.agentId,
|
|
207
|
+
api_base: ctx.apiBase,
|
|
208
|
+
nextRecordIndex,
|
|
209
|
+
updatedAt: new Date().toISOString(),
|
|
210
|
+
};
|
|
211
|
+
return writePushCursor(root, name, state);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function requireManifest(root, name) {
|
|
215
|
+
const p = manifestPath(root, name);
|
|
216
|
+
if (!fs.existsSync(p)) {
|
|
217
|
+
throw new Error(`no manifest at ${path.relative(root, p)} - run "atris compile build ${name}" first`);
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
221
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
222
|
+
throw new Error('manifest must be a JSON object');
|
|
223
|
+
}
|
|
224
|
+
return parsed;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
throw new Error(`invalid manifest at ${path.relative(root, p)}: ${err.message}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function readRunnerSource(root, name) {
|
|
231
|
+
const p = runnerPath(root, name);
|
|
232
|
+
if (!fs.existsSync(p)) {
|
|
233
|
+
throw new Error(`no compiled artifact at ${path.relative(root, p)} - run "atris compile build ${name}" first`);
|
|
234
|
+
}
|
|
235
|
+
return fs.readFileSync(p, 'utf8');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function requireServerContext(flags) {
|
|
239
|
+
const agentId = typeof flags.agent === 'string' ? flags.agent.trim() : '';
|
|
240
|
+
if (!agentId) {
|
|
241
|
+
throw new Error('server compile commands require --agent <id>');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const credentials = loadCredentials();
|
|
245
|
+
if (!credentials || !credentials.token) {
|
|
246
|
+
throw new Error('Missing Atris auth. Run: atris login or set ATRIS_TOKEN, then retry.');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const apiBase = getApiBaseUrl();
|
|
250
|
+
try {
|
|
251
|
+
// Validate now so failures explain configuration instead of surfacing as
|
|
252
|
+
// opaque network errors from the request helper.
|
|
253
|
+
new URL(apiBase);
|
|
254
|
+
} catch {
|
|
255
|
+
throw new Error(`Invalid ATRIS_API_URL: ${apiBase}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return { agentId, token: credentials.token, apiBase };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function processApiPath(agentId, name, suffix = '') {
|
|
262
|
+
return `/processes/${encodeURIComponent(agentId)}/${encodeURIComponent(name)}${suffix}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function serverErrorMessage(method, pathname, result) {
|
|
266
|
+
const status = result && result.status ? `HTTP ${result.status}` : 'network error';
|
|
267
|
+
const detail = result && (result.error || result.text) ? `: ${result.error || result.text}` : '';
|
|
268
|
+
return `${method} ${pathname} failed (${status})${detail}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function serverRequest(ctx, method, pathname, body) {
|
|
272
|
+
const options = {
|
|
273
|
+
method,
|
|
274
|
+
token: ctx.token,
|
|
275
|
+
timeoutMs: 60000,
|
|
276
|
+
retries: 0,
|
|
277
|
+
};
|
|
278
|
+
if (body !== undefined) options.body = body;
|
|
279
|
+
const result = await apiRequestJson(pathname, options);
|
|
280
|
+
if (!result || !result.ok) {
|
|
281
|
+
throw new Error(serverErrorMessage(method, pathname, result || {}));
|
|
282
|
+
}
|
|
283
|
+
return result.data || {};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function recordPushPayload(name, record, index) {
|
|
287
|
+
if (!record || typeof record !== 'object') {
|
|
288
|
+
throw new Error(`record ${index + 1} is not an object`);
|
|
289
|
+
}
|
|
290
|
+
if (!Object.prototype.hasOwnProperty.call(record, 'input')) {
|
|
291
|
+
throw new Error(`record ${index + 1} is missing input`);
|
|
292
|
+
}
|
|
293
|
+
if (!Object.prototype.hasOwnProperty.call(record, 'output')) {
|
|
294
|
+
throw new Error(`record ${index + 1} is missing output`);
|
|
295
|
+
}
|
|
296
|
+
const payload = { input: record.input, output: record.output, record_key: recordKey(name, index) };
|
|
297
|
+
if (Object.prototype.hasOwnProperty.call(record, 'expected')) payload.expected = record.expected;
|
|
298
|
+
if (record.source) payload.source = String(record.source);
|
|
299
|
+
return payload;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function cursorStartIndex(cursorState, total, ctx) {
|
|
303
|
+
const cursor = cursorForContext(cursorState, ctx);
|
|
304
|
+
if (!cursor) return 0;
|
|
305
|
+
const n = Number(cursor.nextRecordIndex);
|
|
306
|
+
if (!Number.isFinite(n) || n < 0) return 0;
|
|
307
|
+
return Math.min(Math.floor(n), total);
|
|
308
|
+
}
|
|
309
|
+
|
|
137
310
|
// ── compare ────────────────────────────────────────────────────────
|
|
138
311
|
|
|
139
312
|
function deepEqual(a, b) {
|
|
@@ -372,6 +545,103 @@ function printBacktest(name, result, failureCount, manifest) {
|
|
|
372
545
|
}
|
|
373
546
|
}
|
|
374
547
|
|
|
548
|
+
function extractServerBacktest(data) {
|
|
549
|
+
const result = (data && data.result && typeof data.result === 'object') ? data.result : (data || {});
|
|
550
|
+
const manifest = (data && data.manifest && typeof data.manifest === 'object') ? data.manifest : {};
|
|
551
|
+
return { result, manifest };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function printServerBacktest(name, data, ctx) {
|
|
555
|
+
const { result, manifest } = extractServerBacktest(data);
|
|
556
|
+
const version = result.version !== undefined ? result.version : '?';
|
|
557
|
+
const passed = result.passed !== undefined ? result.passed : '?';
|
|
558
|
+
const total = result.total !== undefined ? result.total : '?';
|
|
559
|
+
const accuracy = typeof result.accuracy === 'number' ? formatAccuracy(result.accuracy) : 'unknown';
|
|
560
|
+
const threshold = typeof result.threshold === 'number' ? formatAccuracy(result.threshold) : 'unknown';
|
|
561
|
+
const status = manifest.status || result.status || 'unknown';
|
|
562
|
+
console.log(`server backtest ${name} v${version}: ${passed}/${total} passed (${accuracy}), gate ${threshold}, status ${status}`);
|
|
563
|
+
if (typeof result.accuracy === 'number' && typeof result.threshold === 'number' && result.accuracy >= result.threshold) {
|
|
564
|
+
console.log(`next: promote on server with "atris compile promote ${name} --server --agent ${ctx.agentId}"`);
|
|
565
|
+
} else {
|
|
566
|
+
console.log('next: fix records or rebuild, publish again, then promote on server when the gate passes');
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function publishProcessToServer(root, name, flags) {
|
|
571
|
+
const ctx = requireServerContext(flags);
|
|
572
|
+
const runJs = readRunnerSource(root, name);
|
|
573
|
+
const manifest = requireManifest(root, name);
|
|
574
|
+
const artifact = await serverRequest(ctx, 'PUT', processApiPath(ctx.agentId, name, '/artifact'), {
|
|
575
|
+
run_js: runJs,
|
|
576
|
+
manifest,
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const serverStatus = artifact && artifact.manifest && artifact.manifest.status
|
|
580
|
+
? artifact.manifest.status
|
|
581
|
+
: 'unknown';
|
|
582
|
+
console.log(`published artifact ${name} to server (${serverStatus})`);
|
|
583
|
+
|
|
584
|
+
const records = readRecords(root, name);
|
|
585
|
+
let cursorState = normalizePushCursorState(readPushCursor(root, name));
|
|
586
|
+
const startIndex = cursorStartIndex(cursorState, records.length, ctx);
|
|
587
|
+
let pushed = 0;
|
|
588
|
+
for (let i = startIndex; i < records.length; i++) {
|
|
589
|
+
await serverRequest(ctx, 'POST', processApiPath(ctx.agentId, name, '/records'), recordPushPayload(name, records[i], i));
|
|
590
|
+
pushed++;
|
|
591
|
+
cursorState = writePushCursorForContext(root, name, cursorState, ctx, i + 1);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (records.length === 0) {
|
|
595
|
+
console.log('no local records to push');
|
|
596
|
+
} else if (pushed === 0) {
|
|
597
|
+
console.log(`records up to date (${records.length} already pushed)`);
|
|
598
|
+
} else {
|
|
599
|
+
console.log(`pushed ${pushed} new record${pushed === 1 ? '' : 's'} (${records.length} total local)`);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const backtest = await serverRequest(ctx, 'POST', processApiPath(ctx.agentId, name, '/backtest'));
|
|
603
|
+
printServerBacktest(name, backtest, ctx);
|
|
604
|
+
return { artifact, recordsPushed: pushed, backtest };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function promoteProcessOnServer(root, name, flags) {
|
|
608
|
+
const ctx = requireServerContext(flags);
|
|
609
|
+
const body = {};
|
|
610
|
+
if (flags.gate !== undefined) {
|
|
611
|
+
const gate = Number(flags.gate);
|
|
612
|
+
if (!(gate > 0 && gate <= 1)) {
|
|
613
|
+
throw new Error('--gate must be between 0 and 1 (e.g. 0.99)');
|
|
614
|
+
}
|
|
615
|
+
body.gate = gate;
|
|
616
|
+
}
|
|
617
|
+
const data = await serverRequest(
|
|
618
|
+
ctx,
|
|
619
|
+
'POST',
|
|
620
|
+
processApiPath(ctx.agentId, name, '/promote'),
|
|
621
|
+
Object.keys(body).length ? body : undefined
|
|
622
|
+
);
|
|
623
|
+
const manifest = data.manifest || {};
|
|
624
|
+
const bt = manifest.backtest || {};
|
|
625
|
+
const accuracy = typeof bt.accuracy === 'number' ? formatAccuracy(bt.accuracy) : 'unknown accuracy';
|
|
626
|
+
const total = bt.total !== undefined ? `${bt.total} records` : 'server records';
|
|
627
|
+
console.log(`${name} v${manifest.version ?? '?'} server status ${manifest.status || 'unknown'} (${accuracy} over ${total})`);
|
|
628
|
+
return data;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function scheduleProcessOnServer(root, name, flags) {
|
|
632
|
+
const ctx = requireServerContext(flags);
|
|
633
|
+
const cron = typeof flags.cron === 'string' ? flags.cron.trim() : '';
|
|
634
|
+
if (!cron) {
|
|
635
|
+
throw new Error('schedule needs --cron "<expr>"');
|
|
636
|
+
}
|
|
637
|
+
const input = flags.input === undefined ? null : parseValueArg(flags.input);
|
|
638
|
+
const data = await serverRequest(ctx, 'POST', processApiPath(ctx.agentId, name, '/schedule'), { cron, input });
|
|
639
|
+
const schedule = data.schedule || {};
|
|
640
|
+
const id = schedule.id || schedule.schedule_id || schedule.name || '(server did not return an id)';
|
|
641
|
+
console.log(`scheduled ${name}: ${id}`);
|
|
642
|
+
return data;
|
|
643
|
+
}
|
|
644
|
+
|
|
375
645
|
// ── command dispatcher ─────────────────────────────────────────────
|
|
376
646
|
|
|
377
647
|
function showCompileHelp() {
|
|
@@ -392,10 +662,17 @@ function showCompileHelp() {
|
|
|
392
662
|
console.log(' promote <name> [--gate 0.99]');
|
|
393
663
|
console.log(' mark active (gate: backtest accuracy >= threshold);');
|
|
394
664
|
console.log(' --gate is the only way to change the standing threshold');
|
|
665
|
+
console.log(' publish <name> --agent <id>');
|
|
666
|
+
console.log(' push run.js, process.json, new records, then run server backtest');
|
|
667
|
+
console.log(' promote <name> --server --agent <id> [--gate 0.99]');
|
|
668
|
+
console.log(' promote the server-side process');
|
|
669
|
+
console.log(' schedule <name> --agent <id> --cron "<expr>" --input \'<json|@file>\'');
|
|
670
|
+
console.log(' schedule the server-side process');
|
|
395
671
|
console.log(' exec <name> --input <json|@file> [--record]');
|
|
396
672
|
console.log(' run the compiled process on new input');
|
|
397
673
|
console.log(' status [<name>] [--json] list processes, versions, accuracy, drift');
|
|
398
674
|
console.log('');
|
|
675
|
+
console.log('Server commands require --agent <id> and Atris auth from atris login or ATRIS_TOKEN.');
|
|
399
676
|
console.log('Loop: record real runs -> build -> backtest -> promote -> exec --record');
|
|
400
677
|
console.log('When backtest accuracy drops below the gate, the process is marked drifted');
|
|
401
678
|
console.log('and a recompile is suggested — self-healing against process drift.');
|
|
@@ -414,7 +691,7 @@ async function compileCommand(subcommand, ...rawArgs) {
|
|
|
414
691
|
// `atris compile <name>` with an existing process name = build shortcut
|
|
415
692
|
let cmd = subcommand;
|
|
416
693
|
let name = positional[0];
|
|
417
|
-
const subcommands = ['record', 'build', 'backtest', 'promote', 'exec', 'status'];
|
|
694
|
+
const subcommands = ['record', 'build', 'backtest', 'promote', 'publish', 'schedule', 'exec', 'status'];
|
|
418
695
|
if (!subcommands.includes(subcommand)) {
|
|
419
696
|
if (validName(subcommand) && listProcesses(root).includes(subcommand)) {
|
|
420
697
|
cmd = 'build';
|
|
@@ -514,6 +791,10 @@ async function compileCommand(subcommand, ...rawArgs) {
|
|
|
514
791
|
}
|
|
515
792
|
|
|
516
793
|
if (cmd === 'promote') {
|
|
794
|
+
if (flags.server) {
|
|
795
|
+
await promoteProcessOnServer(root, name, flags);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
517
798
|
const options = {};
|
|
518
799
|
if (flags.gate !== undefined) {
|
|
519
800
|
const t = Number(flags.gate);
|
|
@@ -530,6 +811,16 @@ async function compileCommand(subcommand, ...rawArgs) {
|
|
|
530
811
|
return;
|
|
531
812
|
}
|
|
532
813
|
|
|
814
|
+
if (cmd === 'publish') {
|
|
815
|
+
await publishProcessToServer(root, name, flags);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (cmd === 'schedule') {
|
|
820
|
+
await scheduleProcessOnServer(root, name, flags);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
533
824
|
if (cmd === 'exec') {
|
|
534
825
|
const input = parseValueArg(flags.input);
|
|
535
826
|
if (input === undefined) {
|