atris 3.63.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 (65) hide show
  1. package/atris/skills/commerce/SKILL.md +24 -0
  2. package/atris/skills/x-search/SKILL.md +3 -1
  3. package/ax +4 -3
  4. package/bin/atris.js +11 -2
  5. package/cli/atris_code.py +25 -1
  6. package/commands/auth.js +79 -10
  7. package/commands/autoland.js +11 -1
  8. package/commands/browse.js +2 -2
  9. package/commands/business.js +3 -3
  10. package/commands/buy.js +3 -3
  11. package/commands/deck.js +2 -2
  12. package/commands/engine.js +122 -13
  13. package/commands/feedback.js +2 -2
  14. package/commands/fork.js +2 -2
  15. package/commands/gm.js +2 -1
  16. package/commands/improve.js +142 -8
  17. package/commands/integrations.js +4 -3
  18. package/commands/loops.js +2 -1
  19. package/commands/member.js +47 -9
  20. package/commands/mission.js +1 -1
  21. package/commands/pack-craft.js +2 -2
  22. package/commands/publish.js +2 -2
  23. package/commands/radar.js +3 -2
  24. package/commands/router.js +2 -2
  25. package/commands/task.js +84 -2
  26. package/commands/transactions.js +3 -3
  27. package/commands/truth.js +2 -2
  28. package/commands/x-search.js +81 -10
  29. package/commands/youtube.js +2 -1
  30. package/lib/autoland.js +3 -2
  31. package/lib/ax-auto-lane.js +2 -2
  32. package/lib/brief-rules.js +11 -0
  33. package/lib/chat-log-scan.js +2 -1
  34. package/lib/ci-runner.js +3 -2
  35. package/lib/codex-flight.js +2 -1
  36. package/lib/commerce.js +17 -2
  37. package/lib/deck-history.js +2 -2
  38. package/lib/deck-review.js +3 -3
  39. package/lib/design-api.js +2 -2
  40. package/lib/engine-registry.js +85 -18
  41. package/lib/first-minute.js +2 -2
  42. package/lib/fleet.js +8 -36
  43. package/lib/git-spawn.js +5 -2
  44. package/lib/ignored-cache.js +51 -0
  45. package/lib/manifest.js +2 -2
  46. package/lib/map-refs.js +70 -4
  47. package/lib/pack-capabilities.js +2 -1
  48. package/lib/permission-grants.js +2 -2
  49. package/lib/pulse.js +5 -5
  50. package/lib/roster-runs.js +13 -2
  51. package/lib/runner-command.js +22 -6
  52. package/lib/sandbox-pids.js +113 -0
  53. package/lib/secret-gateway.js +3 -13
  54. package/lib/state-home.js +106 -0
  55. package/lib/task-db.js +4 -3
  56. package/lib/task-prune.js +462 -0
  57. package/lib/x-post-check.js +497 -0
  58. package/package.json +1 -1
  59. package/scripts/det/data/xsearch-answer.json +24 -0
  60. package/scripts/det/data/xsearch-live-2026-09-29.json +93 -0
  61. package/scripts/det/xsearch-bench.js +307 -0
  62. package/scripts/det/ytnotes +61 -15
  63. package/scripts/det/ytrail-bench.js +2 -1
  64. package/utils/auth.js +3 -3
  65. package/utils/update-check.js +2 -2
@@ -5443,7 +5443,12 @@ function isWikiMinerNoiseName(value) {
5443
5443
  const name = compactSentence(value || '', 120);
5444
5444
  if (!name) return true;
5445
5445
  if (/^[A-Z]{2,}$/.test(name)) return false;
5446
- return /^(?:i|me|my|mine|myself|we|us|our|ours|ourselves|you|your|yours|yourself|yourselves|he|him|his|himself|she|her|hers|herself|it|its|itself|they|them|their|theirs|themselves|this|that|these|those|the|wiki|mission)$/i.test(name);
5446
+ return /^(?:i|me|my|mine|myself|we|us|our|ours|ourselves|you|your|yours|yourself|yourselves|he|him|his|himself|she|her|hers|herself|it|its|itself|they|them|their|theirs|themselves|this|that|these|those|here|there|where|when|what|who|whom|which|why|how|everything|nothing|something|anything|everyone|anyone|someone|nobody|everybody|anybody|somebody|none|the|wiki|mission)$/i.test(name);
5447
+ }
5448
+
5449
+ function isWikiMinerSelfLoop(relationship) {
5450
+ const from = lowerCompact(relationship?.from);
5451
+ return from !== '' && from === lowerCompact(relationship?.to);
5447
5452
  }
