atris 3.34.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.
Files changed (163) hide show
  1. package/AGENTS.md +35 -0
  2. package/FOR_AGENTS.md +5 -3
  3. package/README.md +5 -3
  4. package/atris/GETTING_STARTED.md +1 -1
  5. package/atris/atris.md +3 -0
  6. package/atris/policies/day-loop-voice.md +102 -0
  7. package/atris/policies/outbound-artifact-gate.md +2 -0
  8. package/atris/skills/design/SKILL.md +56 -32
  9. package/atris/skills/endgame/SKILL.md +12 -6
  10. package/atris/skills/engines/SKILL.md +35 -8
  11. package/atris/skills/fable-method/SKILL.md +66 -0
  12. package/atris/skills/improve/SKILL.md +65 -45
  13. package/atris/skills/render-cli/SKILL.md +88 -0
  14. package/atris/skills/youtube/SKILL.md +10 -1
  15. package/atris.md +6 -2
  16. package/ax +617 -31
  17. package/bin/atris.js +719 -266
  18. package/commands/activate.js +194 -88
  19. package/commands/aeo.js +52 -0
  20. package/commands/agents.js +166 -0
  21. package/commands/autoland.js +718 -72
  22. package/commands/autopilot-front.js +20 -2
  23. package/commands/autopilot.js +118 -2
  24. package/commands/avail.js +407 -0
  25. package/commands/bench.js +188 -0
  26. package/commands/brain.js +3 -0
  27. package/commands/brief.js +651 -0
  28. package/commands/business-sync.js +192 -6
  29. package/commands/business.js +91 -8
  30. package/commands/clean.js +50 -24
  31. package/commands/close.js +1083 -0
  32. package/commands/cloud.js +245 -0
  33. package/commands/codex-goal.js +26 -2
  34. package/commands/compile.js +292 -1
  35. package/commands/computer.js +150 -3
  36. package/commands/dream.js +365 -0
  37. package/commands/drill.js +371 -0
  38. package/commands/drive.js +187 -0
  39. package/commands/engine.js +1061 -29
  40. package/commands/experiments.js +28 -0
  41. package/commands/feed.js +202 -0
  42. package/commands/feedback.js +34 -12
  43. package/commands/fleet-report.js +206 -0
  44. package/commands/github.js +38 -0
  45. package/commands/gm.js +285 -3
  46. package/commands/goal.js +247 -0
  47. package/commands/improve.js +642 -26
  48. package/commands/init.js +83 -43
  49. package/commands/integrations.js +39 -11
  50. package/commands/interview.js +209 -0
  51. package/commands/land.js +253 -52
  52. package/commands/lesson.js +112 -1
  53. package/commands/lifecycle.js +39 -3
  54. package/commands/linear.js +38 -0
  55. package/commands/log.js +84 -1
  56. package/commands/loops.js +220 -16
  57. package/commands/meet.js +220 -0
  58. package/commands/member.js +899 -66
  59. package/commands/mission.js +3512 -332
  60. package/commands/next.js +137 -0
  61. package/commands/now.js +240 -21
  62. package/commands/one-lap.js +776 -0
  63. package/commands/orb.js +314 -0
  64. package/commands/pack-craft.js +179 -0
  65. package/commands/pack.js +823 -0
  66. package/commands/play.js +3 -2
  67. package/commands/probe.js +30 -3
  68. package/commands/pulse.js +241 -46
  69. package/commands/push.js +260 -82
  70. package/commands/radar.js +259 -14
  71. package/commands/rainmaker.js +49 -0
  72. package/commands/report.js +415 -0
  73. package/commands/scout.js +147 -0
  74. package/commands/search.js +363 -0
  75. package/commands/serve.js +54 -0
  76. package/commands/skill.js +47 -3
  77. package/commands/slop.js +50 -2
  78. package/commands/soul.js +1 -1
  79. package/commands/status.js +50 -5
  80. package/commands/stream.js +861 -0
  81. package/commands/stripe.js +38 -0
  82. package/commands/study.js +693 -0
  83. package/commands/supabase.js +39 -0
  84. package/commands/sync.js +67 -54
  85. package/commands/task.js +2275 -182
  86. package/commands/team.js +73 -0
  87. package/commands/truth.js +29 -3
  88. package/commands/unknowns.js +627 -0
  89. package/commands/update.js +44 -0
  90. package/commands/vercel.js +38 -0
  91. package/commands/verify.js +96 -0
  92. package/commands/watch.js +303 -0
  93. package/commands/wish.js +500 -0
  94. package/commands/workflow.js +11 -5
  95. package/commands/worktree.js +299 -20
  96. package/commands/write.js +399 -0
  97. package/commands/xp.js +29 -11
  98. package/lib/auto-accept-certified.js +391 -43
  99. package/lib/autoland.js +353 -52
  100. package/lib/ax-auto-lane.js +79 -0
  101. package/lib/bench/context.js +147 -0
  102. package/lib/bench/engines.js +141 -0
  103. package/lib/bench/report.js +140 -0
  104. package/lib/bench/runner.js +512 -0
  105. package/lib/brief-ledger.js +350 -0
  106. package/lib/cloud-mission.js +259 -0
  107. package/lib/codex-flight.js +154 -0
  108. package/lib/default-runner.js +45 -0
  109. package/lib/default-verifier.js +70 -0
  110. package/lib/engine-registry.js +232 -0
  111. package/lib/experiments/daily.js +640 -0
  112. package/lib/fleet.js +2431 -38
  113. package/lib/improve-vitals-html.js +171 -0
  114. package/lib/known-commands.js +58 -0
  115. package/lib/loop-doctor.js +416 -0
  116. package/lib/member-switches.js +144 -0
  117. package/lib/memory-view.js +14 -5
  118. package/lib/mission-room.js +1 -0
  119. package/lib/mission-root.js +52 -0
  120. package/lib/mission-runtime-loop.js +7 -0
  121. package/lib/next-moves.js +327 -10
  122. package/lib/official-cli-integration.js +174 -0
  123. package/lib/one-lap-validator.js +60 -0
  124. package/lib/orb-context.js +477 -0
  125. package/lib/orb-scorecard.js +224 -0
  126. package/lib/outbound-send-gate.js +165 -0
  127. package/lib/permission-grants.js +293 -0
  128. package/lib/policy-lessons.js +52 -1
  129. package/lib/pulse.js +277 -3
  130. package/lib/receipt-block.js +168 -0
  131. package/lib/receipt-evidence.js +65 -4
  132. package/lib/review-integrity.js +147 -0
  133. package/lib/router-brain.js +352 -0
  134. package/lib/runner-command.js +33 -0
  135. package/lib/self-drive.js +258 -0
  136. package/lib/short-name.js +103 -0
  137. package/lib/spawn-env.js +18 -0
  138. package/lib/state-detection.js +56 -1
  139. package/lib/sync-status.js +59 -0
  140. package/lib/task-db.js +319 -27
  141. package/lib/task-proof.js +43 -1
  142. package/lib/task-receipt.js +93 -0
  143. package/lib/team-presence.js +260 -0
  144. package/lib/tool-result-encode.js +7 -0
  145. package/lib/trust-tiers.js +90 -0
  146. package/lib/usage.js +107 -0
  147. package/lib/voice-gate.js +163 -0
  148. package/lib/wish-audit.js +1368 -0
  149. package/lib/wish-delegate.js +1840 -0
  150. package/lib/wish-design.js +110 -0
  151. package/lib/wish-stats.js +183 -0
  152. package/lib/wish-store.js +354 -0
  153. package/lib/zip.js +221 -0
  154. package/package.json +3 -1
  155. package/templates/loops/atris/loops/LOOPS.md +55 -0
  156. package/templates/loops/atris/loops/TICK.md +24 -0
  157. package/templates/loops/atris/loops/feedback.md +22 -0
  158. package/templates/loops/atris/loops/quality.md +22 -0
  159. package/templates/loops/atris/wiki/systems/loops.md +41 -0
  160. package/utils/api.js +5 -1
  161. package/utils/auth.js +57 -21
  162. package/utils/update-check.js +27 -6
  163. package/atris/learnings.jsonl +0 -1
