atris 3.62.0 → 3.64.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 (72) hide show
  1. package/atris/features/README.md +8 -0
  2. package/atris/skills/README.md +1 -0
  3. package/atris/skills/commerce/SKILL.md +120 -0
  4. package/atris/skills/x-search/SKILL.md +3 -1
  5. package/ax +4 -3
  6. package/bin/atris.js +22 -2
  7. package/cli/atris_code.py +25 -1
  8. package/commands/auth.js +79 -10
  9. package/commands/autoland.js +11 -1
  10. package/commands/browse.js +2 -2
  11. package/commands/business.js +3 -3
  12. package/commands/buy.js +242 -0
  13. package/commands/deck.js +2 -2
  14. package/commands/engine.js +122 -13
  15. package/commands/feedback.js +2 -2
  16. package/commands/fork.js +2 -2
  17. package/commands/gm.js +2 -1
  18. package/commands/improve.js +142 -8
  19. package/commands/integrations.js +4 -3
  20. package/commands/loops.js +2 -1
  21. package/commands/member.js +82 -15
  22. package/commands/mission.js +4 -1
  23. package/commands/pack-craft.js +2 -2
  24. package/commands/publish.js +2 -2
  25. package/commands/radar.js +3 -2
  26. package/commands/router.js +2 -2
  27. package/commands/task.js +91 -2
  28. package/commands/transactions.js +115 -0
  29. package/commands/truth.js +2 -2
  30. package/commands/x-search.js +81 -10
  31. package/commands/youtube.js +24 -1
  32. package/lib/autoland.js +3 -2
  33. package/lib/ax-auto-lane.js +2 -2
  34. package/lib/brief-rules.js +11 -0
  35. package/lib/chat-log-scan.js +2 -1
  36. package/lib/ci-runner.js +3 -2
  37. package/lib/codex-flight.js +2 -1
  38. package/lib/commerce.js +90 -0
  39. package/lib/deck-history.js +2 -2
  40. package/lib/deck-review.js +3 -3
  41. package/lib/design-api.js +2 -2
  42. package/lib/engine-registry.js +102 -18
  43. package/lib/first-minute.js +2 -2
  44. package/lib/fleet.js +8 -36
  45. package/lib/git-spawn.js +5 -2
  46. package/lib/ignored-cache.js +51 -0
  47. package/lib/known-commands.js +1 -1
  48. package/lib/manifest.js +2 -2
  49. package/lib/map-refs.js +70 -4
  50. package/lib/member-engine.js +15 -1
  51. package/lib/member-scaffold.js +2 -0
  52. package/lib/member-type.js +27 -0
  53. package/lib/pack-capabilities.js +2 -1
  54. package/lib/permission-grants.js +2 -2
  55. package/lib/pulse.js +5 -5
  56. package/lib/roster-runs.js +13 -2
  57. package/lib/runner-command.js +54 -3
  58. package/lib/sandbox-pids.js +113 -0
  59. package/lib/secret-gateway.js +3 -13
  60. package/lib/state-home.js +106 -0
  61. package/lib/task-db.js +4 -3
  62. package/lib/task-prune.js +462 -0
  63. package/lib/x-post-check.js +497 -0
  64. package/package.json +1 -1
  65. package/scripts/det/data/xsearch-answer.json +24 -0
  66. package/scripts/det/data/xsearch-live-2026-09-29.json +93 -0
  67. package/scripts/det/xsearch-bench.js +307 -0
  68. package/scripts/det/ytnotes +122 -16
  69. package/scripts/det/ytrail-bench.js +215 -0
  70. package/scripts/det/ytrail-eval.js +128 -56
  71. package/utils/auth.js +3 -3
  72. package/utils/update-check.js +2 -2
package/commands/task.js CHANGED
@@ -253,6 +253,8 @@ atris task - durable local task state (SQLite, gitignored)
253
253
  Sweep off-roadmap/duplicate work as archived (not failed);
254
254
  --from-failed opts in to relabel a fail-closed row (never done)
255
255
  atris task clear-done [--before <days>] [--dry-run] [--json] Archive completed rows, oldest first
256
+ atris task prune-test-junk [--yes] [--vacuum] [--json]
257
+ Count rows test runs left from temp folders; --yes backs up the db, then removes them
256
258
  atris task keep [--json] Put away finished and untouched work, then refresh the list
257
259
  atris task reap-mission-blockers [--json] Close blocker rows whose missions are complete or stopped
258
260
  atris task relabel-archived [--dry-run|--apply]