5448
5453
 
5449
5454
  function normalizeWikiMinerExtraction(raw) {
@@ -5462,7 +5467,7 @@ function normalizeWikiMinerExtraction(raw) {
5462
5467
  to: compactSentence(relationship?.to || '', 120),
5463
5468
  type: allowedRelationTypes.has(String(relationship?.type || '').toLowerCase()) ? String(relationship.type).toLowerCase() : 'uses',
5464
5469
  }))
5465
- .filter((relationship) => !isWikiMinerNoiseName(relationship.from) && !isWikiMinerNoiseName(relationship.to));
5470
+ .filter((relationship) => !isWikiMinerNoiseName(relationship.from) && !isWikiMinerNoiseName(relationship.to) && !isWikiMinerSelfLoop(relationship));
5466
5471
  return { entities, relationships };
5467
5472
  }
5468
5473
 
@@ -5484,7 +5489,9 @@ function heuristicWikiMinerExtraction(pagePath, pageContent) {
5484
5489
  }
5485
5490
  const entities = [{ type: 'concept', name: title }];
5486
5491
  for (const name of [...candidates].slice(0, 12)) entities.push({ type: /agent|api|sdk|cli|hub|system/i.test(name) ? 'system' : 'concept', name });
5487
- const relationships = [...candidates].slice(0, 12).map((name) => ({ from: title, to: name, type: 'uses' }));
5492
+ const relationships = [...candidates].slice(0, 12)
5493
+ .map((name) => ({ from: title, to: name, type: 'uses' }))
5494
+ .filter((relationship) => !isWikiMinerSelfLoop(relationship));
5488
5495
  return { entities, relationships };
5489
5496
  }
5490
5497
 
