atris 3.57.4 → 3.58.6

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 (175) hide show
  1. package/README.md +12 -1
  2. package/atris/skills/clawhub/member-runtime/SKILL.md +9 -9
  3. package/atris/skills/create-member/SKILL.md +16 -13
  4. package/atris/skills/design/SKILL.md +27 -4
  5. package/atris/skills/engines/SKILL.md +55 -12
  6. package/atris/skills/x-search/SKILL.md +8 -6
  7. package/atris/skills/youtube/SKILL.md +66 -21
  8. package/atris/team/brainstormer/MEMBER.md +2 -2
  9. package/atris/team/executor/MEMBER.md +3 -3
  10. package/atris/team/launcher/MEMBER.md +4 -4
  11. package/atris/team/navigator/MEMBER.md +3 -3
  12. package/atris/team/researcher/MEMBER.md +2 -2
  13. package/atris/team/validator/MEMBER.md +1 -1
  14. package/atris.md +14 -0
  15. package/ax +153 -23
  16. package/bin/atris.js +78 -30
  17. package/commands/aeo.js +4 -4
  18. package/commands/agents.js +1 -1
  19. package/commands/align.js +12 -12
  20. package/commands/analytics.js +3 -3
  21. package/commands/app.js +5 -5
  22. package/commands/ask.js +175 -0
  23. package/commands/auth.js +59 -31
  24. package/commands/autoland.js +29 -5
  25. package/commands/autopilot-front.js +6 -6
  26. package/commands/autopilot.js +90 -90
  27. package/commands/avail.js +5 -5
  28. package/commands/bench.js +63 -1
  29. package/commands/browse.js +2 -2
  30. package/commands/business.js +12 -12
  31. package/commands/card.js +2 -2
  32. package/commands/clean.js +13 -13
  33. package/commands/close.js +3 -3
  34. package/commands/compile.js +13 -13
  35. package/commands/computer.js +94 -27
  36. package/commands/console.js +5 -5
  37. package/commands/decide.js +1 -1
  38. package/commands/deck.js +9 -9
  39. package/commands/drive.js +4 -4
  40. package/commands/engine.js +173 -19
  41. package/commands/errors.js +3 -3
  42. package/commands/experiments.js +18 -1
  43. package/commands/feed.js +2 -2
  44. package/commands/feedback.js +4 -4
  45. package/commands/fleet.js +12 -12
  46. package/commands/game.js +1 -1
  47. package/commands/guide.js +49 -0
  48. package/commands/improve.js +19 -17
  49. package/commands/init.js +67 -19
  50. package/commands/interview.js +7 -7
  51. package/commands/land.js +40 -15
  52. package/commands/learn.js +216 -50
  53. package/commands/lesson.js +2 -2
  54. package/commands/log.js +1 -1
  55. package/commands/loops.js +4 -4
  56. package/commands/member.js +75 -50
  57. package/commands/mission.js +90 -82
  58. package/commands/one-lap.js +1 -1
  59. package/commands/pack.js +74 -8
  60. package/commands/plugin.js +6 -6
  61. package/commands/probe.js +9 -9
  62. package/commands/pull.js +35 -35
  63. package/commands/pulse.js +1 -1
  64. package/commands/push.js +21 -21
  65. package/commands/rainmaker.js +1 -1
  66. package/commands/recap.js +7 -7
  67. package/commands/reel.js +2 -2
  68. package/commands/release.js +3 -3
  69. package/commands/report.js +1 -1
  70. package/commands/review.js +5 -5
  71. package/commands/revisions.js +1 -1
  72. package/commands/run.js +1 -1
  73. package/commands/scout.js +3 -3
  74. package/commands/security-review.js +7 -7
  75. package/commands/serve.js +9 -9
  76. package/commands/setup.js +1 -1
  77. package/commands/sign.js +4 -4
  78. package/commands/signup.js +4 -4
  79. package/commands/site.js +8 -3
  80. package/commands/skill.js +6 -6
  81. package/commands/slop.js +18 -18
  82. package/commands/social.js +3 -3
  83. package/commands/soul.js +14 -14
  84. package/commands/spaceship.js +1 -1
  85. package/commands/status.js +2 -2
  86. package/commands/strings.js +13 -13
  87. package/commands/study.js +4 -4
  88. package/commands/sync.js +8 -8
  89. package/commands/task.js +50 -25
  90. package/commands/teach.js +2 -2
  91. package/commands/team.js +1 -1
  92. package/commands/theme.js +2 -2
  93. package/commands/tree.js +34 -0
  94. package/commands/truth.js +7 -7
  95. package/commands/verify.js +12 -12
  96. package/commands/visualize.js +2 -2
  97. package/commands/workflow.js +101 -48
  98. package/commands/worktree.js +1 -1
  99. package/commands/write.js +9 -9
  100. package/commands/x-search.js +93 -22
  101. package/commands/xp.js +6 -3
  102. package/commands/youtube.js +1019 -123
  103. package/lib/accept-verify-gate.js +3 -3
  104. package/lib/activity-stream.js +1 -1
  105. package/lib/auto-accept-certified.js +28 -12
  106. package/lib/autoland.js +6 -6
  107. package/lib/bench/engines.js +59 -8
  108. package/lib/bench/report.js +1 -1
  109. package/lib/bench/runner.js +204 -5
  110. package/lib/bench/tree-render.js +76 -0
  111. package/lib/card.js +1 -1
  112. package/lib/chat-log-scan.js +1 -1
  113. package/lib/claude-boot-block.js +2 -0
  114. package/lib/deck-compose.js +1 -1
  115. package/lib/deck-history.js +2 -2
  116. package/lib/deck-layout.js +1 -1
  117. package/lib/deck-review.js +5 -5
  118. package/lib/deck-schema.js +1 -1
  119. package/lib/default-verifier.js +1 -1
  120. package/lib/developer-api.js +3 -0
  121. package/lib/engine-ask.js +45 -7
  122. package/lib/engine-registry.js +15 -2
  123. package/lib/file-ops.js +1 -1
  124. package/lib/fleet.js +71 -35
  125. package/lib/html-render.js +1 -1
  126. package/lib/intents.js +172 -0
  127. package/lib/journal.js +1 -1
  128. package/lib/known-commands.js +2 -2
  129. package/lib/land-green.js +364 -0
  130. package/lib/lesson-contradiction.js +2 -2
  131. package/lib/lesson-preflight.js +1 -1
  132. package/lib/manifest.js +5 -5
  133. package/lib/member-alive.js +9 -5
  134. package/lib/memory-view.js +3 -3
  135. package/lib/mission-protected-lane.js +1 -1
  136. package/lib/mission-root.js +2 -2
  137. package/lib/orb-context.js +10 -10
  138. package/lib/pack-capabilities.js +861 -3
  139. package/lib/permission-grants.js +2 -2
  140. package/lib/policy-lessons.js +6 -6
  141. package/lib/pulse.js +8 -8
  142. package/lib/receipt-evidence.js +1 -1
  143. package/lib/reel.js +1 -1
  144. package/lib/review-integrity.js +1 -1
  145. package/lib/runner-command.js +35 -5
  146. package/lib/scorecard.js +4 -4
  147. package/lib/security-scan.js +5 -5
  148. package/lib/self-drive.js +1 -1
  149. package/lib/site-publish.js +372 -0
  150. package/lib/site.js +1 -1
  151. package/lib/slides-deck.js +2 -2
  152. package/lib/state-detection.js +2 -2
  153. package/lib/sync-telemetry.js +2 -2
  154. package/lib/task-db.js +17 -10
  155. package/lib/task-explanation.js +1 -1
  156. package/lib/task-proof.js +2 -2
  157. package/lib/task-receipt.js +3 -3
  158. package/lib/theme.js +12 -12
  159. package/lib/todo-fallback.js +3 -3
  160. package/lib/todo-sections.js +1 -1
  161. package/lib/tree-hash.js +79 -0
  162. package/lib/usage.js +1 -1
  163. package/lib/wish-delegate.js +1 -1
  164. package/lib/workspace-safety.js +4 -4
  165. package/lib/workspace-scaffold.js +3 -3
  166. package/package.json +1 -1
  167. package/scripts/det/concierge-bakeoff.sh +26 -0
  168. package/scripts/det/ytnotes +82 -5
  169. package/scripts/det/ytquote-repair.js +0 -1
  170. package/scripts/det/ytrail-race.js +0 -5
  171. package/scripts/member-operate.mjs +52 -14
  172. package/scripts/outbound-artifact-gate.js +0 -3
  173. package/utils/auth.js +47 -9
  174. package/utils/config.js +0 -1
  175. package/scripts/det/checklist-score.js +0 -191
