atris 3.35.0 → 3.37.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 (133) hide show
  1. package/AGENTS.md +37 -0
  2. package/README.md +5 -3
  3. package/atris/GETTING_STARTED.md +1 -1
  4. package/atris/atris.md +3 -0
  5. package/atris/policies/day-loop-voice.md +102 -0
  6. package/atris/policies/outbound-artifact-gate.md +2 -0
  7. package/atris/skills/design/SKILL.md +56 -32
  8. package/atris/skills/endgame/SKILL.md +12 -6
  9. package/atris/skills/engines/SKILL.md +22 -4
  10. package/atris/skills/fable-method/SKILL.md +66 -0
  11. package/atris/skills/improve/SKILL.md +65 -45
  12. package/atris/skills/youtube/SKILL.md +10 -1
  13. package/atris.md +2 -0
  14. package/ax +147 -19
  15. package/bin/atris.js +576 -267
  16. package/commands/activate.js +194 -88
  17. package/commands/agents.js +166 -0
  18. package/commands/autoland.js +459 -107
  19. package/commands/autopilot-front.js +20 -2
  20. package/commands/autopilot.js +118 -2
  21. package/commands/avail.js +407 -0
  22. package/commands/bench.js +188 -0
  23. package/commands/brain.js +3 -0
  24. package/commands/brief.js +651 -0
  25. package/commands/business-sync.js +192 -6
  26. package/commands/clean.js +50 -24
  27. package/commands/close.js +1083 -0
  28. package/commands/cloud.js +245 -0
  29. package/commands/compile.js +292 -1
  30. package/commands/computer.js +150 -3
  31. package/commands/dream.js +365 -0
  32. package/commands/drill.js +371 -0
  33. package/commands/engine.js +993 -32
  34. package/commands/experiments.js +28 -0
  35. package/commands/feedback.js +34 -12
  36. package/commands/fleet-report.js +206 -0
  37. package/commands/gm.js +23 -0
  38. package/commands/goal.js +247 -0
  39. package/commands/improve.js +642 -26
  40. package/commands/init.js +72 -44
  41. package/commands/interview.js +67 -1
  42. package/commands/land.js +152 -52
  43. package/commands/lifecycle.js +39 -3
  44. package/commands/log.js +84 -1
  45. package/commands/loops.js +220 -16
  46. package/commands/meet.js +220 -0
  47. package/commands/member.js +511 -34
  48. package/commands/mission.js +3144 -338
  49. package/commands/next.js +137 -0
  50. package/commands/now.js +220 -25
  51. package/commands/one-lap.js +776 -0
  52. package/commands/orb.js +314 -0
  53. package/commands/pack-craft.js +179 -0
  54. package/commands/pack.js +823 -0
  55. package/commands/play.js +3 -2
  56. package/commands/probe.js +30 -3
  57. package/commands/pulse.js +241 -46
  58. package/commands/push.js +260 -82
  59. package/commands/rainmaker.js +49 -0
  60. package/commands/report.js +415 -0
  61. package/commands/scout.js +147 -0
  62. package/commands/search.js +363 -0
  63. package/commands/skill.js +47 -3
  64. package/commands/slop.js +50 -2
  65. package/commands/soul.js +1 -1
  66. package/commands/stream.js +861 -0
  67. package/commands/study.js +693 -0
  68. package/commands/sync.js +67 -54
  69. package/commands/task.js +1346 -117
  70. package/commands/team.js +73 -0
  71. package/commands/verify.js +96 -0
  72. package/commands/watch.js +303 -0
  73. package/commands/wish.js +500 -0
  74. package/commands/workflow.js +11 -5
  75. package/commands/worktree.js +234 -13
  76. package/commands/xp.js +29 -11
  77. package/lib/auto-accept-certified.js +331 -34
  78. package/lib/autoland.js +319 -54
  79. package/lib/ax-auto-lane.js +96 -0
  80. package/lib/bench/context.js +147 -0
  81. package/lib/bench/engines.js +141 -0
  82. package/lib/bench/report.js +140 -0
  83. package/lib/bench/runner.js +512 -0
  84. package/lib/brief-ledger.js +350 -0
  85. package/lib/cloud-mission.js +259 -0
  86. package/lib/codex-flight.js +154 -0
  87. package/lib/default-runner.js +45 -0
  88. package/lib/default-verifier.js +70 -0
  89. package/lib/engine-registry.js +232 -0
  90. package/lib/experiments/daily.js +640 -0
  91. package/lib/fleet.js +2219 -67
  92. package/lib/improve-vitals-html.js +171 -0
  93. package/lib/known-commands.js +58 -0
  94. package/lib/loop-doctor.js +416 -0
  95. package/lib/member-switches.js +144 -0
  96. package/lib/mission-room.js +1 -0
  97. package/lib/mission-root.js +52 -0
  98. package/lib/next-moves.js +327 -10
  99. package/lib/one-lap-validator.js +60 -0
  100. package/lib/orb-context.js +477 -0
  101. package/lib/orb-scorecard.js +224 -0
  102. package/lib/policy-lessons.js +52 -1
  103. package/lib/pulse.js +277 -3
  104. package/lib/receipt-block.js +168 -0
  105. package/lib/receipt-evidence.js +65 -4
  106. package/lib/router-brain.js +352 -0
  107. package/lib/runner-command.js +10 -0
  108. package/lib/self-drive.js +258 -0
  109. package/lib/short-name.js +103 -0
  110. package/lib/spawn-env.js +18 -0
  111. package/lib/state-detection.js +56 -1
  112. package/lib/sync-status.js +59 -0
  113. package/lib/task-db.js +108 -29
  114. package/lib/task-proof.js +23 -1
  115. package/lib/team-presence.js +260 -0
  116. package/lib/tool-result-encode.js +7 -0
  117. package/lib/trust-tiers.js +90 -0
  118. package/lib/usage.js +107 -0
  119. package/lib/voice-gate.js +163 -0
  120. package/lib/wish-audit.js +1368 -0
  121. package/lib/wish-delegate.js +1840 -0
  122. package/lib/wish-design.js +110 -0
  123. package/lib/wish-stats.js +183 -0
  124. package/lib/wish-store.js +354 -0
  125. package/lib/zip.js +221 -0
  126. package/package.json +3 -1
  127. package/templates/loops/atris/loops/LOOPS.md +55 -0
  128. package/templates/loops/atris/loops/TICK.md +24 -0
  129. package/templates/loops/atris/loops/feedback.md +22 -0
  130. package/templates/loops/atris/loops/quality.md +22 -0
  131. package/templates/loops/atris/wiki/systems/loops.md +41 -0
  132. package/utils/api.js +5 -1
  133. package/utils/auth.js +57 -21