@@ -5538,11 +5545,12 @@ function mergeWikiGraph(graph, extraction, sourcePath) {
5538
5545
 
5539
5546
  const relationshipMap = new Map();
5540
5547
  for (const relationship of graph.relationships || []) {
5541
- if (isWikiMinerNoiseName(relationship.from) || isWikiMinerNoiseName(relationship.to)) continue;
5548
+ if (isWikiMinerNoiseName(relationship.from) || isWikiMinerNoiseName(relationship.to) || isWikiMinerSelfLoop(relationship)) continue;
5542
5549
  const key = `${lowerCompact(relationship.from)}|${String(relationship.type || 'uses').toLowerCase()}|${lowerCompact(relationship.to)}`;
5543
5550
  relationshipMap.set(key, { ...relationship, sources: Array.isArray(relationship.sources) ? relationship.sources : [] });
5544
5551
  }
5545
5552
  for (const relationship of extraction.relationships || []) {
5553
+ if (isWikiMinerNoiseName(relationship.from) || isWikiMinerNoiseName(relationship.to) || isWikiMinerSelfLoop(relationship)) continue;
5546
5554
  const key = `${lowerCompact(relationship.from)}|${relationship.type}|${lowerCompact(relationship.to)}`;
5547
5555
  const existing = relationshipMap.get(key) || { from: relationship.from, to: relationship.to, type: relationship.type, sources: [] };
5548
5556
  if (sourcePath && !existing.sources.includes(sourcePath)) existing.sources.push(sourcePath);
@@ -5819,7 +5827,6 @@ async function callSupervisorLlm(receipts, logs) {
5819
5827
  const output = { isTTY: false, write() { return true; } };
5820
5828
  const result = await postTurn(supervisorPrompt(receipts, logs), {
5821
5829
  mode: process.env.ATRIS_SUPERVISOR_LLM_MODE || 'fast',
5822
- route: 'local',
5823
5830
  cwd: process.cwd(),
5824
5831
  output,
5825
5832
  color: false,
@@ -5905,11 +5912,21 @@ async function runSupervisorWake(name, paths, { execute = false } = {}) {
5905
5912
  analysis = normalizeSupervisorAnalysis(llm.analysis, { status: 'ok', llm_source: llm.source });
5906
5913
  if (!analysis) {
5907
5914
  reason = 'llm_json_parse_failed';
5908
- analysis = emptySupervisorRecommendations({ status: 'parse_error', llm_source: llm.source, llm_error: 'invalid_analysis_shape' });
5915
+ analysis = {
5916
+ ...fallbackSupervisorAnalysis(receipts),
5917
+ status: 'parse_error',
5918
+ llm_source: llm.source,
5919
+ llm_error: 'invalid_analysis_shape',
5920
+ };
5909
5921
  }
5910
5922
  } else if (llm?.error) {
5911
5923
  reason = llm.error === 'invalid_json' ? 'llm_json_parse_failed' : 'llm_analysis_failed';
5912
- analysis = emptySupervisorRecommendations({ status: llm.error === 'invalid_json' ? 'parse_error' : 'llm_error', llm_source: llm.source, llm_error: llm.error });
5924
+ analysis = {
5925
+ ...fallbackSupervisorAnalysis(receipts),
5926
+ status: llm.error === 'invalid_json' ? 'parse_error' : 'llm_error',
5927
+ llm_source: llm.source,
5928
+ llm_error: llm.error,
5929
+ };
5913
5930
  } else {
5914
5931
  reason = 'llm_not_configured';
5915
5932
  analysis = fallbackSupervisorAnalysis(receipts);
@@ -7351,7 +7368,7 @@ async function runGeneralistWake(name, paths, { execute = false, domainInput = {
7351
7368
  record: domain.ok && proof.status === 'ok',
7352
7369
  libraryBefore: crossDomainLibraryBefore,
7353
7370
  receiptPath,
7354
- proofPath,
7371
+ proofPath: execute ? proofPath : null,
7355
7372
  });
7356
7373
  proof.learning = {
7357
7374
  library_path: path.relative(root, learning.library_path),
@@ -7782,6 +7799,24 @@ function wakeRuleDirtyMemberScope(ctx) {
7782
7799
  };
7783
7800
  }
7784
7801
 
7802
+ // Rule: a goal pinned to a terminal mission must not mint another doomed tick.
7803
+ // The goal stays active until a human retires it or revives the mission, so the
7804
+ // wake surfaces that decision instead of proposing `member tick --goal` on a
7805
+ // dead spine. Unknown mission ids pass through: they may live in a worktree.
7806
+ function wakeRuleTerminalMissionGoal(ctx) {
7807
+ const mission = ctx.goalTerminalMission;
7808
+ if (!ctx.goal || !mission) return null;
7809
+ return {
7810
+ decision: 'ask',
7811
+ reason: `goal_mission_terminal:${mission.status}`,
7812
+ needs_user: true,
7813
+ ask: `Goal "${ctx.goal.title}" is pinned to mission ${mission.id} (${mission.status}). Retire the goal or revive the mission, then wake again.`,
7814
+ next_command: `atris member goal-from-mission ${ctx.name}`,
7815
+ goal: ctx.goal,
7816
+ current_experiment: ctx.current || null,
7817
+ };
7818
+ }
7819
+
7785
7820
  // Rule: nothing blocks the member, so take the safe next bounded step.
7786
7821
  function wakeRuleSafeBoundedTick(ctx) {
7787
7822
  return {
@@ -7802,6 +7837,7 @@ const WAKE_DECISION_RULES = [
7802
7837
  wakeRuleOpenReviewableProposal,
7803
7838
  wakeRuleScoredNonTickCandidate,
7804
7839
  wakeRuleDirtyMemberScope,
7840
+ wakeRuleTerminalMissionGoal,
7805
7841
  wakeRuleSafeBoundedTick,
7806
7842
  ];
7807
7843
 
@@ -7815,6 +7851,8 @@ function wakeDecision(name, paths, { force = false, runtimeKind = memberRuntimeK
7815
7851
  const steering = readSteeringMemory(paths, name);
7816
7852
  const state = loadMemberGoals(name, paths);
7817
7853
  const goal = activeGoal(state);
7854
+ const pinnedMission = goal?.mission_id ? memberRunMissionMap().get(String(goal.mission_id).trim()) || null : null;
7855
+ const goalTerminalMission = pinnedMission && MISSION_TERMINAL_STATUSES.has(lowerCompact(pinnedMission.status || '')) ? pinnedMission : null;
7818
7856
  const current = memberOpenExperiment(state);
7819
7857
  const generalistDecision = wakeRuleGeneralistDomainFile(name, { purpose, steering, state, goal, current });
7820
7858
  if (generalistDecision) return generalistDecision;
@@ -7860,7 +7898,7 @@ function wakeDecision(name, paths, { force = false, runtimeKind = memberRuntimeK
7860
7898
  next_command: wakeScores.selected.next_command,
7861
7899
  } : null;
7862
7900
 
7863
- const ctx = { name, force, purpose, nowFile, steering, state, goal, current, blocked, evidence, workspace, checks, wakeScores };
7901
+ const ctx = { name, force, purpose, nowFile, steering, state, goal, goalTerminalMission, current, blocked, evidence, workspace, checks, wakeScores };
7864
7902
  for (const rule of WAKE_DECISION_RULES) {
7865
7903
  const verdict = rule(ctx);
7866
7904
  if (verdict) return composeWakeDecision(ctx, verdict);
@@ -570,7 +570,7 @@ function applyMissionRunnerProfile(runner) {
570
570
  // it only ever takes an explicit pin (the mission's model or the roster
571
571
  // line's); '' lets the engine ride its own default.
572
572
  // A mission often still carries the claude model it was created with; an
573
- // engine that runs only its own models (codex) drops it.
573
+ // engine that runs only its own models (codex, grok) drops it.
574
574
  const CLAUDE_FAMILY_RUNNER_ENGINES = new Set(['claude', 'fable', 'haiku']);
575
575
 
576
576
  function resolveMissionTickRunnerModel(mission) {
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
- const os = require('os');
5
4
  const path = require('path');
5
+ const { stateHome } = require('../lib/state-home');
6
6
 
7
7
  function slugify(value, fallback = 'atris-pack') {
8
8
  return String(value || fallback)
@@ -69,7 +69,7 @@ function isNonEmptyTarget(targetDir) {
69
69
  function defaultAuthor() {
70
70
  try {
71
71
  const creds = JSON.parse(
72
- fs.readFileSync(path.join(os.homedir(), '.atris', 'credentials.json'), 'utf8')
72
+ fs.readFileSync(path.join(stateHome(), '.atris', 'credentials.json'), 'utf8')
73
73
  );
74
74
  if (creds && typeof creds.email === 'string' && creds.email.trim()) return creds.email.trim();
75
75
  } catch {
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const os = require('os');
3
+ const { stateHome } = require('../lib/state-home');
4
4
  const { loadCredentials } = require('../utils/auth');
5
5
  const { apiRequestJson } = require('../utils/api');
6
6
 
@@ -104,7 +104,7 @@ async function publishAtris() {
104
104
  }
105
105
 
106
106
  // Save local copy
107
- const localDir = path.join(os.homedir(), '.atris', 'templates', name.toLowerCase().replace(/[^a-z0-9-]/g, '-'));
107
+ const localDir = path.join(stateHome(), '.atris', 'templates', name.toLowerCase().replace(/[^a-z0-9-]/g, '-'));
108
108
  fs.mkdirSync(localDir, { recursive: true });
109
109
 
110
110
  for (const f of files) {
package/commands/radar.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
+ const { stateHome } = require('../lib/state-home');
6
7
  const { execFileSync } = require('child_process');
7
8
  const { parseScopeFlag, pathUnderRoot } = require('../lib/cli-scope');
8
9
 
@@ -346,7 +347,7 @@ function loadBusinessCollaboration(root, deps, team = {}, scorecardCount = count
346
347
  const business = readJsonFile(path.join(root, '.atris', 'business.json'), deps, null);
347
348
  const runtime = readJsonFile(path.join(root, '.atris', 'state', 'runtime.json'), deps, null);
348
349
  const sync = readJsonFile(path.join(root, '.atris', 'state', '_sync.json'), deps, null);
349
- const cache = readJsonFile(path.join(deps.homeDir || os.homedir(), '.atris', 'businesses.json'), deps, {}) || {};
350
+ const cache = readJsonFile(path.join(deps.homeDir || stateHome(), '.atris', 'businesses.json'), deps, {}) || {};
350
351
  const slug = business?.slug || sync?.workspace_slug || null;
351
352
  const cacheEntry = slug && cache ? cache[slug] : null;
352
353
  const hasAtris = deps.exists(path.join(root, 'atris'));
@@ -804,7 +805,7 @@ function collectRadar(options = {}) {
804
805
  readlink: options.readlinkSync || fs.readlinkSync,
805
806
  readdir: options.readdirSync || fs.readdirSync,
806
807
  platform: options.platform || os.platform(),
807
- homeDir: options.homeDir || os.homedir(),
808
+ homeDir: options.homeDir || stateHome(),
808
809
  };
809
810
  const nowMs = options.nowMs || Date.now();
810
811
  const tasks = loadTasks(root, deps);
@@ -2,8 +2,8 @@
2
2
 
3
3
  const crypto = require('node:crypto');
4
4
  const fs = require('node:fs');
5
- const os = require('node:os');
6
5
  const path = require('node:path');
6
+ const { stateHome } = require('../lib/state-home');
7
7
  const { spawnSync } = require('node:child_process');
8
8
  const { loadOverrides, pickLane } = require('../lib/ax-auto-lane');
9
9
 
@@ -28,7 +28,7 @@ function readFirstNamedFlag(args, names) {
28
28
  }
29
29
 
30
30
  function resolveRouterPaths(args = [], env = process.env) {
31
- const home = env.HOME || os.homedir();
31
+ const home = stateHome(env);
32
32
  return {
33
33
  picks: path.resolve(readFirstNamedFlag(args, ['--picks', '--picks-path'])
34
34
  || env.ATRIS_ROUTER_PICKS_PATH
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 : []);
@@ -8841,7 +8843,7 @@ function taskAllowsReviewChat(task, { allowCertified = false } = {}) {
8841
8843
  if (allowCertified) return true;
8842
8844
  if (review.agent_certified === true || metadata.agent_certified === true) return false;
8843
8845
  const reviewPassCount = Number(review.agent_review_pass_count || metadata.agent_review_pass_count || 0);
8844
- if (reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES) return false;
8846
+ if (reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES && reviewIntegrity.hasIndependentReview(task)) return false;
8845
8847
  const handoff = reviewHandoffForTask(task, { suppressExistingFollowUp: true });
8846
8848
  return !(handoff && (handoff.next_action === 'continue_work' || handoff.next_action === 'human_accept_waiting'));
8847
8849
  }
@@ -9814,6 +9816,78 @@ function cmdClearDone(args) {
9814
9816
  console.log(`cleared ${candidates.length} completed task(s).`);
9815
9817
  }
9816
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
+
9817
9891
  function cmdReapMissionBlockers(args) {
9818
9892
  const taskDb = getTaskDb();
9819
9893
  const db = taskDb.open();
@@ -10468,6 +10542,13 @@ function stampReadyVerifyMetadata(taskDb, db, taskId, verify) {
10468
10542
  if (!row) return;
10469
10543
  const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
10470
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
+ }
10471
10552
  db.prepare(`
10472
10553
  UPDATE tasks
10473
10554
  SET metadata = ?,
@@ -13374,6 +13455,7 @@ async function runTaskCommand(args) {
13374
13455
  case 'fail': return cmdDone([...rest, '--failed']);
13375
13456
  case 'archive': return cmdArchive(rest);
13376
13457
  case 'clear-done': return cmdClearDone(rest);
13458
+ case 'prune-test-junk': return cmdPruneTestJunk(rest);
13377
13459
  case 'keep': return cmdKeep(rest);
13378
13460
  case 'reap-mission-blockers':
13379
13461
  case 'reap-blockers':
@@ -5,8 +5,8 @@
5
5
 
6
6
  const { apiRequestJson } = require('../utils/api');
7
7
  const { loadCredentials } = require('../utils/auth');
8
- const { NOT_LOGGED_IN, tokenFrom, oneLine, printResult } = require('../lib/developer-api');
9
- const { parseFlags, formatCents, errorFrom } = require('../lib/commerce');
8
+ const { NOT_LOGGED_IN, oneLine, printResult } = require('../lib/developer-api');
9
+ const { parseFlags, formatCents, errorFrom, commerceToken } = require('../lib/commerce');
10
10
 
11
11
  const TX_KINDS = ['ticket', 'shop', 'flight', 'test_purchase', 'fee', 'trade', 'wallet_send'];
12
12
 
@@ -98,7 +98,7 @@ async function transactionsCommand(args = [], deps = {}) {
98
98
 
99
99
  const load = deps.loadCredentials || loadCredentials;
100
100
  const request = deps.apiRequestJson || apiRequestJson;
101
- const token = tokenFrom(load());
101
+ const token = commerceToken(load(), 'transactions:read', deps.now ? deps.now() : Date.now());
102
102
  if (!token) return fail(NOT_LOGGED_IN);
103
103
 
104
104
  try {
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]';
@@ -2256,7 +2257,7 @@ function searchCacheTtlMs(deps = {}) {
2256
2257
 
2257
2258
  function resolveSearchCachePath(deps = {}) {
2258
2259
  if (deps.searchCachePath) return deps.searchCachePath;
2259
- const homeDir = deps.homeDir || os.homedir();
2260
+ const homeDir = deps.homeDir || stateHome();
2260
2261
  return path.join(homeDir, '.atris', LOCAL_SEARCH_CACHE_FILE);
2261
2262
  }
2262
2263
 
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 : [];