atris 3.51.0 → 3.53.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.
package/bin/atris.js CHANGED
@@ -3140,7 +3140,7 @@ function inspectAgentCliWiring() {
3140
3140
  },
3141
3141
  ];
3142
3142
 
3143
- const binaries = ['atris', 'ax', 'claude', 'codex', 'cursor-agent', 'devin', 'droid'].map((name) => ({
3143
+ const binaries = ['atris', 'ax', 'claude', 'codex', 'cursor-agent', 'devin', 'agy'].map((name) => ({
3144
3144
  name,
3145
3145
  path: commandOnPath(name),
3146
3146
  }));
@@ -3319,7 +3319,7 @@ async function chatAtris() {
3319
3319
  for (const arg of rawArgs) {
3320
3320
  if (arg === '--agent') {
3321
3321
  agentLane = true;
3322
- } else if (arg === '--print' || arg === '--headless') {
3322
+ } else if (arg === '--print' || arg === '--headless' || arg === '--rapid' || arg === '--auto-approve') {
3323
3323
  fastLaneFlags.push(arg);
3324
3324
  } else {
3325
3325
  messageArgs.push(arg);
@@ -3332,9 +3332,12 @@ async function chatAtris() {
3332
3332
  console.log('Usage: atris chat ["message"]');
3333
3333
  console.log('');
3334
3334
  console.log(' Chat with Atris 2 Fast in this workspace: tools attached, same turn as `ax --fast`.');
3335
+ console.log(' Pass --rapid to use Atris Rapid instead.');
3335
3336
  console.log(' Requires `atris login`.');
3336
3337
  console.log('');
3337
3338
  console.log(' atris chat Interactive chat (ax --fast --chat)');
3339
+ console.log(' atris chat --rapid Interactive Rapid chat (ax --rapid --chat)');
3340
+ console.log(' atris chat --rapid --auto-approve Rapid chat that auto-approves safe git push');
3338
3341
  console.log(' atris chat "what now?" One-shot message (ax --fast)');
3339
3342
  console.log(' atris chat --print "..." Headless JSON result (ax --fast --print)');
3340
3343
  console.log(' atris chat --agent [...] Legacy cloud-agent lane (needs `atris agent`)');
@@ -3361,9 +3364,16 @@ async function chatAtris() {
3361
3364
  if (!agentLane) {
3362
3365
  try {
3363
3366
  const axPath = path.join(__dirname, '..', 'ax');
3364
- const axArgs = message || fastLaneFlags.length ? ['--fast', ...fastLaneFlags, message].filter(Boolean) : ['--fast', '--chat'];
3367
+ const lane = fastLaneFlags.includes('--rapid') ? '--rapid' : '--fast';
3368
+ const extraFlags = fastLaneFlags.filter((flag) => flag !== '--rapid');
3369
+ const axArgs = message
3370
+ ? [lane, ...extraFlags, message].filter(Boolean)
3371
+ : [lane, ...extraFlags, '--chat'];
3365
3372
  const run = spawnSync(process.execPath, [axPath, ...axArgs], { stdio: 'inherit' });
3366
- process.exit(run.status || 0);
3373
+ // spawnSync reports spawn failure via run.error (status stays null), and
3374
+ // a signal-killed ax also leaves status null; neither is a success.
3375
+ if (!run.error) process.exit(run.status ?? 1);
3376
+ // ax unavailable: fall through to the agent lane.
3367
3377
  } catch {
3368
3378
  // ax unavailable: fall through to the agent lane.
3369
3379
  }
@@ -3632,7 +3642,9 @@ async function atrisFastChat() {
3632
3642
  const axModule = require('../ax');
3633
3643
  if (axModule.resolveRoute(message) === 'local') {
3634
3644
  const run = spawnSync(process.execPath, [path.join(__dirname, '..', 'ax'), '--fast', message], { stdio: 'inherit' });
3635
- process.exit(run.status || 0);
3645
+ // Same rule as the chat lane: spawn failure or a signal kill leaves
3646
+ // status null and must not read as success.
3647
+ if (!run.error) process.exit(run.status ?? 1);
3636
3648
  }
3637
3649
  } catch {
3638
3650
  // ax unavailable: fall through to the plain cloud one-shot.
@@ -2141,7 +2141,7 @@ async function runBusinessPromptViaRunnerProxy(token, ctx, prompt, options = {})
2141
2141
  const payload = {
2142
2142
  prompt,
2143
2143
  permission_mode: 'bypassPermissions',
2144
- max_turns: Math.min(Math.max(Number(options.maxTurns || 12), 1), 25),
2144
+ max_turns: Math.min(Math.max(Number(options.maxTurns || 40), 1), 100),
2145
2145
  reset_context: Boolean(options.resetContext),
2146
2146
  };
2147
2147
  if (options.worker) payload.worker = options.worker;
@@ -3399,7 +3399,7 @@ async function sendBusinessChat(token, ctx, message, sessionId, resetContext = f
3399
3399
  const fallback = await runBusinessPromptViaRunnerProxy(token, ctx, message, {
3400
3400
  ...options,
3401
3401
  resetContext,
3402
- maxTurns: 25,
3402
+ maxTurns: 60,
3403
3403
  });
3404
3404
  if (!fallback.ok) {
3405
3405
  if (typeof options.onFailure === 'function') {
@@ -53,6 +53,7 @@ const {
53
53
  runsPruneLines,
54
54
  formatBytes,
55
55
  } = require('../lib/runs-prune');
56
+ const { compactMissionLedger } = require('../lib/mission-ledger-compact');
56
57
  const autolandLib = require('../lib/autoland');
57
58
  const { gateForHuman, landingWhyClause } = require('../lib/voice-gate');
58
59
  const { operatorReady, hasAgentJargon } = require('./autoland');
@@ -1347,6 +1348,11 @@ function saveMission(mission, root = process.cwd(), eventType = 'mission_updated
1347
1348
  updated_at: stampIso(),
1348
1349
  });
1349
1350
  appendJsonLine(paths.missionsJsonl, next);
1351
+ // Self-maintenance: once history outweighs live missions 4:1 the ledger is
1352
+ // rewritten to one row per mission (latest state, first display number —
1353
+ // exactly what loadMissionMap reconstructs). Every status/tick path parses
1354
+ // this file several times, so unbounded snapshot history taxes everything.
1355
+ try { compactMissionLedger(paths.missionsJsonl); } catch {}
1350
1356
  const event = appendEvent(eventType, next, payload, root);
1351
1357
  renderMissionStatus(root);
1352
1358
  renderMemberMissionState(next.owner, root);
@@ -4810,27 +4816,60 @@ function missionTickVerification(tick) {
4810
4816
  return { verified: false, state: ran ? 'unchecked' : 'skipped', unchecked: ran };
4811
4817
  }
4812
4818
 
4813
- // Roll every ran tick for a mission into a verification-debt tally. `unchecked`
4814
- // counts ran ticks that recorded no verifier result at all — the "tick recorded
4815
- // but nothing was checked" red the report and now.md rollup surface and count
4816
- // against the mission.
4817
- function missionVerificationDebt(mission, root = process.cwd()) {
4818
- const paths = statePaths(root);
4819
+ // Per-process index of mission run receipts, keyed by runs dir. Reading and
4820
+ // parsing ~1400 receipt files per mission made `mission list` scale as
4821
+ // O(missions × runs-files); building the mission_id receipts map once per
4822
+ // invocation keeps outputs identical while scanning the dir a single time.
4823
+ // The dir mtime guards the memo so a receipt written mid-process (new file in
4824
+ // the dir) invalidates it; malformed receipts stay silently skipped, matching
4825
+ // readJson's existing tolerance.
4826
+ const missionRunsIndexCache = new Map();
4827
+
4828
+ function missionRunsReceiptIndex(root = process.cwd()) {
4829
+ const runsDir = statePaths(root).runsDir;
4830
+ let dirMtime = -1;
4831
+ try {
4832
+ dirMtime = fs.statSync(runsDir).mtimeMs;
4833
+ } catch {
4834
+ dirMtime = -1;
4835
+ }
4836
+ const cached = missionRunsIndexCache.get(runsDir);
4837
+ if (cached && cached.dirMtime === dirMtime) return cached.byMission;
4838
+
4819
4839
  let files = [];
4820
4840
  try {
4821
- files = fs.readdirSync(paths.runsDir)
4841
+ files = fs.readdirSync(runsDir)
4822
4842
  .filter((file) => file.startsWith('mission-') && file.endsWith('.json'))
4823
- .map((file) => path.join(paths.runsDir, file));
4843
+ .map((file) => path.join(runsDir, file));
4824
4844
  } catch {
4825
4845
  files = [];
4826
4846
  }
4847
+ const byMission = new Map();
4848
+ for (const file of files) {
4849
+ const receipt = readJson(file);
4850
+ if (!receipt || !receipt.mission_id) continue;
4851
+ let bucket = byMission.get(receipt.mission_id);
4852
+ if (!bucket) {
4853
+ bucket = [];
4854
+ byMission.set(receipt.mission_id, bucket);
4855
+ }
4856
+ bucket.push({ file, receipt });
4857
+ }
4858
+ missionRunsIndexCache.set(runsDir, { dirMtime, byMission });
4859
+ return byMission;
4860
+ }
4861
+
4862
+ // Roll every ran tick for a mission into a verification-debt tally. `unchecked`
4863
+ // counts ran ticks that recorded no verifier result at all — the "tick recorded
4864
+ // but nothing was checked" red the report and now.md rollup surface and count
4865
+ // against the mission.
4866
+ function missionVerificationDebt(mission, root = process.cwd()) {
4867
+ const entries = missionRunsReceiptIndex(root).get(mission.id) || [];
4827
4868
  let ran = 0;
4828
4869
  let verified = 0;
4829
4870
  let unchecked = 0;
4830
4871
  const seen = new Set();
4831
- for (const file of files) {
4832
- const receipt = readJson(file);
4833
- if (!receipt || receipt.mission_id !== mission.id) continue;
4872
+ for (const { receipt } of entries) {
4834
4873
  for (const tick of missionReceiptTicks(receipt)) {
4835
4874
  if (!tick || tick.status !== 'ran') continue;
4836
4875
  const key = `${tick.tick_index || ''}:${tick.finished_at || tick.started_at || ''}`;
@@ -4853,21 +4892,10 @@ function missionVerificationDebtLine(debt) {
4853
4892
  }
4854
4893
 
4855
4894
  function missionReportTimeline(mission, root = process.cwd(), limit = 6) {
4856
- const paths = statePaths(root);
4857
- let files = [];
4858
- try {
4859
- files = fs.readdirSync(paths.runsDir)
4860
- .filter((file) => file.startsWith('mission-') && file.endsWith('.json'))
4861
- .map((file) => path.join(paths.runsDir, file));
4862
- } catch {
4863
- files = [];
4864
- }
4865
-
4895
+ const entries = missionRunsReceiptIndex(root).get(mission.id) || [];
4866
4896
  const items = [];
4867
4897
  const seen = new Set();
4868
- for (const file of files) {
4869
- const receipt = readJson(file);
4870
- if (!receipt || receipt.mission_id !== mission.id) continue;
4898
+ for (const { file, receipt } of entries) {
4871
4899
  const receiptPath = path.relative(root, file);
4872
4900
  for (const tick of missionReceiptTicks(receipt)) {
4873
4901
  const summary = missionTickReportSummary(tick);
package/commands/now.js CHANGED
@@ -270,7 +270,13 @@ function taskReceiptTitle(row) {
270
270
  // one per receipt, landed result + human proof state. Shares the collector
271
271
  // with the count so the number and the visible list always match.
272
272
  function todayTaskReceiptLines(root = process.cwd(), date = new Date(), limit = 6) {
273
- return collectTaskReceiptsToday(root, date)
273
+ return formatTaskReceiptLines(collectTaskReceiptsToday(root, date), limit);
274
+ }
275
+
276
+ // Format an already-collected receipt list, so a caller that needs both the
277
+ // count and the lines (renderDefaultNow) parses the receipt files only once.
278
+ function formatTaskReceiptLines(receipts, limit = 6) {
279
+ return receipts
274
280
  .slice(-limit)
275
281
  .reverse()
276
282
  .map((r) => {
@@ -411,7 +417,8 @@ function renderDefaultNow(root = process.cwd()) {
411
417
  const journalPath = currentJournalPath(root);
412
418
  const openTodoCount = countOpenWorkItems(root, todoPath);
413
419
  const inboxCount = countMatches(journalPath, /^-\s+\*\*I\d+:/gm);
414
- const taskReceiptCount = countTaskReceiptsToday(root);
420
+ const taskReceipts = collectTaskReceiptsToday(root);
421
+ const taskReceiptCount = taskReceipts.length;
415
422
  const missionReceiptCount = countMissionReceiptsToday(root);
416
423
  const completedCount = taskReceiptCount + missionReceiptCount || countJournalCompletedReceipts(journalPath);
417
424
  const generated = formatLocalTimestamp();
@@ -419,7 +426,7 @@ function renderDefaultNow(root = process.cwd()) {
419
426
  const whatMattersNow = moveLine
420
427
  ? `${moveLine}\n\n- Run the named next action and leave proof before choosing another.`
421
428
  : '- Decide the next useful move before opening more context.';
422
- const receiptLines = todayTaskReceiptLines(root);
429
+ const receiptLines = formatTaskReceiptLines(taskReceipts);
423
430
  const missionReceiptLines = todayMissionReceiptLines(root);
424
431
  const commitLines = landedCommitLines(root);
425
432
  const hiddenReceiptCount = Math.max(0, completedCount - missionReceiptLines.length - receiptLines.length);
package/commands/radar.js CHANGED
@@ -54,23 +54,39 @@ function agentTypeForCommand(command) {
54
54
  if (/(^|\s|\/)claude(\s|$)/.test(cmd) && !/Claude\.app/.test(cmd)) return 'claude';
55
55
  if (/(^|\s|\/)opencode(\s|$)/.test(cmd)) return 'opencode';
56
56
  if (/(^|\s|\/)devin(\s|$)/.test(cmd)) return 'devin';
57
+ if (/(^|\s|\/)agy(\s|$)/.test(cmd)) return 'agy';
57
58
  if (/(^|\s|\/)droid(\s|$)/.test(cmd)) return 'droid';
58
59
  return null;
59
60
  }
60
61
 
61
- function processCwd(pid, deps) {
62
+ // Resolve cwd for many pids at once: one lsof call on darwin instead of one
63
+ // per process. Returns a Map of pid (string) -> cwd; missing pids stay absent.
64
+ function processCwds(pids, deps) {
62
65
  const { platform, execFile } = deps;
63
- try {
64
- if (platform === 'linux') {
65
- return deps.readlink(`/proc/${pid}/cwd`);
66
+ const byPid = new Map();
67
+ if (!pids.length) return byPid;
68
+ if (platform === 'linux') {
69
+ for (const pid of pids) {
70
+ try { byPid.set(String(pid), deps.readlink(`/proc/${pid}/cwd`)); } catch {}
71
+ }
72
+ return byPid;
73
+ }
74
+ if (platform === 'darwin') {
75
+ let out = '';
76
+ try {
77
+ out = execFile('lsof', ['-a', '-d', 'cwd', '-Fn', '-p', pids.map(String).join(',')], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
78
+ } catch (error) {
79
+ // lsof exits non-zero when any pid in the batch is gone; keep the rows it
80
+ // did print instead of losing every cwd.
81
+ out = error && error.stdout ? error.stdout : '';
66
82
  }
67
- if (platform === 'darwin') {
68
- const out = execFile('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
69
- const line = String(out).split(/\r?\n/).find(value => value.startsWith('n'));
70
- return line ? line.slice(1) : '';
83
+ let currentPid = '';
84
+ for (const line of String(out).split(/\r?\n/)) {
85
+ if (line.startsWith('p')) currentPid = line.slice(1).trim();
86
+ else if (line.startsWith('n') && currentPid) byPid.set(currentPid, line.slice(1));
71
87
  }
72
- } catch {}
73
- return '';
88
+ }
89
+ return byPid;
74
90
  }
75
91
 
76
92
  function gitBranch(cwd, execFile) {
@@ -93,22 +109,28 @@ function collectAgents(deps) {
93
109
  .map(row => ({ ...row, agent: agentTypeForCommand(row.command) }))
94
110
  .filter(row => row.agent);
95
111
  const parentPids = new Set(agentRows.map(row => String(row.ppid || '')).filter(Boolean));
96
- return agentRows
97
- .filter(row => !parentPids.has(String(row.pid)))
98
- .map(row => {
99
- const cwd = processCwd(row.pid, deps);
100
- return {
101
- pid: row.pid,
102
- agent: row.agent,
103
- command: row.command,
104
- status: row.stat.includes('Z') ? 'zombie' : row.stat.includes('T') ? 'stopped' : 'active',
105
- cwd,
106
- repo: repoLabel(cwd),
107
- branch: gitBranch(cwd, deps.execFile),
108
- cpu: row.cpu,
109
- mem: row.mem,
110
- };
111
- });
112
+ const topRows = agentRows.filter(row => !parentPids.has(String(row.pid)));
113
+ const cwdByPid = processCwds(topRows.map(row => row.pid), deps);
114
+ const branchByCwd = new Map();
115
+ const branchFor = (cwd) => {
116
+ if (!cwd) return '';
117
+ if (!branchByCwd.has(cwd)) branchByCwd.set(cwd, gitBranch(cwd, deps.execFile));
118
+ return branchByCwd.get(cwd);
119
+ };
120
+ return topRows.map(row => {
121
+ const cwd = cwdByPid.get(String(row.pid)) || '';
122
+ return {
123
+ pid: row.pid,
124
+ agent: row.agent,
125
+ command: row.command,
126
+ status: row.stat.includes('Z') ? 'zombie' : row.stat.includes('T') ? 'stopped' : 'active',
127
+ cwd,
128
+ repo: repoLabel(cwd),
129
+ branch: branchFor(cwd),
130
+ cpu: row.cpu,
131
+ mem: row.mem,
132
+ };
133
+ });
112
134
  }
113
135
 
114
136
  function loadTasks(root, deps) {
@@ -287,8 +309,11 @@ function loadTeam(root, deps) {
287
309
  };
288
310
  }
289
311
 
290
- function loadBrain(root, deps) {
291
- const scorecards = readJsonLines(path.join(root, '.atris', 'state', 'scorecards.jsonl'), deps.readFile, deps.exists);
312
+ function loadScorecards(root, deps) {
313
+ return readJsonLines(path.join(root, '.atris', 'state', 'scorecards.jsonl'), deps.readFile, deps.exists);
314
+ }
315
+
316
+ function loadBrain(root, deps, scorecards = loadScorecards(root, deps)) {
292
317
  const operatorDir = path.join(root, '.atris', 'state', 'operator-scorecards');
293
318
  let operatorScorecards = 0;
294
319
  try {
@@ -316,7 +341,7 @@ function countDirectoryEntries(dir, deps, predicate = () => true) {
316
341
  return listNames(dir, deps).filter(predicate).length;
317
342
  }
318
343
 
319
- function loadBusinessCollaboration(root, deps, team = {}) {
344
+ function loadBusinessCollaboration(root, deps, team = {}, scorecardCount = countJsonLines(path.join(root, '.atris', 'state', 'scorecards.jsonl'), deps)) {
320
345
  const business = readJsonFile(path.join(root, '.atris', 'business.json'), deps, null);
321
346
  const runtime = readJsonFile(path.join(root, '.atris', 'state', 'runtime.json'), deps, null);
322
347
  const sync = readJsonFile(path.join(root, '.atris', 'state', '_sync.json'), deps, null);
@@ -335,7 +360,7 @@ function loadBusinessCollaboration(root, deps, team = {}) {
335
360
  const localReceipts = countDirectoryEntries(path.join(root, '.atris', 'receipts'), deps, name => /\.(json|md|txt)$/i.test(name));
336
361
  const events = countJsonLines(path.join(root, '.atris', 'state', 'events.jsonl'), deps);
337
362
  const episodes = countJsonLines(path.join(root, '.atris', 'state', 'episodes.jsonl'), deps);
338
- const scorecards = countJsonLines(path.join(root, '.atris', 'state', 'scorecards.jsonl'), deps);
363
+ const scorecards = scorecardCount;
339
364
  const computerDirs = countDirectoryEntries(path.join(root, 'atris', 'computers'), deps, name => !name.startsWith('.'));
340
365
  const hasOnboarding = ingestPacks > 0 || starterBriefs > 0 || onePagers > 0;
341
366
  const hasProofLoop = events > 0 || episodes > 0 || scorecards > 0 || localReceipts > 0;
@@ -786,12 +811,18 @@ function collectRadar(options = {}) {
786
811
  const missions = loadMissions(root, deps, nowMs);
787
812
  identityCache.set(root, { missions, missionLocks: loadMissionLocks(path.join(root, '.atris', 'state'), deps), memberLocks: loadMemberLoopLocks(path.join(root, '.atris', 'state'), deps) });
788
813
  const worktrees = loadWorktrees(root, deps);
814
+ const branchIdentityCache = new Map();
815
+ const branchIdentityFor = (cwd) => {
816
+ if (!cwd) return null;
817
+ if (!branchIdentityCache.has(cwd)) branchIdentityCache.set(cwd, branchIdentityAtCwd(cwd, deps.execFile));
818
+ return branchIdentityCache.get(cwd);
819
+ };
789
820
  const agents = collectAgents(deps).map(agent => {
790
821
  const taskWorkspaceRoot = findTaskWorkspaceRoot(agent.cwd, deps);
791
822
  const agentTasks = taskWorkspaceRoot ? loadTasksCached(taskWorkspaceRoot, deps, taskCache) : [];
792
823
  const identity = taskWorkspaceRoot ? loadWorkspaceIdentity(taskWorkspaceRoot, deps, identityCache, nowMs) : { missions: [], missionLocks: [], memberLocks: [] };
793
824
  const sidecar = loadAgentWorktreeSidecar(agent.cwd, deps);
794
- const branchIdentity = branchIdentityAtCwd(agent.cwd, deps.execFile);
825
+ const branchIdentity = branchIdentityFor(agent.cwd);
795
826
  const sessionId = parseSessionIdFromCommand(agent.command);
796
827
  const interactive = agent.agent === 'claude' && isInteractiveClaudeCommand(agent.command);
797
828
  const resolved = resolveAgentTaskBinding({
@@ -823,14 +854,15 @@ function collectRadar(options = {}) {
823
854
  task_action: task ? taskSessionAction(agent, task, taskWorkspaceRoot) : untaskedAction(agent, taskWorkspaceRoot, agentTasks, resolved),
824
855
  };
825
856
  });
857
+ const scorecards = loadScorecards(root, deps);
826
858
  const osState = {
827
859
  xp: loadXp(root, deps),
828
860
  team: loadTeam(root, deps),
829
- brain: loadBrain(root, deps),
861
+ brain: loadBrain(root, deps, scorecards),
830
862
  swarlo: loadSwarlo(tasks),
831
863
  loop: loadLoop(missions, root, deps),
832
864
  };
833
- osState.business = loadBusinessCollaboration(root, deps, osState.team);
865
+ osState.business = loadBusinessCollaboration(root, deps, osState.team, scorecards.length);
834
866
  return { root, generated_at: new Date(nowMs).toISOString(), summary: summarize(tasks, missions, worktrees, agents), os: osState, next_action: nextAction(tasks, missions, worktrees, agents, osState), agents, tasks, missions, worktrees };
835
867
  }
836
868