@@ -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
+ };
@@ -99,11 +99,35 @@ function chmodPrivate(filePath) {
99
99
  }
100
100
  }
101
101
 
102
- function runSqliteJson(dbPath, sql, { readonly = true } = {}) {
102
+ function runSqliteOnce(dbPath, sql, readonly) {
103
103
  const sqliteArgs = [];
104
104
  if (readonly) sqliteArgs.push('-readonly');
105
105
  sqliteArgs.push('-json', dbPath, sql);
106
- const result = spawnSync('sqlite3', sqliteArgs, { encoding: 'utf8' });
106
+ return spawnSync('sqlite3', sqliteArgs, { encoding: 'utf8' });
107
+ }
108
+
109
+ function runSqliteJson(dbPath, sql, { readonly = true } = {}) {
110
+ let result = runSqliteOnce(dbPath, sql, readonly);
111
+ // Codex DBs are WAL-mode. `sqlite3 -readonly` cannot open a WAL db unless a
112
+ // writer already holds the -shm/-wal sidecars open (error 14 with no Codex
113
+ // running). Fall back to reading a private snapshot copy of db+wal.
114
+ const walLocked =
115
+ readonly &&
116
+ result.status !== 0 &&
117
+ /unable to open database file/i.test(String(result.stderr || result.stdout || ''));
118
+ if (walLocked) {
119
+ const snapDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-codex-goal-'));
120
+ const snapDb = path.join(snapDir, path.basename(dbPath));
121
+ try {
122
+ fs.copyFileSync(dbPath, snapDb);
123
+ for (const ext of ['-wal', '-shm']) {
124
+ if (fs.existsSync(dbPath + ext)) fs.copyFileSync(dbPath + ext, snapDb + ext);
125
+ }
126
+ result = runSqliteOnce(snapDb, sql, false);
127
+ } finally {
128
+ fs.rmSync(snapDir, { recursive: true, force: true });
129
+ }
130
+ }
107
131
  if (result.error) throw new Error(`sqlite3 failed: ${result.error.message}`);
108
132
  if (result.status !== 0) {
109
133
  const detail = (result.stderr || result.stdout || '').trim();
@@ -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) {