@@ -0,0 +1,640 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+
7
+ const SCORECARD_SCHEMA = 'atris.experiment.daily.v1';
8
+ const BENCH_SCHEMA = 'atris.bench.run.v1';
9
+
10
+ function todayDate() {
11
+ return new Date().toISOString().slice(0, 10);
12
+ }
13
+
14
+ function statePath(root) {
15
+ return path.join(root, '.atris', 'state', 'experiments-daily.json');
16
+ }
17
+
18
+ function queuePath(root) {
19
+ return path.join(root, 'atris', 'experiments', 'daily', 'queue.jsonl');
20
+ }
21
+
22
+ function resultsPath(root) {
23
+ return path.join(root, 'atris', 'experiments', 'daily', 'results.tsv');
24
+ }
25
+
26
+ function scorecardsPath(root) {
27
+ return path.join(root, '.atris', 'state', 'scorecards.jsonl');
28
+ }
29
+
30
+ function readJson(file, fallback) {
31
+ try {
32
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
33
+ } catch {
34
+ return fallback;
35
+ }
36
+ }
37
+
38
+ function writeJson(file, value) {
39
+ fs.mkdirSync(path.dirname(file), { recursive: true });
40
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
41
+ }
42
+
43
+ function readDailyState(root) {
44
+ const state = readJson(statePath(root), {});
45
+ if (!Array.isArray(state.history)) state.history = [];
46
+ return state;
47
+ }
48
+
49
+ function writeDailyState(root, state) {
50
+ writeJson(statePath(root), state);
51
+ }
52
+
53
+ function readQueue(root) {
54
+ const file = queuePath(root);
55
+ if (!fs.existsSync(file)) return [];
56
+ const out = [];
57
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
58
+ const trimmed = line.trim();
59
+ if (!trimmed) continue;
60
+ try {
61
+ out.push(JSON.parse(trimmed));
62
+ } catch {
63
+ // skip malformed lines
64
+ }
65
+ }
66
+ return out;
67
+ }
68
+
69
+ function appendQueueEntry(root, entry) {
70
+ const file = queuePath(root);
71
+ fs.mkdirSync(path.dirname(file), { recursive: true });
72
+ fs.appendFileSync(file, `${JSON.stringify(entry)}\n`, 'utf8');
73
+ }
74
+
75
+ function pickQueueEntry(root, history = []) {
76
+ const seen = new Set(history);
77
+ return readQueue(root).find((entry) => entry && entry.id && !seen.has(entry.id)) || null;
78
+ }
79
+
80
+ function parseCommaList(value) {
81
+ return String(value || '')
82
+ .split(',')
83
+ .map((part) => part.trim())
84
+ .filter(Boolean);
85
+ }
86
+
87
+ function parseQueueAddArgs(args) {
88
+ const options = {
89
+ id: null,
90
+ target: [],
91
+ apply: null,
92
+ verify: null,
93
+ note: '',
94
+ };
95
+
96
+ if (args[0] && !args[0].startsWith('--')) {
97
+ options.id = args.shift();
98
+ }
99
+
100
+ for (let i = 0; i < args.length; i++) {
101
+ const arg = args[i];
102
+ if (arg === '--target' && args[i + 1]) {
103
+ options.target = parseCommaList(args[++i]);
104
+ continue;
105
+ }
106
+ if (arg.startsWith('--target=')) {
107
+ options.target = parseCommaList(arg.slice('--target='.length));
108
+ continue;
109
+ }
110
+ if (arg === '--apply' && args[i + 1]) {
111
+ options.apply = args[++i];
112
+ continue;
113
+ }
114
+ if (arg.startsWith('--apply=')) {
115
+ options.apply = arg.slice('--apply='.length);
116
+ continue;
117
+ }
118
+ if (arg === '--verify' && args[i + 1]) {
119
+ options.verify = args[++i];
120
+ continue;
121
+ }
122
+ if (arg.startsWith('--verify=')) {
123
+ options.verify = arg.slice('--verify='.length);
124
+ continue;
125
+ }
126
+ if (arg === '--note' && args[i + 1]) {
127
+ options.note = args[++i];
128
+ continue;
129
+ }
130
+ if (arg.startsWith('--note=')) {
131
+ options.note = arg.slice('--note='.length);
132
+ continue;
133
+ }
134
+ }
135
+
136
+ return options;
137
+ }
138
+
139
+ function parseDailyArgs(args) {
140
+ return {
141
+ dryRun: args.includes('--dry-run'),
142
+ force: args.includes('--force'),
143
+ json: args.includes('--json'),
144
+ keepIfAllPass: args.includes('--keep-if') && args[args.indexOf('--keep-if') + 1] === 'all-pass',
145
+ };
146
+ }
147
+
148
+ function areTargetsClean(root, targetFiles) {
149
+ for (const rel of targetFiles) {
150
+ const result = spawnSync('git', ['status', '--porcelain', '--', rel], {
151
+ cwd: root,
152
+ encoding: 'utf8',
153
+ });
154
+ if (result.status !== 0) {
155
+ return { clean: false, reason: `git status failed for ${rel}` };
156
+ }
157
+ if (String(result.stdout || '').trim()) {
158
+ return { clean: false, reason: `target file not clean in git: ${rel}` };
159
+ }
160
+ }
161
+ return { clean: true };
162
+ }
163
+
164
+ function defaultBenchArgv(root, label, experimentId) {
165
+ const bin = path.join(root, 'bin', 'atris.js');
166
+ const cliBin = fs.existsSync(bin) ? bin : path.resolve(__dirname, '..', '..', 'bin', 'atris.js');
167
+ return {
168
+ cmd: process.execPath,
169
+ args: [cliBin, 'bench', 'run', '--label', label, '--experiment', experimentId, '--json'],
170
+ };
171
+ }
172
+
173
+ function runBench(root, { label, experimentId, env = process.env }) {
174
+ let cmd;
175
+ let args;
176
+ if (env.ATRIS_BENCH_CMD) {
177
+ cmd = env.ATRIS_BENCH_CMD;
178
+ args = ['--label', label, '--experiment', experimentId, '--json'];
179
+ } else {
180
+ ({ cmd, args } = defaultBenchArgv(root, label, experimentId));
181
+ }
182
+
183
+ const result = spawnSync(cmd, args, {
184
+ cwd: root,
185
+ encoding: 'utf8',
186
+ env: {
187
+ ...env,
188
+ ATRIS_SKIP_UPDATE_CHECK: '1',
189
+ },
190
+ });
191
+
192
+ let parsed = null;
193
+ const stdout = String(result.stdout || '').trim();
194
+ if (stdout) {
195
+ try {
196
+ parsed = JSON.parse(stdout);
197
+ } catch {
198
+ parsed = null;
199
+ }
200
+ }
201
+
202
+ const exitCode = typeof result.status === 'number' ? result.status : 2;
203
+ return { exitCode, parsed, stdout, stderr: String(result.stderr || '') };
204
+ }
205
+
206
+ function benchInfraError(exitCode) {
207
+ return exitCode === 2;
208
+ }
209
+
210
+ function parseBenchResult(parsed) {
211
+ if (!parsed || parsed.schema !== BENCH_SCHEMA) {
212
+ return {
213
+ passed: [],
214
+ failed: [],
215
+ skipped: [],
216
+ valid: false,
217
+ };
218
+ }
219
+ return {
220
+ passed: Array.isArray(parsed.passed) ? parsed.passed : [],
221
+ failed: Array.isArray(parsed.failed) ? parsed.failed : [],
222
+ skipped: Array.isArray(parsed.skipped) ? parsed.skipped : [],
223
+ valid: true,
224
+ };
225
+ }
226
+
227
+ function evaluateKeepRule({ baseline, candidate, verifyPassed, keepIfAllPass }) {
228
+ const baselinePassed = new Set(baseline.passed || []);
229
+ const candidatePassed = new Set(candidate.passed || []);
230
+ const candidateFailed = new Set(candidate.failed || []);
231
+ const skipped = new Set([...(baseline.skipped || []), ...(candidate.skipped || [])]);
232
+
233
+ if (verifyPassed === false) return { keep: false, reason: 'verify_failed' };
234
+
235
+ if (keepIfAllPass) {
236
+ const allTaskIds = new Set([
237
+ ...(baseline.passed || []),
238
+ ...(baseline.failed || []),
239
+ ...(candidate.passed || []),
240
+ ...(candidate.failed || []),
241
+ ]);
242
+ for (const id of allTaskIds) {
243
+ if (skipped.has(id)) continue;
244
+ if (!candidatePassed.has(id) || candidateFailed.has(id)) {
245
+ return { keep: false, reason: 'all_pass_required' };
246
+ }
247
+ }
248
+ return { keep: true, reason: 'all_pass' };
249
+ }
250
+
251
+ for (const id of baselinePassed) {
252
+ if (!candidatePassed.has(id)) {
253
+ return { keep: false, reason: 'baseline_regression' };
254
+ }
255
+ }
256
+ return { keep: true, reason: 'superset' };
257
+ }
258
+
259
+ function runApplyScript(root, applyPath) {
260
+ const scriptPath = path.isAbsolute(applyPath) ? applyPath : path.join(root, applyPath);
261
+ // .js apply scripts run on the node that runs the CLI: the sh-polyglot
262
+ // header needs `node` on PATH, which overnight cron environments lack.
263
+ const isJs = scriptPath.endsWith('.js') || scriptPath.endsWith('.cjs') || scriptPath.endsWith('.mjs');
264
+ return spawnSync(isJs ? process.execPath : 'sh', [scriptPath], {
265
+ cwd: root,
266
+ encoding: 'utf8',
267
+ env: process.env,
268
+ });
269
+ }
270
+
271
+ function runVerifyCommand(root, command) {
272
+ const { envWithNodeDir } = require('../spawn-env');
273
+ return spawnSync(command, {
274
+ cwd: root,
275
+ encoding: 'utf8',
276
+ shell: true,
277
+ env: envWithNodeDir(process.env),
278
+ });
279
+ }
280
+
281
+ function appendResultsTsv(root, row) {
282
+ const file = resultsPath(root);
283
+ fs.mkdirSync(path.dirname(file), { recursive: true });
284
+ const header = 'timestamp\texperiment_id\tdecision\tbaseline_passed\tcandidate_passed\tverify_passed\tnote\n';
285
+ if (!fs.existsSync(file) || fs.statSync(file).size === 0) {
286
+ fs.writeFileSync(file, header, 'utf8');
287
+ }
288
+ const line = [
289
+ row.timestamp,
290
+ row.experiment_id,
291
+ row.decision,
292
+ row.baseline_passed,
293
+ row.candidate_passed,
294
+ row.verify_passed,
295
+ row.note,
296
+ ].join('\t');
297
+ fs.appendFileSync(file, `${line}\n`, 'utf8');
298
+ }
299
+
300
+ function appendScorecardRow(root, row) {
301
+ const file = scorecardsPath(root);
302
+ fs.mkdirSync(path.dirname(file), { recursive: true });
303
+ fs.appendFileSync(file, `${JSON.stringify(row)}\n`, 'utf8');
304
+ return file;
305
+ }
306
+
307
+ function buildScorecardRow(payload) {
308
+ return {
309
+ schema: SCORECARD_SCHEMA,
310
+ ts: payload.ts || new Date().toISOString(),
311
+ experiment_id: payload.experiment_id,
312
+ decision: payload.decision,
313
+ baseline_passed: payload.baseline_passed || [],
314
+ candidate_passed: payload.candidate_passed || [],
315
+ baseline_failed: payload.baseline_failed || [],
316
+ candidate_failed: payload.candidate_failed || [],
317
+ verify_passed: payload.verify_passed,
318
+ keep_reason: payload.keep_reason || null,
319
+ note: payload.note || null,
320
+ };
321
+ }
322
+
323
+ function commitKeptTargets(root, experimentId, targetFiles) {
324
+ const add = spawnSync('git', ['add', '--', ...targetFiles], { cwd: root, encoding: 'utf8' });
325
+ if (add.status !== 0) {
326
+ return { ok: false, error: add.stderr || add.stdout || 'git add failed' };
327
+ }
328
+ const commit = spawnSync('git', ['commit', '-m', `experiment(${experimentId}): kept`], {
329
+ cwd: root,
330
+ encoding: 'utf8',
331
+ });
332
+ if (commit.status !== 0) {
333
+ return { ok: false, error: commit.stderr || commit.stdout || 'git commit failed' };
334
+ }
335
+ return { ok: true };
336
+ }
337
+
338
+ function recordOutcome(root, {
339
+ experimentId,
340
+ decision,
341
+ baseline,
342
+ candidate,
343
+ verifyPassed,
344
+ keepReason,
345
+ note,
346
+ verifyCommand,
347
+ writeLesson,
348
+ upsertLessonMeta,
349
+ lessonSlugFn,
350
+ }) {
351
+ const slug = lessonSlugFn(`experiment-${experimentId}`);
352
+ const explanation = decision === 'kept'
353
+ ? `Daily experiment ${experimentId} kept (${keepReason}).`
354
+ : `Daily experiment ${experimentId} reverted (${keepReason}).`;
355
+
356
+ appendResultsTsv(root, {
357
+ timestamp: new Date().toISOString(),
358
+ experiment_id: experimentId,
359
+ decision,
360
+ baseline_passed: (baseline.passed || []).join(','),
361
+ candidate_passed: (candidate.passed || []).join(','),
362
+ verify_passed: verifyPassed == null ? '' : String(verifyPassed),
363
+ note: note || '',
364
+ });
365
+
366
+ appendScorecardRow(root, buildScorecardRow({
367
+ experiment_id: experimentId,
368
+ decision,
369
+ baseline_passed: baseline.passed || [],
370
+ candidate_passed: candidate.passed || [],
371
+ baseline_failed: baseline.failed || [],
372
+ candidate_failed: candidate.failed || [],
373
+ verify_passed: verifyPassed,
374
+ keep_reason: keepReason,
375
+ note: note || null,
376
+ }));
377
+
378
+ writeLesson(root, slug, decision === 'kept' ? 'pass' : 'fail', explanation);
379
+ if (decision === 'kept' && verifyCommand) {
380
+ upsertLessonMeta(root, slug, {
381
+ status: 'pass',
382
+ detector: verifyCommand,
383
+ scope: 'atris-cli',
384
+ });
385
+ } else if (decision === 'reverted') {
386
+ upsertLessonMeta(root, slug, {
387
+ status: 'observed',
388
+ scope: 'atris-cli',
389
+ detector: null,
390
+ });
391
+ }
392
+ }
393
+
394
+ function runDaily(root, args = [], deps = {}) {
395
+ const options = parseDailyArgs(args);
396
+ const env = deps.env || process.env;
397
+ const writeLesson = deps.writeLesson || require('../../commands/autopilot').writeLesson;
398
+ const upsertLessonMeta = deps.upsertLessonMeta || require('../../commands/autopilot').upsertLessonMeta;
399
+ const lessonSlugFn = deps.lessonSlug || require('../../commands/autopilot').lessonSlug;
400
+
401
+ const respond = (payload, exitCode = 0) => {
402
+ if (options.json) console.log(JSON.stringify(payload));
403
+ else if (payload.message) console.log(payload.message);
404
+ return exitCode;
405
+ };
406
+
407
+ const state = readDailyState(root);
408
+ const today = todayDate();
409
+ if (!options.force && state.last_run_date === today) {
410
+ return respond({
411
+ ok: true,
412
+ skipped: true,
413
+ reason: 'already_ran_today',
414
+ last_run_date: state.last_run_date,
415
+ }, 0);
416
+ }
417
+
418
+ const entry = pickQueueEntry(root, state.history);
419
+ if (!entry) {
420
+ return respond({ ok: true, skipped: true, reason: 'queue_empty' }, 0);
421
+ }
422
+
423
+ const targetFiles = Array.isArray(entry.target_files) ? entry.target_files : [];
424
+ if (!entry.id || !entry.apply || targetFiles.length === 0) {
425
+ return respond({ ok: false, error: 'invalid_queue_entry', id: entry.id || null }, 1);
426
+ }
427
+
428
+ const clean = areTargetsClean(root, targetFiles);
429
+ if (!clean.clean) {
430
+ return respond({ ok: false, error: 'dirty_targets', reason: clean.reason, experiment_id: entry.id }, 1);
431
+ }
432
+
433
+ if (options.dryRun) {
434
+ return respond({
435
+ ok: true,
436
+ dry_run: true,
437
+ experiment_id: entry.id,
438
+ target_files: targetFiles,
439
+ apply: entry.apply,
440
+ verify: entry.verify || null,
441
+ }, 0);
442
+ }
443
+
444
+ const baselineRun = runBench(root, { label: 'baseline', experimentId: entry.id, env });
445
+ if (benchInfraError(baselineRun.exitCode)) {
446
+ return respond({
447
+ ok: false,
448
+ aborted: true,
449
+ stage: 'baseline',
450
+ experiment_id: entry.id,
451
+ error: 'bench_infra_error',
452
+ stderr: baselineRun.stderr.slice(0, 500),
453
+ }, 1);
454
+ }
455
+
456
+ const baseline = parseBenchResult(baselineRun.parsed);
457
+ const backups = new Map();
458
+ for (const rel of targetFiles) {
459
+ const abs = path.join(root, rel);
460
+ backups.set(rel, fs.readFileSync(abs));
461
+ }
462
+
463
+ const applyResult = runApplyScript(root, entry.apply);
464
+ if (applyResult.status !== 0) {
465
+ for (const [rel, bytes] of backups) {
466
+ fs.writeFileSync(path.join(root, rel), bytes);
467
+ }
468
+ const applyError = String(applyResult.stderr || applyResult.error || `exit ${applyResult.status}`)
469
+ .replace(/\s+/g, ' ').trim().slice(0, 200);
470
+ recordOutcome(root, {
471
+ experimentId: entry.id,
472
+ decision: 'reverted',
473
+ baseline,
474
+ candidate: { passed: [], failed: [], skipped: [] },
475
+ verifyPassed: null,
476
+ keepReason: 'apply_failed',
477
+ note: `${entry.note || ''} [apply error: ${applyError}]`.trim(),
478
+ verifyCommand: entry.verify || null,
479
+ writeLesson,
480
+ upsertLessonMeta,
481
+ lessonSlugFn,
482
+ });
483
+ state.last_run_date = today;
484
+ if (!state.history.includes(entry.id)) state.history.push(entry.id);
485
+ writeDailyState(root, state);
486
+ return respond({
487
+ ok: false,
488
+ experiment_id: entry.id,
489
+ decision: 'reverted',
490
+ reason: 'apply_failed',
491
+ }, 1);
492
+ }
493
+
494
+ const candidateRun = runBench(root, { label: 'candidate', experimentId: entry.id, env });
495
+ if (benchInfraError(candidateRun.exitCode)) {
496
+ for (const [rel, bytes] of backups) {
497
+ fs.writeFileSync(path.join(root, rel), bytes);
498
+ }
499
+ recordOutcome(root, {
500
+ experimentId: entry.id,
501
+ decision: 'reverted',
502
+ baseline,
503
+ candidate: { passed: [], failed: [], skipped: [] },
504
+ verifyPassed: null,
505
+ keepReason: 'candidate_bench_infra_error',
506
+ note: entry.note || '',
507
+ verifyCommand: entry.verify || null,
508
+ writeLesson,
509
+ upsertLessonMeta,
510
+ lessonSlugFn,
511
+ });
512
+ state.last_run_date = today;
513
+ if (!state.history.includes(entry.id)) state.history.push(entry.id);
514
+ writeDailyState(root, state);
515
+ return respond({
516
+ ok: false,
517
+ experiment_id: entry.id,
518
+ decision: 'reverted',
519
+ reason: 'candidate_bench_infra_error',
520
+ }, 1);
521
+ }
522
+
523
+ const candidate = parseBenchResult(candidateRun.parsed);
524
+ let verifyPassed = null;
525
+ if (entry.verify) {
526
+ const verifyResult = runVerifyCommand(root, entry.verify);
527
+ verifyPassed = verifyResult.status === 0;
528
+ }
529
+
530
+ const keepEval = evaluateKeepRule({
531
+ baseline,
532
+ candidate,
533
+ verifyPassed,
534
+ keepIfAllPass: options.keepIfAllPass,
535
+ });
536
+
537
+ let decision = keepEval.keep ? 'kept' : 'reverted';
538
+ if (decision === 'kept') {
539
+ const commit = commitKeptTargets(root, entry.id, targetFiles);
540
+ if (!commit.ok) {
541
+ for (const [rel, bytes] of backups) {
542
+ fs.writeFileSync(path.join(root, rel), bytes);
543
+ }
544
+ decision = 'reverted';
545
+ keepEval.reason = 'commit_failed';
546
+ }
547
+ }
548
+
549
+ if (decision === 'reverted') {
550
+ for (const [rel, bytes] of backups) {
551
+ fs.writeFileSync(path.join(root, rel), bytes);
552
+ }
553
+ }
554
+
555
+ recordOutcome(root, {
556
+ experimentId: entry.id,
557
+ decision,
558
+ baseline,
559
+ candidate,
560
+ verifyPassed,
561
+ keepReason: keepEval.reason,
562
+ note: entry.note || '',
563
+ verifyCommand: entry.verify || null,
564
+ writeLesson,
565
+ upsertLessonMeta,
566
+ lessonSlugFn,
567
+ });
568
+
569
+ state.last_run_date = today;
570
+ if (!state.history.includes(entry.id)) state.history.push(entry.id);
571
+ writeDailyState(root, state);
572
+
573
+ return respond({
574
+ ok: decision === 'kept',
575
+ experiment_id: entry.id,
576
+ decision,
577
+ keep_reason: keepEval.reason,
578
+ baseline_passed: baseline.passed,
579
+ candidate_passed: candidate.passed,
580
+ verify_passed: verifyPassed,
581
+ }, decision === 'kept' ? 0 : 1);
582
+ }
583
+
584
+ function queueAdd(root, args = []) {
585
+ const parsed = parseQueueAddArgs(args);
586
+ if (!parsed.id || parsed.target.length === 0 || !parsed.apply) {
587
+ console.error('Usage: atris experiments queue add <id> --target <file>[,<file>] --apply <script> [--verify "<cmd>"] [--note "..."]');
588
+ return 1;
589
+ }
590
+
591
+ appendQueueEntry(root, {
592
+ id: parsed.id,
593
+ kind: 'patch',
594
+ target_files: parsed.target,
595
+ apply: parsed.apply,
596
+ verify: parsed.verify || null,
597
+ note: parsed.note || '',
598
+ added: new Date().toISOString(),
599
+ });
600
+ console.log(`✓ Queued experiment ${parsed.id}`);
601
+ return 0;
602
+ }
603
+
604
+ function queueList(root, args = []) {
605
+ const json = args.includes('--json');
606
+ const entries = readQueue(root);
607
+ if (json) {
608
+ console.log(JSON.stringify({ entries }, null, 2));
609
+ } else if (entries.length === 0) {
610
+ console.log('Queue is empty.');
611
+ } else {
612
+ for (const entry of entries) {
613
+ console.log(`${entry.id}\t${(entry.target_files || []).join(',')}\t${entry.apply}`);
614
+ }
615
+ }
616
+ return 0;
617
+ }
618
+
619
+ module.exports = {
620
+ SCORECARD_SCHEMA,
621
+ BENCH_SCHEMA,
622
+ appendQueueEntry,
623
+ appendResultsTsv,
624
+ appendScorecardRow,
625
+ areTargetsClean,
626
+ buildScorecardRow,
627
+ evaluateKeepRule,
628
+ parseBenchResult,
629
+ parseDailyArgs,
630
+ parseQueueAddArgs,
631
+ pickQueueEntry,
632
+ queueAdd,
633
+ queueList,
634
+ readDailyState,
635
+ readQueue,
636
+ runApplyScript,
637
+ runBench,
638
+ runDaily,
639
+ writeDailyState,
640
+ };