@@ -1421,7 +1423,7 @@ function taskReviewSummary(task) {
1421
1423
  const reviewPassCount = Number(metadata.agent_review_pass_count || payload.review_pass_count || 0);
1422
1424
  const agentCertified = metadata.agent_certified === true
1423
1425
  || payload.agent_certified === true
1424
- || reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES;
1426
+ || (reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES && reviewIntegrity.hasIndependentReview(task));
1425
1427
  const reviewedEventHas = (key) => reviewed && reviewed.event_type === 'reviewed'
1426
1428
  && Object.prototype.hasOwnProperty.call(payload, key);
1427
1429
  const clearedReviewFields = new Set(Array.isArray(payload.cleared_review_fields) ? payload.cleared_review_fields : []);
@@ -7922,6 +7924,8 @@ function publicWords(value) {
7922
7924
  ]).has(word));
7923
7925
  }
7924
7926
 
7927
+ const { memberTypeOf } = require('../lib/member-type');
7928
+
7925
7929
  function parseMemberFrontmatter(text) {
7926
7930
  const source = String(text || '');
7927
7931
  if (!source.startsWith('---')) return {};
@@ -7952,6 +7956,7 @@ function readTeamMembers(root = process.cwd()) {
7952
7956
  slug,
7953
7957
  role: cleanPublicText(frontmatter.role || slug.replace(/[-_]/g, ' '), 120),
7954
7958
  description: cleanPublicText(frontmatter.description || '', 240),
7959
+ type: memberTypeOf(frontmatter).type,
7955
7960
  path: memberPath,
7956
7961
  };
7957
7962
  })