@@ -3,8 +3,10 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const { spawnSync } = require('node:child_process');
6
+ const { treeHashFor } = require('../tree-hash');
6
7
  const { createBenchContext, withBenchContext } = require('./context');
7
8
  const { ENGINE_NAMES, getEngineAdapter, normalizeEngineName } = require('./engines');
9
+ const { renderTreeInto } = require('./tree-render');
8
10
 
9
11
  const DEFAULT_PACK = 'core-v1';
10
12
 
@@ -208,6 +210,23 @@ function copyFixtureIntoWorkspace(fixtureDir, workspace) {
208
210
  });
209
211
  }
210
212
 
213
+ function shellQuote(value) {
214
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
215
+ }
216
+
217
+ function installAtrisShim(ctx) {
218
+ const shimDir = path.join(ctx.env.HOME, 'bin');
219
+ const shimPath = path.join(shimDir, 'atris');
220
+ fs.mkdirSync(shimDir, { recursive: true });
221
+ fs.writeFileSync(
222
+ shimPath,
223
+ `#!/bin/sh\nexec ${shellQuote(process.execPath)} ${shellQuote(ctx.cliPath)} "$@"\n`,
224
+ { encoding: 'utf8', mode: 0o755 },
225
+ );
226
+ ctx.env.PATH = [shimDir, ctx.env.PATH].filter(Boolean).join(path.delimiter);
227
+ return shimPath;
228
+ }
229
+
211
230
  async function runSetup(spec, ctx) {
212
231
  if (!spec.setupPath) return;
213
232
  delete require.cache[require.resolve(spec.setupPath)];
@@ -246,6 +265,7 @@ async function runAgentTaskSpec(spec, options = {}) {
246
265
  const started = Date.now();
247
266
  const timeoutMs = Number(spec.timeoutMs || options.timeoutMs || 300000);
248
267
  let bench = null;
268
+ let reachedCheck = false;
249
269
  try {
250
270
  bench = createBenchContext({
251
271
  repoRoot: options.repoRoot,
@@ -257,9 +277,15 @@ async function runAgentTaskSpec(spec, options = {}) {
257
277
  ctx.fixtureDir = spec.fixtureDir;
258
278
  ctx.promptPath = spec.promptPath;
259
279
  copyFixtureIntoWorkspace(spec.fixtureDir, ctx.workspace);
280
+ installAtrisShim(ctx);
281
+ if (options.treeRoot) {
282
+ const rendered = renderTreeInto(ctx.workspace, options.treeRoot);
283
+ if (typeof options.onTreeRendered === 'function') options.onTreeRendered(rendered);
284
+ }
260
285
 
261
- const engine = getEngineAdapter(options.engine, { solutionPath: spec.solutionPath });
262
- const availability = engine.available(ctx.workspace);
286
+ const engine = options.engineAdapter
287
+ || getEngineAdapter(options.engine, { solutionPath: spec.solutionPath, model: options.model });
288
+ const availability = engine.available(ctx.workspace, ctx.env);
263
289
  if (!availability.available) {
264
290
  return {
265
291
  id: spec.id,
@@ -268,15 +294,27 @@ async function runAgentTaskSpec(spec, options = {}) {
268
294
  failures: [],
269
295
  duration_ms: Date.now() - started,
270
296
  retried: false,
297
+ ...(options.classifyCouldNotRun ? { could_not_run: true } : {}),
271
298
  };
272
299
  }
273
300
 
274
301
  await runSetup(spec, ctx);
275
302
  const promptText = readTaskPrompt(spec);
276
- const engineResult = await withTimeout(Promise.resolve(engine.run(promptText, ctx.workspace, timeoutMs)), spec, timeoutMs);
303
+ const engineResult = await withTimeout(
304
+ Promise.resolve(engine.run(promptText, ctx.workspace, timeoutMs, ctx.env)),
305
+ spec,
306
+ timeoutMs,
307
+ );
308
+ ctx.engineResult = {
309
+ status: engineResult && engineResult.status,
310
+ stdout: String(engineResult && engineResult.stdout || ''),
311
+ stderr: String(engineResult && engineResult.stderr || ''),
312
+ timedOut: Boolean(engineResult && engineResult.timedOut),
313
+ };
277
314
  if (!engineResult || engineResult.status !== 0) {
278
315
  throw new Error(formatEngineFailure(options.engine, engineResult));
279
316
  }
317
+ reachedCheck = true;
280
318
  await withTimeout(Promise.resolve().then(() => spec.check(ctx)), spec, timeoutMs);
281
319
  return {
282
320
  id: spec.id,
@@ -285,6 +323,7 @@ async function runAgentTaskSpec(spec, options = {}) {
285
323
  failures: [],
286
324
  duration_ms: Date.now() - started,
287
325
  retried: false,
326
+ ...(options.classifyCouldNotRun ? { could_not_run: false } : {}),
288
327
  };
289
328
  } catch (err) {
290
329
  return {
@@ -294,6 +333,7 @@ async function runAgentTaskSpec(spec, options = {}) {
294
333
  failures: [formatFailure(err)],
295
334
  duration_ms: Date.now() - started,
296
335
  retried: false,
336
+ ...(options.classifyCouldNotRun ? { could_not_run: !reachedCheck } : {}),
297
337
  };
298
338
  } finally {
299
339
  if (bench) bench.teardown();
@@ -314,6 +354,7 @@ async function runTaskSpec(spec, options = {}) {
314
354
  failures: [],
315
355
  duration_ms: Date.now() - started,
316
356
  retried: false,
357
+ ...(options.classifyCouldNotRun ? { could_not_run: true } : {}),
317
358
  };
318
359
  }
319
360
 
@@ -328,6 +369,7 @@ async function runTaskSpec(spec, options = {}) {
328
369
  failures: [],
329
370
  duration_ms: Date.now() - started,
330
371
  retried,
372
+ ...(options.classifyCouldNotRun ? { could_not_run: true } : {}),
331
373
  };
332
374
  }
333
375
  return {
@@ -337,6 +379,7 @@ async function runTaskSpec(spec, options = {}) {
337
379
  failures: [],
338
380
  duration_ms: Date.now() - started,
339
381
  retried,
382
+ ...(options.classifyCouldNotRun ? { could_not_run: false } : {}),
340
383
  };
341
384
  } catch (err) {
342
385
  const infra = isInfraFailure(err);
@@ -351,6 +394,7 @@ async function runTaskSpec(spec, options = {}) {
351
394
  failures: [formatFailure(err)],
352
395
  duration_ms: Date.now() - started,
353
396
  retried,
397
+ ...(options.classifyCouldNotRun ? { could_not_run: infra } : {}),
354
398
  };
355
399
  }
356
400
  }
@@ -362,6 +406,7 @@ async function runTaskSpec(spec, options = {}) {
362
406
  failures: ['unreachable benchmark runner state'],
363
407
  duration_ms: Date.now() - started,
364
408
  retried,
409
+ ...(options.classifyCouldNotRun ? { could_not_run: true } : {}),
365
410
  };
366
411
  }
367
412
 
@@ -449,6 +494,7 @@ function exitCodeForRecord(record) {
449
494
 
450
495
  async function runBench(options = {}) {
451
496
  const repoRoot = path.resolve(options.repoRoot || repoRootFromHere());
497
+ const workspaceRoot = path.resolve(options.stateRoot || process.cwd());
452
498
  const pack = options.pack || (options.tasksDir ? path.basename(path.resolve(options.tasksDir)) : DEFAULT_PACK);
453
499
  const specs = selectTaskSpecs(loadTaskSpecs({ repoRoot, tasksDir: options.tasksDir, pack }), options.taskIds);
454
500
  const engine = normalizeBenchEngine(options.engine);
@@ -458,18 +504,27 @@ async function runBench(options = {}) {
458
504
  const label = normalizeLabel(options.label);
459
505
  const started = new Date().toISOString();
460
506
  const pythonCmd = options.pythonCmd === undefined ? findPython() : options.pythonCmd;
507
+ let renderedTreeHash = null;
461
508
  const taskRecords = await runTaskSpecs(specs, {
462
509
  repoRoot,
463
510
  timeoutMs: options.timeoutMs,
464
511
  pythonCmd,
465
512
  engine,
513
+ engineAdapter: options.engineAdapter,
514
+ model: options.model || null,
515
+ treeRoot: options.treeRoot,
516
+ onTreeRendered(rendered) {
517
+ renderedTreeHash = rendered.tree_hash;
518
+ },
466
519
  });
467
520
  const finished = new Date().toISOString();
468
521
  const summary = summarizeTasks(taskRecords);
469
522
  const record = {
470
523
  schema: 'atris.bench.run.v1',
524
+ tree_hash: options.treeRoot ? (renderedTreeHash || treeHashFor(options.treeRoot)) : treeHashFor(workspaceRoot),
471
525
  pack,
472
526
  engine,
527
+ model: engine ? (options.model || null) : null,
473
528
  label,
474
529
  experiment: options.experiment || null,
475
530
  started,
@@ -481,11 +536,154 @@ async function runBench(options = {}) {
481
536
  summary: summary.summary,
482
537
  };
483
538
 
484
- if (options.persist !== false) appendResultRecord(record, options.stateRoot || process.cwd());
485
- if (options.updateBaseline) writeBaselineRecord(record, options.stateRoot || process.cwd());
539
+ if (options.persist !== false) appendResultRecord(record, workspaceRoot);
540
+ if (options.updateBaseline) writeBaselineRecord(record, workspaceRoot);
486
541
  return { record, exitCode: exitCodeForRecord(record) };
487
542
  }
488
543
 
544
+ function selectTaskSpecsInPackOrder(specs, taskIds = []) {
545
+ const ids = (taskIds || []).filter(Boolean);
546
+ if (!ids.length) return specs;
547
+ const requested = new Set(ids);
548
+ const known = new Set(specs.map((spec) => spec.id));
549
+ for (const id of requested) {
550
+ if (!known.has(id)) throw new BenchInfraError(`unknown benchmark task: ${id}`);
551
+ }
552
+ return specs.filter((spec) => requested.has(spec.id));
553
+ }
554
+
555
+ function positiveInteger(value, fallback, name) {
556
+ const normalized = value === undefined || value === null || value === '' ? fallback : Number(value);
557
+ if (!Number.isInteger(normalized) || normalized < 1) {
558
+ throw new BenchInfraError(`${name} must be a positive integer`);
559
+ }
560
+ return normalized;
561
+ }
562
+
563
+ function nonNegativeInteger(value, fallback, name) {
564
+ const normalized = value === undefined || value === null || value === '' ? fallback : Number(value);
565
+ if (!Number.isInteger(normalized) || normalized < 0) {
566
+ throw new BenchInfraError(`${name} must be a non-negative integer`);
567
+ }
568
+ return normalized;
569
+ }
570
+
571
+ function requireTreeDirectory(value, name) {
572
+ if (!value) throw new BenchInfraError(`${name} is required`);
573
+ const root = path.resolve(value);
574
+ try {
575
+ if (fs.statSync(root).isDirectory()) return root;
576
+ } catch {
577
+ // Fall through to the plain benchmark error.
578
+ }
579
+ throw new BenchInfraError(`${name} is not a directory: ${value}`);
580
+ }
581
+
582
+ function pairingScore(task) {
583
+ if (task.skipped || task.could_not_run) return null;
584
+ return task.passed ? 1 : 0;
585
+ }
586
+
587
+ function pairingOutcome(current, candidate) {
588
+ if (current === candidate) return 'tie';
589
+ if (candidate === null) return 'loss';
590
+ if (current === null) return 'win';
591
+ return candidate > current ? 'win' : 'loss';
592
+ }
593
+
594
+ function pairReason(wins, losses, verdict) {
595
+ if (wins === 0 && losses === 0) return 'all pairings tied';
596
+ if (verdict === 'select') {
597
+ return `candidate won ${wins} ${wins === 1 ? 'pairing' : 'pairings'} and lost ${losses}`;
598
+ }
599
+ return `candidate did not exceed its ${losses} ${losses === 1 ? 'loss' : 'losses'} with ${wins} ${wins === 1 ? 'win' : 'wins'}`;
600
+ }
601
+
602
+ async function runBenchPair(options = {}) {
603
+ const repoRoot = path.resolve(options.repoRoot || repoRootFromHere());
604
+ const workspaceRoot = path.resolve(options.stateRoot || process.cwd());
605
+ const currentRoot = requireTreeDirectory(options.currentRoot || workspaceRoot, 'current tree');
606
+ const candidateRoot = requireTreeDirectory(options.candidateRoot, 'candidate tree');
607
+ const pack = options.pack || (options.tasksDir ? path.basename(path.resolve(options.tasksDir)) : DEFAULT_PACK);
608
+ const specs = selectTaskSpecsInPackOrder(
609
+ loadTaskSpecs({ repoRoot, tasksDir: options.tasksDir, pack }),
610
+ options.taskIds,
611
+ );
612
+ const engine = normalizeBenchEngine(options.engine);
613
+ if (specs.some((spec) => spec.kind === 'agent') && !engine) {
614
+ throw new BenchInfraError(`agent benchmark pack ${pack} requires --engine <${ENGINE_NAMES.join('|')}>`);
615
+ }
616
+ const repeats = positiveInteger(options.repeats, 1, 'repeats');
617
+ const minWinMargin = nonNegativeInteger(options.minWinMargin, 0, 'min win margin');
618
+ const pythonCmd = options.pythonCmd === undefined ? findPython() : options.pythonCmd;
619
+ const started = new Date().toISOString();
620
+ const tasks = [];
621
+ const currentScores = [];
622
+ const candidateScores = [];
623
+ let wins = 0;
624
+ let losses = 0;
625
+ let ties = 0;
626
+
627
+ for (const spec of specs) {
628
+ for (let repeat = 0; repeat < repeats; repeat += 1) {
629
+ const shared = {
630
+ repoRoot,
631
+ timeoutMs: options.timeoutMs,
632
+ pythonCmd,
633
+ engine,
634
+ engineAdapter: options.engineAdapter,
635
+ model: options.model || null,
636
+ classifyCouldNotRun: true,
637
+ };
638
+ const currentTask = await runTaskSpec(spec, { ...shared, treeRoot: currentRoot });
639
+ const candidateTask = await runTaskSpec(spec, { ...shared, treeRoot: candidateRoot });
640
+ const current = pairingScore(currentTask);
641
+ const candidate = pairingScore(candidateTask);
642
+ const outcome = pairingOutcome(current, candidate);
643
+ if (outcome === 'win') wins += 1;
644
+ else if (outcome === 'loss') losses += 1;
645
+ else ties += 1;
646
+ currentScores.push(current);
647
+ candidateScores.push(candidate);
648
+ tasks.push({
649
+ id: spec.id,
650
+ current,
651
+ candidate,
652
+ outcome,
653
+ current_duration_ms: currentTask.duration_ms,
654
+ candidate_duration_ms: candidateTask.duration_ms,
655
+ current_failures: currentTask.failures,
656
+ candidate_failures: candidateTask.failures,
657
+ });
658
+ }
659
+ }
660
+
661
+ const verdict = wins - losses > minWinMargin ? 'select' : 'reject';
662
+ const record = {
663
+ schema: 'atris.bench.pair.v1',
664
+ pack,
665
+ engine,
666
+ model: engine ? (options.model || null) : null,
667
+ started,
668
+ finished: new Date().toISOString(),
669
+ current_tree_hash: treeHashFor(currentRoot),
670
+ candidate_tree_hash: treeHashFor(candidateRoot),
671
+ tasks,
672
+ current_scores: currentScores,
673
+ candidate_scores: candidateScores,
674
+ wins,
675
+ losses,
676
+ ties,
677
+ min_win_margin: minWinMargin,
678
+ repeats,
679
+ verdict,
680
+ reason: pairReason(wins, losses, verdict),
681
+ };
682
+ if (options.persist !== false) appendResultRecord(record, workspaceRoot);
683
+ const allTied = wins === 0 && losses === 0;
684
+ return { record, exitCode: allTied ? 2 : verdict === 'select' ? 0 : 1 };
685
+ }
686
+
489
687
  function taskMetadata(options = {}) {
490
688
  return loadTaskSpecs(options).map((spec) => ({
491
689
  id: spec.id,
@@ -514,6 +712,7 @@ module.exports = {
514
712
  readResultRecords,
515
713
  readTaskPrompt,
516
714
  runBench,
715
+ runBenchPair,
517
716
  summarizeTasks,
518
717
  taskMetadata,
519
718
  };
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
+ const { computeTreeHash } = require('../tree-hash');
8
+
9
+ const POINTER_BODY = [
10
+ 'Read atris.md at the workspace root first and follow it.',
11
+ 'Skills live under atris/skills, team briefs under atris/team.',
12
+ ];
13
+
14
+ function writePointerIfAbsent(workspaceDir, filename) {
15
+ const destination = path.join(workspaceDir, filename);
16
+ if (fs.existsSync(destination)) return;
17
+ fs.writeFileSync(destination, [`# ${filename}`, ...POINTER_BODY, ''].join('\n'), 'utf8');
18
+ }
19
+
20
+ function renderTreeInto(workspaceDir, treeRoot) {
21
+ const workspace = path.resolve(workspaceDir);
22
+ const root = path.resolve(treeRoot);
23
+ const tree = computeTreeHash(root);
24
+ fs.mkdirSync(workspace, { recursive: true });
25
+
26
+ for (const entry of tree.manifest) {
27
+ const source = path.join(root, ...entry.path.split('/'));
28
+ const destination = path.join(workspace, ...entry.path.split('/'));
29
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
30
+ fs.copyFileSync(source, destination);
31
+ }
32
+
33
+ writePointerIfAbsent(workspace, 'CLAUDE.md');
34
+ writePointerIfAbsent(workspace, 'AGENTS.md');
35
+ return { tree_hash: tree.hash, files: tree.files };
36
+ }
37
+
38
+ function candidateError(source) {
39
+ return new Error(`candidate source not found: ${source}`);
40
+ }
41
+
42
+ function materializeCandidate(source, repoRoot) {
43
+ const candidate = String(source || '').trim();
44
+ const directPath = path.resolve(candidate || '.');
45
+ try {
46
+ if (candidate && fs.statSync(directPath).isDirectory()) return directPath;
47
+ } catch {
48
+ // A non-directory source may still name a git ref.
49
+ }
50
+
51
+ if (!candidate || candidate.startsWith('-')) throw candidateError(source);
52
+ const root = path.resolve(repoRoot || process.cwd());
53
+ const archive = spawnSync('git', ['archive', '--format=tar', candidate], {
54
+ cwd: root,
55
+ maxBuffer: 128 * 1024 * 1024,
56
+ });
57
+ if (archive.error || archive.status !== 0) throw candidateError(source);
58
+
59
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-bench-candidate-'));
60
+ const extract = spawnSync('tar', ['-x', '-C', tempDir], {
61
+ cwd: root,
62
+ input: archive.stdout,
63
+ encoding: 'utf8',
64
+ maxBuffer: 128 * 1024 * 1024,
65
+ });
66
+ if (extract.error || extract.status !== 0) {
67
+ fs.rmSync(tempDir, { recursive: true, force: true });
68
+ throw candidateError(source);
69
+ }
70
+ return tempDir;
71
+ }
72
+
73
+ module.exports = {
74
+ materializeCandidate,
75
+ renderTreeInto,
76
+ };
package/lib/card.js CHANGED
@@ -1,4 +1,4 @@
1
- // atris card turn one line of text into a beautiful, on-brand image.
1
+ // atris card, turn one line of text into a beautiful, on-brand image.
2
2
  // Pure: spec -> self-contained HTML string. Reuses the design system: the same
3
3
  // themes as deck/site/html (incl. your .atris/theme.json brand), one accent,
4
4
  // Fraunces display, restraint. Render to PNG with headless Chrome (commands/card.js).
@@ -176,7 +176,7 @@ function parseAxLog(filePath, text) {
176
176
  if (prompt) userTurns.push(compact(prompt[1], 240));
177
177
  if (/^●\s/.test(line)) toolCalls += 1;
178
178
  if (/·\s*Interrupted/i.test(line)) interrupted = true;
179
- const worked = line.match(/^— Worked for ([^-]+?) —/);
179
+ const worked = line.match(/^\u2014 Worked for ([^-]+?) \u2014/);
180
180
  if (worked) lastWorkedSec = worked[1];
181
181
  if (ERROR_LINE.test(line) || /^x\s/.test(line.trim())) {
182
182
  findings.push(compact(line, 180));
@@ -12,6 +12,8 @@ You are now running inside Atris, an AI workspace operating system.
12
12
  **ON EVERY CONVERSATION START:** If a SessionStart hook already displayed the Atris status block in this session, do not run it again. Otherwise, before your first response, execute \`atris atris.md\` and display the full output. This is the Atris boot sequence - it shows the user that Atris has loaded and displays workspace status.
13
13
 
14
14
  After displaying the boot output, respond to the user naturally.
15
+
16
+ The person does not know atris words. Translate their plain English; when unsure run \`atris guide "<their words>"\`.
15
17
  ${ATRIS_END_MARKER}
16
18
  `;
17
19
  }
@@ -50,7 +50,7 @@ function toItem(line) {
50
50
 
51
51
  // A quote may carry an attribution: `"..." - Name` or `... (Name)`.
52
52
  function splitQuote(text) {
53
- const dash = text.match(/^["“]?(.+?)["”]?\s+[-–—]\s+(.+)$/);
53
+ const dash = text.match(/^["“]?(.+?)["”]?\s+[-–\u2014]\s+(.+)$/);
54
54
  if (dash) return { text: dash[1].trim(), author: dash[2].trim() };
55
55
  const paren = text.match(/^["“]?(.+?)["”]?\s+\(([^)]+)\)\s*$/);
56
56
  if (paren) return { text: paren[1].trim(), author: paren[2].trim() };
@@ -1,4 +1,4 @@
1
- // Deck build history every build appends one line to a JSONL ledger so an
1
+ // Deck build history: every build appends one line to a JSONL ledger so an
2
2
  // operator can answer "which presentation did this spec become, and when?".
3
3
  // Rebuilds reuse the same presentation id under --update; this ledger is how
4
4
  // you track iterations of a spec over time.
@@ -10,7 +10,7 @@ const crypto = require('crypto');
10
10
 
11
11
  const DEFAULT_HISTORY_PATH = path.join(os.homedir(), '.atris', 'deck-history.jsonl');
12
12
 
13
- // Short, stable content hash of a spec same spec -> same hash, so you can
13
+ // Short, stable content hash of a spec, same spec -> same hash, so you can
14
14
  // tell a no-op rebuild from a real change.
15
15
  function specHash(spec) {
16
16
  return crypto.createHash('sha256').update(JSON.stringify(spec || {})).digest('hex').slice(0, 12);
@@ -1,4 +1,4 @@
1
- // Layout planner map content shapes to slide archetypes deterministically.
1
+ // Layout planner, map content shapes to slide archetypes deterministically.
2
2
  //
3
3
  // Agents (and `atris deck compose`) describe a deck as an ordered list of
4
4
  // SECTIONS, each tagged with a `kind` (cover, quote, points, compare, metrics,
@@ -1,4 +1,4 @@
1
- // Deck review loop build -> thumbnail fetch -> agent visual review -> confirm.
1
+ // Deck review loop, build -> thumbnail fetch -> agent visual review -> confirm.
2
2
  //
3
3
  // Agents should not call a deck "ready" until thumbnails pass visual review.
4
4
  // This module downloads slide PNGs via the Atris Google Slides API and writes
@@ -58,7 +58,7 @@ const BOXED_TYPES = new Set(['panel', 'receipt', 'versus', 'metricgrid', 'stack'
58
58
 
59
59
  // Clip thresholds. `warn` = check the thumbnail; `error` = it will clip or lose
60
60
  // content, so block the build. `caps` mirror the .slice() limits the engine
61
- // applies exceeding one silently drops content, which is always an error.
61
+ // applies, exceeding one silently drops content, which is always an error.
62
62
  // Pass `lintSpec(spec, { limits })` to override any of these.
63
63
  const CLIP_LIMITS = {
64
64
  statementSubWarn: 130,
@@ -80,7 +80,7 @@ function lintSpec(spec, opts = {}) {
80
80
  severity: 'error',
81
81
  slide: slideNo,
82
82
  rule: 'content-truncated',
83
- message: `${count} ${label} but only ${cap} render; ${count - cap} will be dropped split the slide`,
83
+ message: `${count} ${label} but only ${cap} render; ${count - cap} will be dropped, split the slide`,
84
84
  });
85
85
  }
86
86
  };
@@ -227,7 +227,7 @@ function lintSpec(spec, opts = {}) {
227
227
  severity: 'warn',
228
228
  slide: slideNo,
229
229
  rule: 'stack-sub-clip',
230
- message: 'Four-layer stack plus a sub: the sub renders below the slide edge drop a layer or the sub',
230
+ message: 'Four-layer stack plus a sub: the sub renders below the slide edge, drop a layer or the sub',
231
231
  });
232
232
  }
233
233
  if (layerCount >= 3) {
@@ -253,7 +253,7 @@ function lintSpec(spec, opts = {}) {
253
253
  severity: 'warn',
254
254
  slide: 0,
255
255
  rule: 'template-fatigue',
256
- message: `${boxedCount} boxed slides consider narrative types: interstitial, lede, prose, split, statement, bignumber`,
256
+ message: `${boxedCount} boxed slides, consider narrative types: interstitial, lede, prose, split, statement, bignumber`,
257
257
  });
258
258
  }
259
259
  return findings;
@@ -1,4 +1,4 @@
1
- // Deck spec schema validation catch a bad spec before it hits the Slides API.
1
+ // Deck spec schema validation: catch a bad spec before it hits the Slides API.
2
2
  //
3
3
  // lintSpec (lib/deck-review.js) judges *taste* (clip risk, template fatigue,
4
4
  // AI-tell copy). validateSpec judges *shape*: unknown slide types, missing
@@ -10,7 +10,7 @@ const DEFAULT_MISSION_VERIFIER_TIMEOUT_MS = 120000;
10
10
  // default is a proven footgun: in atrisos-backend `npm test` runs
11
11
  // backend/scripts/test_fast.sh, which fails without its env and killed missions
12
12
  // after two ticks (2026-07-16). A default the operator never chose must be a
13
- // check that reflects the mission's own work, not the whole repo's health so
13
+ // check that reflects the mission's own work, not the whole repo's health, so
14
14
  // the mission lane falls back to the always-safe `git diff --check` and leaves
15
15
  // broad suites to explicit `--verify`. Fleet/wish keep the suite default.
16
16
  function resolveDefaultVerifier(root = process.cwd(), { allowBroadSuite = true } = {}) {
@@ -62,6 +62,9 @@ function formatDollars(amount) {
62
62
  return `$${n.toFixed(2)}`;
63
63
  }
64
64
 
65
+ // normalizeBalance is the only in-repo caller; keep the helper internal and
66
+ // stop exporting it so the repo-hygiene ratchet stays green.
67
+
65
68
  function normalizeBalance(data = {}) {
66
69
  const raw = data && typeof data === 'object' ? data : {};
67
70
  const credits = Number(raw.balance != null ? raw.balance : raw.credits);
package/lib/engine-ask.js CHANGED
@@ -24,13 +24,13 @@ const DEFAULT_ASK_CONCURRENCY = 3;
24
24
  const DEFAULT_ASK_TIMEOUT_MS = 120000;
25
25
  const MAX_ASK_CONCURRENCY = 4;
26
26
  const MAX_ASK_JOBS = 8;
27
- const MAX_ASK_TIMEOUT_MS = 10 * 60 * 1000;
28
- const DEFAULT_FABLE_ASK_TIMEOUT_MS = MAX_ASK_TIMEOUT_MS;
27
+ const DEFAULT_FABLE_ASK_TIMEOUT_MS = 10 * 60 * 1000;
28
+ const MAX_ASK_TIMEOUT_MS = 60 * 60 * 1000;
29
29
  const MAX_ASK_PROMPT_BYTES = 16 * 1024;
30
30
  const MAX_ASK_TOTAL_PROMPT_BYTES = 64 * 1024;
31
31
  const MAX_ASK_OUTPUT_BYTES = 1024 * 1024;
32
32
  const ASK_STOP_GRACE_MS = 250;
33
- const ASK_MODEL_ENGINES = new Set(['claude', 'fable', 'haiku', 'codex', 'cursor', 'devin', 'grok', 'agy']);
33
+ const ASK_MODEL_ENGINES = new Set(['claude', 'fable', 'haiku', 'codex', 'cursor', 'devin', 'grok', 'agy', 'opencode', 'commandcode']);
34
34
  const READ_ONLY_PREAMBLE = [
35
35
  'This is a read-only request.',
36
36
  'Do not modify files, create worktrees, start background agents, or run commands with side effects.',
@@ -49,6 +49,9 @@ function askUsage() {
49
49
  ` --timeout <sec> per-engine timeout, 1-${MAX_ASK_TIMEOUT_MS / 1000} (default ${DEFAULT_ASK_TIMEOUT_MS / 1000}; Fable ${DEFAULT_FABLE_ASK_TIMEOUT_MS / 1000})`,
50
50
  ' --json print the receipt as json',
51
51
  '',
52
+ 'long jobs: set --timeout explicitly; agy receives the same print deadline.',
53
+ 'watch a running receipt from another shell: atris engine watch latest',
54
+ '',
52
55
  `jobs files contain up to ${MAX_ASK_JOBS} objects: [{"engine":"codex","model":"optional","prompt":"question","label":"optional"}]`,
53
56
  ].join('\n');
54
57
  }
@@ -202,7 +205,9 @@ function resolveAskModel(engine, modelName = '') {
202
205
  return RUNNER_PROFILE_DEFS[engine]?.model || DEFAULT_CLAUDE_RUNNER_MODEL;
203
206
  }
204
207
 
205
- function buildReadOnlyEngineInvocation(engineName, prompt, modelName = '') {
208
+ function buildReadOnlyEngineInvocation(engineName, prompt, modelName = '', {
209
+ timeoutMs = DEFAULT_ASK_TIMEOUT_MS,
210
+ } = {}) {
206
211
  const engine = canonicalEngineName(engineName);
207
212
  const profile = RUNNER_PROFILE_DEFS[engine];
208
213
  if (!profile) throw new Error(`unknown engine "${engineName}"`);
@@ -258,9 +263,28 @@ function buildReadOnlyEngineInvocation(engineName, prompt, modelName = '') {
258
263
  return {
259
264
  engine,
260
265
  bin: profile.bin,
261
- args: ['--mode', 'plan', '--sandbox', '--dangerously-skip-permissions', ...(model ? ['--model', model] : []), '-p', request],
266
+ args: [
267
+ '--mode', 'plan',
268
+ '--sandbox',
269
+ '--dangerously-skip-permissions',
270
+ '--print-timeout', `${Math.max(1, Math.ceil(timeoutMs / 1000))}s`,
271
+ ...(model ? ['--model', model] : []),
272
+ '-p', request,
273
+ ],
262
274
  };
263
275
  }
276
+ if (engine === 'commandcode') {
277
+ // Headless print mode blocks file edits and shell commands by default,
278
+ // so plain -p is already read-only; no sandbox flags needed. Default is
279
+ // no --model: the run rides this machine's configured Command Code
280
+ // default, with an explicit ask --model still honored.
281
+ return { engine, bin: profile.bin, args: ['-p', request, ...(model ? ['--model', model] : [])] };
282
+ }
283
+ if (engine === 'opencode') {
284
+ // `run` with a message is headless print mode; the built-in plan agent
285
+ // keeps the ask read-only (verified live 2026-08-21, ~7s per lookup).
286
+ return { engine, bin: profile.bin, args: ['--agent', 'plan', ...(model ? ['-m', model] : []), request] };
287
+ }
264
288
  throw new Error(`engine ask has no read-only command for ${engine}`);
265
289
  }
266
290
 
@@ -292,6 +316,14 @@ function cancelledAskResult(job, extra = {}) {
292
316
  };
293
317
  }
294
318
 
319
+ function normalizeAskResult(engine, result = {}) {
320
+ const output = `${String(result.stdout || '')}\n${String(result.stderr || '')}`;
321
+ if (engine === 'agy' && !result.cancelled && /timeout waiting for response/i.test(output)) {
322
+ return { ...result, ok: false, reason: 'timeout', timed_out: true };
323
+ }
324
+ return result;
325
+ }
326
+
295
327
  function osAccountName() {
296
328
  try {
297
329
  return String(os.userInfo().username || '').trim();
@@ -448,7 +480,7 @@ async function runEngineAskJobs(jobs, {
448
480
  const answers = new Array(jobs.length);
449
481
  let nextIndex = 0;
450
482
  const execute = executeAskJob || (async (job) => {
451
- const invocation = buildReadOnlyEngineInvocation(job.engine, job.prompt, job.model);
483
+ const invocation = buildReadOnlyEngineInvocation(job.engine, job.prompt, job.model, { timeoutMs });
452
484
  return runAskProcess(invocation, { cwd: invocation.cwd || root, timeoutMs, signal, onOutputChunk });
453
485
  });
454
486
  const workerCount = Math.min(concurrency, jobs.length);
@@ -465,7 +497,8 @@ async function runEngineAskJobs(jobs, {
465
497
  }
466
498
  try {
467
499
  assertAskModelSupported(job.engine, job.model);
468
- answers[index] = { ...job, ...(await execute(job, { root, timeoutMs, index, signal, onOutputChunk })) };
500
+ const result = await execute(job, { root, timeoutMs, index, signal, onOutputChunk });
501
+ answers[index] = { ...job, ...normalizeAskResult(job.engine, result) };
469
502
  } catch (error) {
470
503
  answers[index] = {
471
504
  ...job,
@@ -586,12 +619,17 @@ async function runEngineAskCommand(args, root = process.cwd(), deps = {}) {
586
619
  started_at: startedAt,
587
620
  at: startedAt,
588
621
  read_only: true,
622
+ timeout_ms: parsed.timeoutMs,
589
623
  };
590
624
  const receiptPath = (deps.createReceipt || createEngineAskReceipt)(root, runningReceipt);
591
625
  const relativeReceiptPath = path.relative(root, receiptPath) || receiptPath;
592
626
  const liveLogPath = (deps.createLiveLog || createEngineLiveLog)(receiptPath);
593
627
  runningReceipt.live_log = path.relative(root, liveLogPath) || liveLogPath;
594
628
  (deps.updateReceipt || atomicWriteEngineAskReceipt)(receiptPath, runningReceipt);
629
+ if (!parsed.json) {
630
+ console.log(`engine ask started: ${relativeReceiptPath}`);
631
+ console.log(`watch: atris engine watch ${path.basename(receiptPath, '.json')}`);
632
+ }
595
633
  const appendLiveLog = deps.appendLiveLog
596
634
  || ((filePath, chunk) => appendEngineLiveLogChunk(filePath, chunk));
597
635
  let interruptedSignal = '';