@@ -8018,6 +8023,8 @@ function chooseTaskOwner({ purpose, tag, requestedOwner, root = process.cwd() }
8018
8023
  const words = publicWords(`${purpose || ''} ${tag || ''}`);
8019
8024
  let best = null;
8020
8025
  for (const member of members) {
8026
+ // A person can be named as an owner, but routing never hands them work as if they were an AI.
8027
+ if (member.type !== 'ai') continue;
8021
8028
  const score = scoreTeamMember(member, words, tag);
8022
8029
  if (!best || score > best.score) best = { member, score };
8023
8030
  }
@@ -8836,7 +8843,7 @@ function taskAllowsReviewChat(task, { allowCertified = false } = {}) {
8836
8843
  if (allowCertified) return true;
8837
8844
  if (review.agent_certified === true || metadata.agent_certified === true) return false;
8838
8845
  const reviewPassCount = Number(review.agent_review_pass_count || metadata.agent_review_pass_count || 0);
8839
- if (reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES) return false;
8846
+ if (reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES && reviewIntegrity.hasIndependentReview(task)) return false;
8840
8847
  const handoff = reviewHandoffForTask(task, { suppressExistingFollowUp: true });
8841
8848
  return !(handoff && (handoff.next_action === 'continue_work' || handoff.next_action === 'human_accept_waiting'));
8842
8849
  }
@@ -9809,6 +9816,78 @@ function cmdClearDone(args) {
9809
9816
  console.log(`cleared ${candidates.length} completed task(s).`);
9810
9817
  }
9811
9818
 
9819
+ function formatBytes(bytes) {
9820
+ const mb = Number(bytes || 0) / (1024 * 1024);
9821
+ return mb >= 1 ? `${mb.toFixed(1)} MB` : `${Math.round(Number(bytes || 0) / 1024)} KB`;
9822
+ }
9823
+
9824
+ // Test runs that forgot ATRIS_TASKS_DB left rows in the shared database
9825
+ // under temp folders. Default is a read-only count; --yes backs up, then
9826
+ // removes those rows and their history. Real project rows are never touched.
9827
+ function cmdPruneTestJunk(args) {
9828
+ const taskDb = getTaskDb();
9829
+ const prune = require('../lib/task-prune');
9830
+ const dbPath = taskDb.getDbPath();
9831
+ const apply = hasFlag(args, '--yes') && !hasFlag(args, '--dry-run');
9832
+ const vacuum = apply && hasFlag(args, '--vacuum');
9833
+ if (!apply) {
9834
+ const plan = prune.dryRun(dbPath);
9835
+ const { rows, ...summary } = plan;
9836
+ if (wantsJson(args)) {
9837
+ printJson({ ok: true, action: 'prune-test-junk', dry_run: true, ...summary });
9838
+ return;
9839
+ }
9840
+ if (plan.missing) {
9841
+ console.log(`no task database at ${dbPath}. nothing to prune.`);
9842
+ return;
9843
+ }
9844
+ const r = plan.remove;
9845
+ console.log('prune-test-junk dry run: nothing was changed.');
9846
+ console.log(`database: ${dbPath} (${formatBytes(plan.db_bytes)}, ${plan.total_tasks} tasks)`);
9847
+ console.log(`would remove ${r.tasks} test task(s) from temp folders, ${r.history_rows} history row(s), `
9848
+ + `${r.stray_history_rows} stray history row(s), ${r.part_uses} part use row(s).`);
9849
+ for (const g of plan.groups) {
9850
+ console.log(` ${g.prefix.padEnd(22)} ${String(g.tasks).padStart(7)} tasks ${String(g.history_rows + g.stray_history_rows).padStart(8)} history rows`);
9851
+ }
9852
+ const k = plan.kept;
9853
+ console.log(`kept: ${k.folder_exists} temp task(s) whose folder still exists, ${k.recent} active in the last ${plan.grace_hours} hour(s), `
9854
+ + `${k.held} with history from a real project, ${k.stray_held + k.stray_recent} leftover history set(s) that are recent or name a real project.`);
9855
+ if (r.tasks || r.stray_history_rows) {
9856
+ console.log('run with --yes to remove them. a backup copy is written next to the database first.');
9857
+ }
9858
+ return;
9859
+ }
9860
+
9861
+ if (vacuum && !wantsJson(args)) {
9862
+ console.log('note: --vacuum rewrites the whole file and needs every other atris process idle; it waits a few seconds, then gives up.');
9863
+ }
9864
+ let result;
9865
+ try {
9866
+ result = prune.applyPrune(dbPath, { vacuum });
9867
+ } catch (err) {
9868
+ failTask('atris task prune-test-junk', 'prune_failed', `prune-test-junk stopped: ${err && err.message || err}`, 1);
9869
+ }
9870
+ if (wantsJson(args)) {
9871
+ printJson({ ok: true, action: 'prune-test-junk', dry_run: false, ...result });
9872
+ return;
9873
+ }
9874
+ if (result.missing) {
9875
+ console.log(`no task database at ${dbPath}. nothing to prune.`);
9876
+ return;
9877
+ }
9878
+ const r = result.removed;
9879
+ console.log(`backup: ${result.backup_path} (${result.backup_tasks} tasks)`);
9880
+ console.log(`removed ${r.tasks} test task(s), ${r.history_rows} history row(s), `
9881
+ + `${r.stray_history_rows} stray history row(s), ${r.part_uses} part use row(s).`);
9882
+ const sk = result.skipped;
9883
+ if (sk.held || sk.changed_since_backup || sk.recent || sk.folder_exists) {
9884
+ console.log(`left ${sk.changed_since_backup} that changed after the backup, ${sk.recent} that became active, `
9885
+ + `${sk.folder_exists} whose folder came back, and ${sk.held} with real-project history.`);
9886
+ }
9887
+ console.log(`database now has ${result.total_tasks} tasks, ${formatBytes(result.bytes_after)} on disk`
9888
+ + (result.vacuumed ? ' after vacuum.' : '. run again with --yes --vacuum to shrink the file.'));
9889
+ }
9890
+
9812
9891
  function cmdReapMissionBlockers(args) {
9813
9892
  const taskDb = getTaskDb();
9814
9893
  const db = taskDb.open();
@@ -10463,6 +10542,13 @@ function stampReadyVerifyMetadata(taskDb, db, taskId, verify) {
10463
10542
  if (!row) return;
10464
10543
  const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
10465
10544
  metadata.verify = verify;
10545
+ // A real verify stamped at ready means the creation-time degraded flag is
10546
+ // stale: clear it so `task show` stops warning about a missing check. A
10547
+ // diff-only verify keeps the flag; it still cannot fail on real breakage.
10548
+ if (verify.toLowerCase() !== 'git diff --check') {
10549
+ delete metadata.verification_status;
10550
+ delete metadata.verification_degraded_reason;
10551
+ }
10466
10552
  db.prepare(`
10467
10553
  UPDATE tasks
10468
10554
  SET metadata = ?,
@@ -13369,6 +13455,7 @@ async function runTaskCommand(args) {
13369
13455
  case 'fail': return cmdDone([...rest, '--failed']);
13370
13456
  case 'archive': return cmdArchive(rest);
13371
13457
  case 'clear-done': return cmdClearDone(rest);
13458
+ case 'prune-test-junk': return cmdPruneTestJunk(rest);
13372
13459
  case 'keep': return cmdKeep(rest);
13373
13460
  case 'reap-mission-blockers':
13374
13461
  case 'reap-blockers':
@@ -13465,6 +13552,8 @@ async function run(args) {
13465
13552
 
13466
13553
  module.exports = {
13467
13554
  run,
13555
+ chooseTaskOwner,
13556
+ readTeamMembers,
13468
13557
  createTaskApiServer,
13469
13558
  taskDayGroups,
13470
13559
  taskDayTextGroups,
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ // atris transactions: every order, fee, trade, and wallet send, newest first.
4
+ // Read only. Backend: GET /api/transactions.
5
+
6
+ const { apiRequestJson } = require('../utils/api');
7
+ const { loadCredentials } = require('../utils/auth');
8
+ const { NOT_LOGGED_IN, oneLine, printResult } = require('../lib/developer-api');
9
+ const { parseFlags, formatCents, errorFrom, commerceToken } = require('../lib/commerce');
10
+
11
+ const TX_KINDS = ['ticket', 'shop', 'flight', 'test_purchase', 'fee', 'trade', 'wallet_send'];
12
+
13
+ function showTransactionsHelp(log = console.log) {
14
+ log(`usage: atris transactions [--since YYYY-MM-DD] [--kind k[,k]] [--limit n] [--include-quotes] [--json]
15
+
16
+ list what you bought, paid in fees, traded, or sent, newest first. read only.
17
+
18
+ --since only rows on or after this date, like 2026-09-01
19
+ --kind ${TX_KINDS.join(', ')}
20
+ --limit 1 to 200 (default 50)
21
+ --include-quotes also list quotes nobody approved`);
22
+ }
23
+
24
+ function clip(text, width) {
25
+ const s = String(text == null ? '' : text).replace(/\s+/g, ' ').trim();
26
+ return s.length > width ? `${s.slice(0, width - 1)}…` : s;
27
+ }
28
+
29
+ function whenText(value) {
30
+ const s = String(value || '');
31
+ const m = s.match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2})/);
32
+ return m ? `${m[1]} ${m[2]}` : clip(s, 16);
33
+ }
34
+
35
+ function renderTransactions(data = {}) {
36
+ const rows = Array.isArray(data.transactions) ? data.transactions : [];
37
+ const lines = [];
38
+ if (!rows.length) lines.push('No transactions yet.');
39
+ else {
40
+ const table = rows.map((r) => [
41
+ whenText(r.when),
42
+ clip(r.kind, 12),
43
+ clip(r.what, 40),
44
+ formatCents(r.amount_cents, r.currency),
45
+ clip(r.state, 16),
46
+ ]);
47
+ const head = ['WHEN', 'KIND', 'WHAT', 'AMOUNT', 'STATE'];
48
+ const widths = head.map((h, i) => Math.max(h.length, ...table.map((row) => row[i].length)));
49
+ const fmt = (row) => row.map((cell, i) => (i === 3 ? cell.padStart(widths[i]) : cell.padEnd(widths[i])))
50
+ .join(' ').trimEnd();
51
+ lines.push(fmt(head));
52
+ for (const row of table) lines.push(fmt(row));
53
+ }
54
+ const down = Object.entries(data.sources || {}).filter(([, v]) => v !== 'ok').map(([k]) => k);
55
+ if (down.length) lines.push('', `Partial list: could not load ${down.join(', ')}.`);
56
+ return lines.join('\n');
57
+ }
58
+
59
+ function buildQuery(flags) {
60
+ const params = new URLSearchParams();
61
+ if (flags.since != null) {
62
+ const since = String(flags.since).trim();
63
+ if (!/^\d{4}-\d{2}-\d{2}/.test(since)) return { ok: false, error: '--since must be a date like 2026-09-01' };
64
+ params.set('since', since);
65
+ }
66
+ const rawKind = flags.kind != null ? flags.kind : flags.kinds;
67
+ if (rawKind != null) {
68
+ const kinds = String(rawKind).split(',').map((k) => k.trim().toLowerCase()).filter(Boolean);
69
+ const bad = kinds.filter((k) => !TX_KINDS.includes(k));
70
+ if (bad.length || !kinds.length) return { ok: false, error: `--kind must be one of ${TX_KINDS.join(', ')}` };
71
+ params.set('kinds', kinds.join(','));
72
+ }
73
+ if (flags.limit != null) {
74
+ const limit = Number(flags.limit);
75
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200) return { ok: false, error: '--limit must be a number from 1 to 200' };
76
+ params.set('limit', String(limit));
77
+ }
78
+ if (flags.include_quotes === true) params.set('include_quotes', 'true');
79
+ const qs = params.toString();
80
+ return { ok: true, path: qs ? `/transactions?${qs}` : '/transactions' };
81
+ }
82
+
83
+ async function transactionsCommand(args = [], deps = {}) {
84
+ const io = { log: deps.log || console.log, err: deps.err || console.error };
85
+ const { flags, pos } = parseFlags(args);
86
+ if (flags.help || pos[0] === 'help') {
87
+ showTransactionsHelp(io.log);
88
+ return 0;
89
+ }
90
+ const json = flags.json === true;
91
+ const fail = (error, status) => {
92
+ printResult({ json, ok: false, error, payload: { ok: false, error, ...(status ? { status } : {}) } }, io);
93
+ return 1;
94
+ };
95
+
96
+ const query = buildQuery(flags);
97
+ if (!query.ok) return fail(query.error);
98
+
99
+ const load = deps.loadCredentials || loadCredentials;
100
+ const request = deps.apiRequestJson || apiRequestJson;
101
+ const token = commerceToken(load(), 'transactions:read', deps.now ? deps.now() : Date.now());
102
+ if (!token) return fail(NOT_LOGGED_IN);
103
+
104
+ try {
105
+ const result = await request(query.path, { method: 'GET', token });
106
+ if (!result || !result.ok) return fail(errorFrom(result), result && result.status);
107
+ const data = result.data || {};
108
+ printResult({ json, ok: true, text: renderTransactions(data), payload: { ok: true, ...data } }, io);
109
+ return 0;
110
+ } catch (error) {
111
+ return fail(oneLine(error && error.message));
112
+ }
113
+ }
114
+
115
+ module.exports = { transactionsCommand };
package/commands/truth.js CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
- const os = require('os');
10
+ const { stateHome } = require('../lib/state-home');
11
11
  const { execFileSync } = require('child_process');
12
12
  const taskDb = require('../lib/task-db');
13
13
  const { parseScopeFlag } = require('../lib/cli-scope');
@@ -172,7 +172,7 @@ function loadFeatures(cwd) {
172
172
  }
173
173
 
174
174
  function loadHeartbeats() {
175
- const hbDir = path.join(os.homedir(), '.atris', 'heartbeat');
175
+ const hbDir = path.join(stateHome(), '.atris', 'heartbeat');
176
176
  const registry = readJson(path.join(hbDir, 'registry.json'));
177
177
  const state = readJson(path.join(hbDir, 'state.json')) || {};
178
178
  if (!registry || !Array.isArray(registry.jobs)) return [];
@@ -4,7 +4,9 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { apiRequestJson } = require('../utils/api');
6
6
  const { ensureBilledCommandAuth } = require('./auth');
7
+ const { spawnSync } = require('child_process');
7
8
  const applyGate = require('../lib/apply-gate');
9
+ const { checkXPosts, annotateContent, formatTally } = require('../lib/x-post-check');
8
10
  const {
9
11
  fileTeachExperiment,
10
12
  extractTeachNumbers,
@@ -23,9 +25,10 @@ const KEEP_RULE = 'keep only if measure.py moves 0→1. scores 1 only when the f
23
25
 
24
26
  function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
25
27
  output('');
26
- output(`Usage: ${commandName} "<query>" [--limit N] [--days N] [--save] [--json]`);
27
- output(` ${commandName} person --name <name> [--handle <h>] [--company <c>] [--context <text>] [--save] [--json]`);
28
+ output(`Usage: ${commandName} "<query>" [--limit N] [--days N] [--save] [--json] [--no-check]`);
29
+ output(` ${commandName} person --name <name> [--handle <h>] [--company <c>] [--context <text>] [--save] [--json] [--no-check]`);
28
30
  output(` ${commandName} unsave <query-or-source>`);
31
+ output(` ${commandName} bench [--quick|--free]`);
29
32
  output('');
30
33
  output(`Search X/Twitter via Atris (${COST_HINT}).`);
31
34
  output('Requires login. Same auth path as atris youtube process.');
@@ -33,13 +36,15 @@ function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
33
36
  output('--save files a brief only when the result is rich.');
34
37
  output('unsave deletes the filed brief, apply stub, and matching experiment pack (no paid calls).');
35
38
  output('Empty or failed search prints credits refunded only when the server marks a refund.');
39
+ output('Each quoted post is looked up on X for free and marked checked, unverified, or unknown.');
36
40
  output('');
37
41
  output('Options:');
38
42
  output(' --limit <n> Max results hint (search only)');
39
43
  output(' --days <n> Only tweets from the last N days (search only)');
40
44
  output(' --save File brief, journal, apply; rich results mint a keep/revert experiment');
41
45
  output(' --unsave Delete filed brief, apply stub, and matching experiment pack (no paid calls)');
42
- output(' --json Print the raw JSON response');
46
+ output(' --json Print the raw JSON response (plus a checks list)');
47
+ output(' --no-check Skip looking up the quoted posts on X');
43
48
  output(' -h, --help This help');
44
49
  output('');
45
50
  output('Person options:');
@@ -55,6 +60,7 @@ function showXSearchHelp(output = console.log, commandName = 'atris x-search') {
55
60
  output(` ${commandName} person --name "Leah Bonvissuto" --handle leahbon`);
56
61
  output(` ${commandName} unsave "MCP agents"`);
57
62
  output(` ${commandName} --unsave "MCP agents"`);
63
+ output(` ${commandName} bench --free`);
58
64
  output('');
59
65
  }
60
66
 
@@ -81,6 +87,7 @@ function parseSearchArgs(argv = []) {
81
87
  json: false,
82
88
  save: false,
83
89
  unsave: false,
90
+ check: true,
84
91
  query: null,
85
92
  limit: null,
86
93
  daysBack: null,
@@ -100,6 +107,8 @@ function parseSearchArgs(argv = []) {
100
107
  options.json = true;
101
108
  } else if (arg === '--save') {
102
109
  options.save = true;
110
+ } else if (arg === '--no-check') {
111
+ options.check = false;
103
112
  } else if (arg === '--unsave') {
104
113
  options.unsave = true;
105
114
  } else if (arg === '--limit') {
@@ -153,6 +162,7 @@ function parsePersonArgs(argv = []) {
153
162
  help: false,
154
163
  json: false,
155
164
  save: false,
165
+ check: true,
156
166
  name: null,
157
167
  handle: null,
158
168
  company: null,
@@ -173,6 +183,8 @@ function parsePersonArgs(argv = []) {
173
183
  options.json = true;
174
184
  } else if (arg === '--save') {
175
185
  options.save = true;
186
+ } else if (arg === '--no-check') {
187
+ options.check = false;
176
188
  } else if (arg === '--name') {
177
189
  options.name = readValue(args, i, arg);
178
190
  i++;
@@ -592,23 +604,26 @@ function ensureXSearchApply({ cwd, source, packRel, now, output } = {}) {
592
604
  });
593
605
  }
594
606
 
595
- function formatXSearchResult(data) {
607
+ // check is the result of checkXPosts. With it, each post carries its own
608
+ // link or warning, and citations no post uses are listed as other sources.
609
+ function formatXSearchResult(data, check = null) {
596
610
  const lines = [];
597
611
  const content = xSearchContent(data);
598
612
  const citations = xSearchCitations(data);
599
613
 
600
614
  if (content) {
601
- lines.push(content);
615
+ lines.push(check ? annotateContent(content, check) : content);
602
616
  } else if (data?.message) {
603
617
  lines.push(String(data.message).trim());
604
618
  } else {
605
619
  lines.push('X search completed');
606
620
  }
607
621
 
608
- if (citations.length) {
622
+ const listed = check ? check.otherSources : citations;
623
+ if (listed.length) {
609
624
  lines.push('');
610
- lines.push('Citations:');
611
- for (const cite of citations) {
625
+ lines.push(check ? 'Other sources:' : 'Citations:');
626
+ for (const cite of listed) {
612
627
  lines.push(` ${cite}`);
613
628
  }
614
629
  }
@@ -619,9 +634,54 @@ function formatXSearchResult(data) {
619
634
  lines.push(...creditLines);
620
635
  }
621
636
 
637
+ if (check) {
638
+ lines.push('');
639
+ lines.push(formatTally(check.tally));
640
+ }
641
+
622
642
  return lines.join('\n');
623
643
  }
624
644
 
645
+ // Look the quoted posts up on X. Free, and never fatal: any failure means no
646
+ // check, and the original output prints unchanged.
647
+ async function checkXSearchPosts(data, options, deps = {}) {
648
+ if (options.check === false) return null;
649
+ try {
650
+ return await (deps.checkXPosts || checkXPosts)({
651
+ content: xSearchContent(data),
652
+ citations: xSearchCitations(data),
653
+ fetchPost: deps.fetchPost,
654
+ fetch: deps.fetch,
655
+ });
656
+ } catch {
657
+ return null;
658
+ }
659
+ }
660
+
661
+ function renderXSearchResult(data, check) {
662
+ if (check) {
663
+ try {
664
+ return formatXSearchResult(data, check);
665
+ } catch {
666
+ // fall through to the original output
667
+ }
668
+ }
669
+ return formatXSearchResult(data);
670
+ }
671
+
672
+ function runXSearchBench(argv, deps = {}) {
673
+ const run = (deps.spawnSync || spawnSync)(
674
+ process.execPath,
675
+ [path.join(__dirname, '..', 'scripts', 'det', 'xsearch-bench.js'), ...argv],
676
+ { stdio: 'inherit' },
677
+ );
678
+ if (run.error) {
679
+ (deps.output || console.error)(`bench did not start: ${run.error.message}`);
680
+ return 1;
681
+ }
682
+ return typeof run.status === 'number' ? run.status : 1;
683
+ }
684
+
625
685
  function formatEmptyXSearchResult(data) {
626
686
  const lines = ['no results'];
627
687
  const creditLines = formatCreditsLines(xSearchCredits(data));
@@ -634,6 +694,11 @@ function formatEmptyXSearchResult(data) {
634
694
 
635
695
  async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
636
696
  const output = deps.output || ((line = '') => console.log(line));
697
+ if (argv[0] === 'bench') {
698
+ const code = runXSearchBench(argv.slice(1), deps);
699
+ if (!deps.output && !deps.spawnSync) process.exit(code);
700
+ return code;
701
+ }
637
702
  let options;
638
703
  try {
639
704
  options = parseXSearchArgs(argv);
@@ -655,10 +720,14 @@ async function xSearchCommand(argv = process.argv.slice(3), deps = {}) {
655
720
  try {
656
721
  const data = await runXSearch(options, deps);
657
722
  const hasResults = xSearchHasResults(data);
723
+ const check = hasResults ? await checkXSearchPosts(data, options, deps) : null;
658
724
  if (options.json) {
659
- output(JSON.stringify(data, null, 2));
725
+ const withChecks = check && data && typeof data === 'object' && !Array.isArray(data)
726
+ ? { ...data, checks: check.checks }
727
+ : data;
728
+ output(JSON.stringify(withChecks, null, 2));
660
729
  } else {
661
- output(hasResults ? formatXSearchResult(data) : formatEmptyXSearchResult(data));
730
+ output(hasResults ? renderXSearchResult(data, check) : formatEmptyXSearchResult(data));
662
731
  }
663
732
  if (!hasResults) {
664
733
  if (
@@ -734,5 +803,7 @@ module.exports = {
734
803
  xSearchExperimentSlug,
735
804
  xSearchExperimentRel,
736
805
  unsaveXSearch,
806
+ formatXSearchResult,
807
+ runXSearchBench,
737
808
  xSearchCommand,
738
809
  };
@@ -5,6 +5,7 @@ const { spawnSync } = require('child_process');
5
5
  const fs = require('fs');
6
6
  const os = require('os');
7
7
  const path = require('path');
8
+ const { stateHome } = require('../lib/state-home');
8
9
  const https = require('https');
9
10
 
10
11
  const YTNOTES_USAGE = 'usage: ytnotes <youtube-url> [youtube-url-or-playlist...] [auto|gemini|haiku|atris-fast|grok|codex|cursor]';
@@ -40,6 +41,7 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
40
41
  output(` ${commandName} watch list`);
41
42
  output(` ${commandName} watch remove <number>`);
42
43
  output(` ${commandName} watch tick`);
44
+ output(` ${commandName} bench [--quick] [--case <name>]`);
43
45
  output(` ${commandName} <youtube-url> [options]`);
44
46
  output('');
45
47
  output('search = free local discovery (ytsearch / yt-dlp), returns youtu.be links; rich free search writes one apply and a failing keep/revert pack; thin hands off to teach');
@@ -50,6 +52,7 @@ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
50
52
  output('process = 5 credits cloud knowledge (needs a filled Apply); rich process writes one apply and a failing keep/revert pack');
51
53
  output('digest = one decision page from this week\'s video briefs; rich digest writes one apply and a failing keep/revert pack');
52
54
  output('watch = subscribed channels turn into briefs without a human; add hands off to tick; tick hands off to teach when it briefed');
55
+ output('bench = run notes on real videos, uncached, and grade what a user gets; --quick runs the short video and the stranger checks');
53
56
  output('Process a YouTube video through Atris using timestamped transcript-first analysis.');
54
57
  output('Falls back to cloud video processing when local captions are unavailable.');
55
58
  output('');
@@ -2254,7 +2257,7 @@ function searchCacheTtlMs(deps = {}) {
2254
2257
 
2255
2258
  function resolveSearchCachePath(deps = {}) {
2256
2259
  if (deps.searchCachePath) return deps.searchCachePath;
2257
- const homeDir = deps.homeDir || os.homedir();
2260
+ const homeDir = deps.homeDir || stateHome();
2258
2261
  return path.join(homeDir, '.atris', LOCAL_SEARCH_CACHE_FILE);
2259
2262
  }
2260
2263
 
@@ -3851,6 +3854,20 @@ async function runYoutubeTeach(args = [], deps = {}) {
3851
3854
  return applyCode;
3852
3855
  }
3853
3856
 
3857
+ // Bench the notes feature end to end. The script prints its own lines.
3858
+ function runYoutubeBench(argv, deps = {}) {
3859
+ const run = (deps.spawnSync || spawnSync)(
3860
+ process.execPath,
3861
+ [path.join(__dirname, '..', 'scripts', 'det', 'ytrail-bench.js'), ...argv],
3862
+ { stdio: 'inherit' },
3863
+ );
3864
+ if (run.error) {
3865
+ (deps.output || console.error)(`bench did not start: ${run.error.message}`);
3866
+ return 1;
3867
+ }
3868
+ return typeof run.status === 'number' ? run.status : 1;
3869
+ }
3870
+
3854
3871
  async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
3855
3872
  const output = deps.output || ((line = '') => console.log(line));
3856
3873
  if (argv[0] === 'search') {
@@ -3880,6 +3897,11 @@ async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
3880
3897
  if (!deps.output && !deps.runner) process.exit(code);
3881
3898
  return code;
3882
3899
  }
3900
+ if (argv[0] === 'bench') {
3901
+ const code = runYoutubeBench(argv.slice(1), deps);
3902
+ if (!deps.output && !deps.spawnSync) process.exit(code);
3903
+ return code;
3904
+ }
3883
3905
  if (argv[0] === 'watch') {
3884
3906
  const code = await watchCommand(argv.slice(1), { ...deps, output });
3885
3907
  if (!deps.output && !deps.fetcher && !deps.runner && !deps.briefFiler) process.exit(code);
@@ -3978,5 +4000,6 @@ module.exports = {
3978
4000
  searchApplyRel,
3979
4001
  firstRichWatchLesson,
3980
4002
  fileTeachExperiment,
4003
+ runYoutubeBench,
3981
4004
  youtubeCommand,
3982
4005
  };
package/lib/autoland.js CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
+ const { stateHome } = require('./state-home');
10
11
  const os = require('os');
11
12
  const { spawnSync } = require('child_process');
12
13
  const { computeTrustTier } = require('./trust-tiers');
@@ -189,7 +190,7 @@ function buildCronLine(root) {
189
190
  const node = process.execPath;
190
191
  const bin = path.join(root, 'bin', 'atris.js');
191
192
  const cliBin = fs.existsSync(bin) ? bin : 'atris';
192
- const logDir = path.join(os.homedir(), '.atris', 'overnight');
193
+ const logDir = path.join(stateHome(), '.atris', 'overnight');
193
194
  const logFile = path.join(logDir, `autoland-${path.basename(root)}.log`);
194
195
  const invoke = cliBin.endsWith('.js') ? `${node} ${cliBin}` : cliBin;
195
196
  return `14 * * * * cd ${root} && ${invoke} autoland tick >> ${logFile} 2>&1 # ${cronMarker(root)}`;
@@ -210,7 +211,7 @@ function cronInstalled(root) {
210
211
  }
211
212
 
212
213
  function installCron(root) {
213
- fs.mkdirSync(path.join(os.homedir(), '.atris', 'overnight'), { recursive: true });
214
+ fs.mkdirSync(path.join(stateHome(), '.atris', 'overnight'), { recursive: true });
214
215
  const marker = cronMarker(root);
215
216
  const kept = readCrontab().split(/\r?\n/).filter((line) => line.trim() && !line.includes(marker));
216
217
  const next = `${kept.join('\n')}${kept.length ? '\n' : ''}${buildCronLine(root)}\n`;
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('node:fs');
4
- const os = require('node:os');
5
4
  const path = require('node:path');
5
+ const { stateHome } = require('./state-home');
6
6
 
7
7
  const SHORT_LOOKUP_MAX_CHARS = 140;
8
8
  const LONG_INPUT_MIN_CHARS = 1200;
@@ -75,7 +75,7 @@ function overrideMatches(text, override) {
75
75
  function loadOverrides(filePath) {
76
76
  const resolved = filePath
77
77
  || process.env.ATRIS_ROUTER_OVERRIDES_PATH
78
- || path.join(os.homedir(), '.atris', 'router-overrides.json');
78
+ || path.join(stateHome(), '.atris', 'router-overrides.json');
79
79
  try {
80
80
  const parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
81
81
  return Array.isArray(parsed) ? parsed : [];
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ // Rule lines every builder brief carries (lib/codex-flight.js buildPrompt and
4
+ // lib/fleet.js buildFleetPrompt). The text lives here once so the two briefs
5
+ // cannot drift apart.
6
+ const SHARED_BRIEF_RULES = [
7
+ '- Keep one concern per PR; split anything larger into separate PRs because git history guides future agents and small PRs are cheap to revert and bisect.',
8
+ '- A guard, limit, or cache must hold on every path to the thing it protects; grep for the other callers, templates, and writers, and test at least one path you were not pointed at.',
9
+ ];
10
+
11
+ module.exports = { SHARED_BRIEF_RULES };
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
+ const { stateHome } = require('./state-home');
4
5
 
5
6
  const SCHEMA = 'atris.chat_scan.v1';
6
7
  const PROMPT_LINE = /^(?:max|pro|fast|code-fast)\s*›\s*(.+)$/i;
@@ -73,7 +74,7 @@ function axLogDirs(cwd = process.cwd(), options = {}) {
73
74
  const local = path.join(cwd, 'atris', 'runs');
74
75
  if (fs.existsSync(local)) dirs.push(local);
75
76
  if (options.global) {
76
- const home = path.join(os.homedir(), '.atris', 'runs');
77
+ const home = path.join(stateHome(), '.atris', 'runs');
77
78
  if (fs.existsSync(home) && home !== local) dirs.push(home);
78
79
  }
79
80
  return dirs;
package/lib/ci-runner.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
+ const { stateHome } = require('./state-home');
5
6
  const os = require('node:os');
6
7
  const https = require('node:https');
7
8
  const { spawn, spawnSync } = require('node:child_process');
@@ -9,7 +10,7 @@ const { spawn, spawnSync } = require('node:child_process');
9
10
  const RUNNER_VERSION = '2.336.0';
10
11
  const GITHUB_API_VERSION = '2026-03-10';
11
12
 
12
- function usageFilePath(home = os.homedir()) {
13
+ function usageFilePath(home = stateHome()) {
13
14
  return path.join(home, '.atris', 'ci', 'usage.jsonl');
14
15
  }
15
16
 
@@ -309,7 +310,7 @@ async function ensureRunner(options = {}) {
309
310
  const version = options.version || RUNNER_VERSION;
310
311
  const platform = options.platform || process.platform;
311
312
  const arch = options.arch || process.arch;
312
- const home = options.home || os.homedir();
313
+ const home = options.home || stateHome();
313
314
  const log = options.log || console.log;
314
315
  const asset = runnerAssetName(platform, arch, version);
315
316
  const runnerPlatform = platform === 'darwin' ? 'osx' : platform;