atris 3.15.48 → 3.15.50

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.
@@ -12,6 +12,7 @@
12
12
  - [[atris/wiki/concepts/intent-capability-composition.md]] — the operating loop; roadmap from gaps
13
13
  - [[atris/wiki/concepts/wiki-as-memory-substrate.md]] — what `atris/wiki/` is and isn't
14
14
  - [[atris/wiki/concepts/plan-do-review-loop.md]] — core Atris workflow and how local memory fits into it
15
+ - [[atris/wiki/concepts/rebased-pack-co-first-loop.md]] — local-only business workspace first loop and proof guardrails
15
16
  - [[atris/wiki/concepts/atris-labs-goals.md]] — atris-labs north star, 2026 Q2 targets, standing constraints
16
17
  - [[atris/wiki/concepts/horizon-types.md]] — horizon slug prefix convention; type categories and inference rules
17
18
  - [[atris/wiki/concepts/verifiable-reward-loop.md]] — reward, scorecards, and why the repo now acts like an RL-style environment
@@ -21,6 +22,7 @@
21
22
 
22
23
  ## Briefs
23
24
 
25
+ - [[atris/wiki/briefs/rebased-pack-co-starter-brief.md]] — starter brief for the local-only Rebased Pack Co smoke workspace
24
26
  - [[atris/wiki/briefs/atris-cli-overview.md]] — summary of CLI, owner/computer model, workspace layers, and why `atris/wiki/` exists
25
27
  - [[atris/wiki/briefs/atris-labs-workspace-protocol.md]] — atris-labs workspace protocol: on-load sequence, layout, surfaces, north star
26
28
  - [[atris/wiki/briefs/atrisos-generative-ui-product-surface.md]] — historical AtrisOS generative UI / block surface design note
package/bin/atris.js CHANGED
@@ -334,6 +334,7 @@ function showHelp() {
334
334
  console.log(' now - Show atris/now.md, the current operating truth');
335
335
  console.log(' activate - Load Atris context');
336
336
  console.log(' radar - Show live agents joined with tasks, missions, and worktrees');
337
+ console.log(' ctop - Show a process-first live agent CPU/memory view');
337
338
  console.log(' status - See local work and completions (`atris status <business>` for remote)');
338
339
  console.log(' xp - Show Career XP and contribution graph');
339
340
  console.log(' analytics - Show recent productivity from journals');
@@ -765,7 +766,7 @@ if (command === '2' && ['fast', 'pro'].includes(String(firstCommandArg || '').to
765
766
  }
766
767
 
767
768
  // Check if this is a known command or natural language input
768
- const knownCommands = ['init', 'log', 'now', 'radar', 'status', 'analytics', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
769
+ const knownCommands = ['init', 'log', 'now', 'radar', 'ctop', 'status', 'analytics', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
769
770
  'activate', '_activate', 'agent', 'chat', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
770
771
  'clean', 'verify', 'search', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'plugin', 'experiments', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync',
771
772
  'ingest', 'query', 'lint', 'loop', 'task', 'mission', 'worktree', 'aeo', 'xp', 'play', 'gm', 'x',
@@ -1182,8 +1183,9 @@ if (command === 'init') {
1182
1183
  Promise.resolve(require('../commands/worktree').worktreeCommand(process.argv.slice(3)))
1183
1184
  .then((code) => process.exit(code || 0))
1184
1185
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
1185
- } else if (command === 'radar') {
1186
- Promise.resolve(require('../commands/radar').radarCommand(process.argv.slice(3)))
1186
+ } else if (command === 'radar' || command === 'ctop') {
1187
+ const radarArgs = command === 'ctop' ? ['--agents', ...process.argv.slice(3)] : process.argv.slice(3);
1188
+ Promise.resolve(require('../commands/radar').radarCommand(radarArgs))
1187
1189
  .then((code) => process.exit(code || 0))
1188
1190
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
1189
1191
  } else if (command === 'codex-goal') {
package/commands/brain.js CHANGED
@@ -183,7 +183,7 @@ function countTodoItems(todoText) {
183
183
  if (!isTitled) continue;
184
184
 
185
185
  titled += 1;
186
- if (hasRenderedSections && ['Backlog', 'In Progress', 'Blocked'].includes(section)) renderedOpen += 1;
186
+ if (hasRenderedSections && ['Backlog', 'In Progress'].includes(section)) renderedOpen += 1;
187
187
  if (hasRenderedSections && section === 'Completed') renderedDone += 1;
188
188
  }
189
189
 
@@ -195,6 +195,70 @@ function countTodoItems(todoText) {
195
195
  };
196
196
  }
197
197
 
198
+ const EXECUTABLE_TASK_STATUSES = new Set(['open', 'claimed']);
199
+ const COMPLETED_TASK_STATUSES = new Set(['done', 'completed', 'accepted']);
200
+
201
+ function readTaskProjectionTasks(root) {
202
+ const projection = readJson(path.join(root, '.atris', 'state', 'tasks.projection.json'));
203
+ const tasks = Array.isArray(projection?.tasks) ? projection.tasks : null;
204
+ return tasks || null;
205
+ }
206
+
207
+ function isCertifiedReviewTask(task) {
208
+ if (String(task?.status || '').toLowerCase() !== 'review') return false;
209
+ const metadata = task.metadata || {};
210
+ const review = task.review || {};
211
+ const passCount = Number(metadata.agent_review_pass_count || review.agent_review_pass_count || 0);
212
+ return Boolean(metadata.agent_certified || review.agent_certified || passCount >= 2);
213
+ }
214
+
215
+ function summarizeTaskProjection(root) {
216
+ const tasks = readTaskProjectionTasks(root);
217
+ if (!tasks) return null;
218
+
219
+ const counts = {};
220
+ const certifiedReviewTasks = [];
221
+ for (const task of tasks) {
222
+ const status = String(task?.status || '').toLowerCase();
223
+ counts[status] = (counts[status] || 0) + 1;
224
+ if (isCertifiedReviewTask(task)) {
225
+ certifiedReviewTasks.push({
226
+ ref: task.display_id || task.legacy_ref || task.id,
227
+ title: task.title || 'Untitled task',
228
+ });
229
+ }
230
+ }
231
+
232
+ return {
233
+ tasks,
234
+ counts,
235
+ certifiedReviewTasks,
236
+ };
237
+ }
238
+
239
+ function countTaskProjectionItems(root) {
240
+ const summary = summarizeTaskProjection(root);
241
+ if (!summary) return null;
242
+
243
+ let open = 0;
244
+ let done = 0;
245
+ for (const [status, count] of Object.entries(summary.counts)) {
246
+ if (EXECUTABLE_TASK_STATUSES.has(status)) open += count;
247
+ if (COMPLETED_TASK_STATUSES.has(status)) done += count;
248
+ }
249
+
250
+ return {
251
+ open,
252
+ checked: done,
253
+ titled: summary.tasks.length,
254
+ done,
255
+ };
256
+ }
257
+
258
+ function countWorkItems(root, todoText) {
259
+ return countTaskProjectionItems(root) || countTodoItems(todoText);
260
+ }
261
+
198
262
  function listMarkdown(root, relDir, limit = 12) {
199
263
  const dir = path.join(root, relDir);
200
264
  if (!fs.existsSync(dir)) return [];
@@ -271,7 +335,8 @@ function collectState(root) {
271
335
  name: business.name || business.slug || firstHeading(status || mapText, path.basename(root)),
272
336
  slug: business.slug || path.basename(root),
273
337
  business,
274
- todo: countTodoItems(todoText),
338
+ todo: countWorkItems(root, todoText),
339
+ taskProjection: summarizeTaskProjection(root),
275
340
  hasNow: nowText.length > 0,
276
341
  nowHeading: firstHeading(nowText, null),
277
342
  hasMap: mapText.length > 0,
@@ -292,7 +357,7 @@ function collectState(root) {
292
357
  }
293
358
 
294
359
  function prepareBrainState(root) {
295
- refreshNowFile(root);
360
+ refreshNowFile(root, { preserveCustom: true });
296
361
  return collectState(root);
297
362
  }
298
363
 
@@ -482,6 +547,12 @@ function memberNextMove(member, state = null) {
482
547
  const name = member.name || member.slug;
483
548
  const context = `${member.startHere}\n${member.goals}`;
484
549
  const identity = `${member.slug}\n${member.name}`;
550
+ const certifiedReview = state?.taskProjection?.certifiedReviewTasks?.[0] || null;
551
+ const certifiedReviewMove = certifiedReview
552
+ ? `${name}: hand off certified review ${certifiedReview.ref} to the operator: run ` +
553
+ `\`atris task accept ${certifiedReview.ref}\` if approved or ` +
554
+ `\`atris task revise ${certifiedReview.ref} --note "<what must change>"\` if not; do not create new work until this checkpoint is clear.`
555
+ : null;
485
556
  if (member.slug === 'justin' || /justin/i.test(member.name || '')) {
486
557
  return `${name}: run one customer-moving GTM rep, update the relevant workspace state within 10 minutes, and leave a scorecard.`;
487
558
  }
@@ -496,6 +567,7 @@ function memberNextMove(member, state = null) {
496
567
  return `${name}: choose or create one bounded mission step, run its verifier, and close it with proof, a scorecard, and the next move.`;
497
568
  }
498
569
  if (/validator|reviewer/i.test(identity)) {
570
+ if (certifiedReviewMove) return certifiedReviewMove;
499
571
  if ((state?.todo?.open || 0) === 0 && (state?.todo?.done || 0) === 0) {
500
572
  return `${name}: wait for one concrete artifact or ask Navigator to create a reviewable task with verifier, proof target, and residual-risk checklist.`;
501
573
  }
@@ -503,6 +575,7 @@ function memberNextMove(member, state = null) {
503
575
  }
504
576
  if (/executor|builder/i.test(identity)) {
505
577
  if ((state?.todo?.open || 0) === 0) {
578
+ if (certifiedReviewMove) return certifiedReviewMove;
506
579
  return `${name}: ask Navigator to create one bounded task with files, verifier, and stop rule before making a patch.`;
507
580
  }
508
581
  return `${name}: execute the highest-leverage claimed task one scoped step at a time, run the verifier after the patch, and hand off proof for review.`;
@@ -511,6 +584,7 @@ function memberNextMove(member, state = null) {
511
584
  return `${name}: turn one messy or unclaimed intent into a MAP-backed plan with ASCII visualization, exact files, verifier, rollback, and a review-ready task.`;
512
585
  }
513
586
  if (/launcher|closer/i.test(identity)) {
587
+ if (certifiedReviewMove) return certifiedReviewMove;
514
588
  if ((state?.todo?.done || 0) === 0) {
515
589
  return `${name}: wait for one validated task receipt before closeout, or ask Validator to produce a review decision with proof.`;
516
590
  }
@@ -544,6 +544,7 @@ function parseComputerOptions(argv) {
544
544
  let workspaceId = null;
545
545
  let waitForResult = true;
546
546
  let message = null;
547
+ let force = false;
547
548
 
548
549
  for (let i = 0; i < argv.length; i++) {
549
550
  const arg = argv[i];
@@ -600,6 +601,10 @@ function parseComputerOptions(argv) {
600
601
  waitForResult = false;
601
602
  continue;
602
603
  }
604
+ if (arg === '--force') {
605
+ force = true;
606
+ continue;
607
+ }
603
608
  positional.push(arg);
604
609
  }
605
610
 
@@ -618,6 +623,7 @@ function parseComputerOptions(argv) {
618
623
  workspaceId: workspaceId ? String(workspaceId).trim() : null,
619
624
  waitForResult,
620
625
  message,
626
+ force,
621
627
  },
622
628
  };
623
629
  }
@@ -1174,7 +1180,8 @@ function printComputerCommandFailure(result, ctx = null) {
1174
1180
  if (result?.status === 409) {
1175
1181
  const mismatch = extractAttachedWorkspaceMismatch(detail, result?.data);
1176
1182
  const targetWorkspace = mismatch?.requestedWorkspaceId || ctx?.workspaceId || '<workspace-id>';
1177
- console.error(`Run: atris computer activate --business ${businessSelector(ctx)} --workspace ${targetWorkspace}`);
1183
+ const forceFlag = /--force|force to take over|re-run with --force/i.test(detail) ? ' --force' : '';
1184
+ console.error(`Run: atris computer activate --business ${businessSelector(ctx)} --workspace ${targetWorkspace}${forceFlag}`);
1178
1185
  }
1179
1186
  }
1180
1187
 
@@ -1253,6 +1260,17 @@ function formatWorkspaceRef(workspace) {
1253
1260
  return workspace.name ? `${workspace.name} (${workspace.id})` : workspace.id;
1254
1261
  }
1255
1262
 
1263
+ function formatLeaseAge(seconds) {
1264
+ const value = Number(seconds);
1265
+ if (!Number.isFinite(value) || value < 0) return '-';
1266
+ if (value < 60) return `${Math.floor(value)}s`;
1267
+ const minutes = Math.floor(value / 60);
1268
+ if (minutes < 60) return `${minutes}m`;
1269
+ const hours = Math.floor(minutes / 60);
1270
+ if (hours < 48) return `${hours}h`;
1271
+ return `${Math.floor(hours / 24)}d`;
1272
+ }
1273
+
1256
1274
  async function probeAttachedWorkspace(token, ctx) {
1257
1275
  const result = await apiRequestJson(
1258
1276
  `/business/${ctx.businessId}/workspaces/${ctx.workspaceId}/terminal`,
@@ -1272,7 +1290,7 @@ async function probeAttachedWorkspace(token, ctx) {
1272
1290
  return { workspaceId: null, health: 'degraded', result };
1273
1291
  }
1274
1292
 
1275
- async function bootstrapBusinessComputerRuntime(token, ctx, boundary = 'computer-wake') {
1293
+ async function bootstrapBusinessComputerRuntime(token, ctx, boundary = 'computer-wake', options = {}) {
1276
1294
  if (!ctx?.businessId || !ctx?.workspaceId) {
1277
1295
  return { ok: false, skipped: true, reason: 'missing_workspace' };
1278
1296
  }
@@ -1288,8 +1306,10 @@ async function bootstrapBusinessComputerRuntime(token, ctx, boundary = 'computer
1288
1306
  });
1289
1307
  const result = await runBusinessTerminalCommand(token, ctx, command, 120);
1290
1308
  if (!result.ok) {
1291
- console.log(' Runtime: bootstrap could not run.');
1292
- console.log(` Recovery: atris computer run "npm install --prefix /workspace/.atris-npm atris@latest && /workspace/.atris-npm/node_modules/.bin/atris update" --business ${ctx.slug || ctx.businessId} --workspace ${ctx.workspaceId}`);
1309
+ if (!options.quiet) {
1310
+ console.log(' Runtime: bootstrap could not run.');
1311
+ console.log(` Recovery: atris computer run "npm install --prefix /workspace/.atris-npm atris@latest && /workspace/.atris-npm/node_modules/.bin/atris update" --business ${ctx.slug || ctx.businessId} --workspace ${ctx.workspaceId}`);
1312
+ }
1293
1313
  return { ok: false, result };
1294
1314
  }
1295
1315
 
@@ -1297,13 +1317,15 @@ async function bootstrapBusinessComputerRuntime(token, ctx, boundary = 'computer
1297
1317
  const output = String(data.stdout || data.output || data.result || '').trim();
1298
1318
  const line = output.split('\n').find((entry) => entry.includes('atris_runtime_bootstrap'));
1299
1319
  const recovery = output.split('\n').find((entry) => entry.startsWith('recovery='));
1300
- if (line) {
1301
- console.log(` Runtime: ${line.replace(/^atris_runtime_bootstrap\s*/, '')}`);
1302
- } else {
1303
- console.log(' Runtime: Atris bootstrap receipt written.');
1304
- }
1305
- if (recovery) {
1306
- console.log(` Recovery: atris computer run "${recovery.slice('recovery='.length)}" --business ${ctx.slug || ctx.businessId} --workspace ${ctx.workspaceId}`);
1320
+ if (!options.quiet) {
1321
+ if (line) {
1322
+ console.log(` Runtime: ${line.replace(/^atris_runtime_bootstrap\s*/, '')}`);
1323
+ } else {
1324
+ console.log(' Runtime: Atris bootstrap receipt written.');
1325
+ }
1326
+ if (recovery) {
1327
+ console.log(` Recovery: atris computer run "${recovery.slice('recovery='.length)}" --business ${ctx.slug || ctx.businessId} --workspace ${ctx.workspaceId}`);
1328
+ }
1307
1329
  }
1308
1330
  return { ok: true, output };
1309
1331
  }
@@ -1457,12 +1479,12 @@ async function runBusinessPromptViaRunnerProxy(token, ctx, prompt, options = {})
1457
1479
  return { ok: false, error: 'runner proxy timed out', status: 0 };
1458
1480
  }
1459
1481
 
1460
- async function ensureBusinessAwake(token, ctx, maxWaitSec = 90) {
1482
+ async function ensureBusinessAwake(token, ctx, maxWaitSec = 90, options = {}) {
1461
1483
  const status = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/status`, { method: 'GET', token });
1462
1484
  if (status.ok && status.data && status.data.status === 'running' && status.data.endpoint) {
1463
1485
  return true;
1464
1486
  }
1465
- process.stdout.write(' Waking business computer... ');
1487
+ if (!options.quiet) process.stdout.write(' Waking business computer... ');
1466
1488
  await apiRequestJson(`/business/${ctx.businessId}/ai-computer/wake`, { method: 'POST', token, body: {} });
1467
1489
  const start = Date.now();
1468
1490
  while (Date.now() - start < maxWaitSec * 1000) {
@@ -1470,12 +1492,12 @@ async function ensureBusinessAwake(token, ctx, maxWaitSec = 90) {
1470
1492
  const next = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/status`, { method: 'GET', token });
1471
1493
  if (next.ok && next.data && next.data.status === 'running' && next.data.endpoint) {
1472
1494
  const elapsed = Math.floor((Date.now() - start) / 1000);
1473
- console.log(`awake (${elapsed}s)`);
1474
- await bootstrapBusinessComputerRuntime(token, ctx, 'computer-auto-wake');
1495
+ if (!options.quiet) console.log(`awake (${elapsed}s)`);
1496
+ await bootstrapBusinessComputerRuntime(token, ctx, 'computer-auto-wake', options);
1475
1497
  return true;
1476
1498
  }
1477
1499
  }
1478
- console.log('timeout');
1500
+ if (!options.quiet) console.log('timeout');
1479
1501
  return false;
1480
1502
  }
1481
1503
 
@@ -1500,10 +1522,24 @@ async function computerStatus(token, ctx = null) {
1500
1522
  const targetWorkspace = workspaces.find((workspace) => workspace.id === ctx.workspaceId) || (ctx.workspaceId ? { id: ctx.workspaceId } : null);
1501
1523
  console.log(` Default workspace: ${formatWorkspaceRef(defaultWorkspace)}`);
1502
1524
  console.log(` Target workspace: ${formatWorkspaceRef(targetWorkspace)}`);
1525
+ const attachedFromStatus = d.attached_workspace_id
1526
+ ? { workspaceId: d.attached_workspace_id, health: null }
1527
+ : null;
1528
+ if (attachedFromStatus) {
1529
+ const attachedWorkspace = workspaces.find((workspace) => workspace.id === attachedFromStatus.workspaceId)
1530
+ || { id: attachedFromStatus.workspaceId, name: d.attached_workspace_name || null };
1531
+ console.log(` Attached workspace: ${formatWorkspaceRef(attachedWorkspace)}`);
1532
+ console.log(` Attached by: ${d.attached_by || '-'}`);
1533
+ console.log(` Attached at: ${d.attached_at || '-'}`);
1534
+ console.log(` Lease age: ${formatLeaseAge(d.lease_age_seconds)}`);
1535
+ if (d.takeover_hint) console.log(` Takeover hint: ${d.takeover_hint}`);
1536
+ }
1503
1537
  if (status === 'running' && d.endpoint && ctx.workspaceId) {
1504
1538
  const attached = await probeAttachedWorkspace(token, ctx);
1505
- const attachedWorkspace = workspaces.find((workspace) => workspace.id === attached.workspaceId) || (attached.workspaceId ? { id: attached.workspaceId } : null);
1506
- console.log(` Attached workspace: ${formatWorkspaceRef(attachedWorkspace)}`);
1539
+ if (!attachedFromStatus) {
1540
+ const attachedWorkspace = workspaces.find((workspace) => workspace.id === attached.workspaceId) || (attached.workspaceId ? { id: attached.workspaceId } : null);
1541
+ console.log(` Attached workspace: ${formatWorkspaceRef(attachedWorkspace)}`);
1542
+ }
1507
1543
  if (attached.health === 'workspace_mismatch') {
1508
1544
  printComputerCommandFailure(attached.result, ctx);
1509
1545
  } else if (attached.health !== 'ready') {
@@ -1686,7 +1722,7 @@ async function computerCreate(token, args = [], defaults = {}) {
1686
1722
  console.log(` atris computer sleep --business ${owner} --workspace ${workspaceId}`);
1687
1723
  }
1688
1724
 
1689
- async function computerActivate(token, ctx = null) {
1725
+ async function computerActivate(token, ctx = null, options = {}) {
1690
1726
  if (!ctx?.businessId || !ctx?.workspaceId) {
1691
1727
  console.error('Usage: atris computer activate --business <slug> --workspace <id>');
1692
1728
  process.exitCode = 1;
@@ -1696,7 +1732,7 @@ async function computerActivate(token, ctx = null) {
1696
1732
  const result = await apiRequestJson(`/business/${ctx.businessId}/workspaces/${ctx.workspaceId}/activate`, {
1697
1733
  method: 'POST',
1698
1734
  token,
1699
- body: {},
1735
+ body: { force: Boolean(options.force) },
1700
1736
  });
1701
1737
  if (!result.ok) {
1702
1738
  printComputerCommandFailure(result, ctx);
@@ -2420,7 +2456,7 @@ async function computerAudit(token, ctx, limit = 10) {
2420
2456
  printBusinessChatAudit(result.data?.rows || []);
2421
2457
  }
2422
2458
 
2423
- async function streamBusinessChatResult(token, ctx, executionId, rl = null) {
2459
+ async function streamBusinessChatResult(token, ctx, executionId, rl = null, options = {}) {
2424
2460
  let fromIndex = 0;
2425
2461
  let errors = 0;
2426
2462
  let cancelling = false;
@@ -2458,7 +2494,7 @@ async function streamBusinessChatResult(token, ctx, executionId, rl = null) {
2458
2494
  const sigintTarget = rl || process;
2459
2495
  sigintTarget.on('SIGINT', onSigint);
2460
2496
 
2461
- console.log(ui.dim('Running on cloud. Ctrl-C interrupts this run.'));
2497
+ if (!options.quiet) console.log(ui.dim('Running on cloud. Ctrl-C interrupts this run.'));
2462
2498
 
2463
2499
  try {
2464
2500
  while (true) {
@@ -2486,7 +2522,7 @@ async function streamBusinessChatResult(token, ctx, executionId, rl = null) {
2486
2522
  } else if (event.type === 'result' && event.result && !sawVisibleOutput) {
2487
2523
  sawVisibleOutput = true;
2488
2524
  process.stdout.write(String(event.result));
2489
- } else if (event.type === 'tool_use' && event.tool) {
2525
+ } else if (!options.quiet && event.type === 'tool_use' && event.tool) {
2490
2526
  const arg = event.input?.file_path || event.input?.path || event.input?.pattern || event.input?.command || '';
2491
2527
  if (arg) {
2492
2528
  console.log(`\n [${event.tool}] ${String(arg).slice(0, 120)}`);
@@ -2566,7 +2602,7 @@ async function sendBusinessChat(token, ctx, message, sessionId, resetContext = f
2566
2602
  const nextSessionId = data.session_id || sessionId;
2567
2603
  if (rl) rl.pause();
2568
2604
  try {
2569
- await streamBusinessChatResult(token, ctx, data.execution_id, rl);
2605
+ await streamBusinessChatResult(token, ctx, data.execution_id, rl, { quiet: Boolean(options.quiet) });
2570
2606
  } finally {
2571
2607
  if (rl) rl.resume();
2572
2608
  }
@@ -2581,31 +2617,36 @@ async function computerChat(token, ctx, initialOptions = {}) {
2581
2617
  }
2582
2618
 
2583
2619
  const isCodeOps = initialOptions.mode === 'codeops' || ctx.slug === 'atris-codeops';
2620
+ const oneShotMessage = initialOptions.message != null;
2584
2621
  const chatSystemPrompt = isCodeOps
2585
2622
  ? appendSystemPrompt(initialOptions.systemPrompt, CODEOPS_WORKFLOW_PROMPT)
2586
2623
  : initialOptions.systemPrompt;
2587
2624
  let sessionId = `biz-${ctx.businessId.slice(0, 8)}-${Date.now().toString(36)}`;
2588
2625
  const pipedInput = initialOptions.message != null ? null : await readPipedStdin();
2589
2626
  const scriptedInput = initialOptions.message != null ? String(initialOptions.message) : pipedInput;
2590
- printCloudWordmark();
2591
- const selection = await chooseCloudLane(token, ctx, initialOptions);
2627
+ if (!oneShotMessage) printCloudWordmark();
2628
+ const selection = oneShotMessage
2629
+ ? { worker: initialOptions.worker, model: initialOptions.model }
2630
+ : await chooseCloudLane(token, ctx, initialOptions);
2592
2631
  if (selection.cancelled) return;
2593
2632
  let worker = activeWorker(selection.worker);
2594
2633
  let model = selection.model || null;
2595
2634
  let awaitingLoginCode = false;
2596
- let billingLabel = await describeBillingMode(token, ctx, worker);
2597
- let authSummary = activeWorker(worker) === 'claude' ? await describeClaudeAuth(token, ctx) : null;
2635
+ let billingLabel = oneShotMessage ? null : await describeBillingMode(token, ctx, worker);
2636
+ let authSummary = oneShotMessage || activeWorker(worker) !== 'claude' ? null : await describeClaudeAuth(token, ctx);
2598
2637
 
2599
- const awake = await ensureBusinessAwake(token, ctx);
2638
+ const awake = await ensureBusinessAwake(token, ctx, 90, { quiet: oneShotMessage });
2600
2639
  if (!awake) {
2601
2640
  console.error(' Computer did not become ready in time.');
2602
2641
  return;
2603
2642
  }
2604
2643
 
2605
- if (isCodeOps) {
2606
- printCodeOpsStartPanel(ctx, worker, model, billingLabel, authSummary);
2607
- } else {
2608
- printCloudStartPanel(ctx, worker, model, billingLabel, authSummary);
2644
+ if (!oneShotMessage) {
2645
+ if (isCodeOps) {
2646
+ printCodeOpsStartPanel(ctx, worker, model, billingLabel, authSummary);
2647
+ } else {
2648
+ printCloudStartPanel(ctx, worker, model, billingLabel, authSummary);
2649
+ }
2609
2650
  }
2610
2651
 
2611
2652
  if (scriptedInput !== null) {
@@ -2640,6 +2681,7 @@ async function computerChat(token, ctx, initialOptions = {}) {
2640
2681
  model,
2641
2682
  systemPrompt: chatSystemPrompt,
2642
2683
  allowedTools: initialOptions.allowedTools,
2684
+ quiet: oneShotMessage,
2643
2685
  });
2644
2686
  }
2645
2687
  return;
@@ -3392,7 +3434,7 @@ async function runComputer() {
3392
3434
  case 'chat': return computerChat(token, ctx, cloudOptions);
3393
3435
  case 'card': return computerCard(args.slice(1));
3394
3436
  case 'proof': return computerProof(token, ctx, cloudOptions);
3395
- case 'activate': return computerActivate(token, ctx);
3437
+ case 'activate': return computerActivate(token, ctx, cloudOptions);
3396
3438
  case 'status': return computerStatus(token, ctx);
3397
3439
  case 'up':
3398
3440
  case 'wake': return computerWake(token, ctx);
@@ -7,6 +7,7 @@ const { spawn, spawnSync } = require('child_process');
7
7
 
8
8
  const VALID_STATUSES = new Set(['planning', 'running', 'ready', 'paused', 'blocked', 'stopped', 'complete']);
9
9
  const TERMINAL_STATUSES = new Set(['stopped', 'complete']);
10
+ const GOAL_LOOP_STATUSES = new Set(['planning', 'running', 'ready']);
10
11
  const STATUS_ALIASES = new Set(['active']);
11
12
 
12
13
  function stampIso() {
@@ -763,7 +764,7 @@ function secondsUntilMissionDue(mission, now = new Date()) {
763
764
  }
764
765
 
765
766
  function missionIsRunnable(mission) {
766
- return mission && !TERMINAL_STATUSES.has(mission.status) && mission.status !== 'paused';
767
+ return mission && GOAL_LOOP_STATUSES.has(String(mission.status || ''));
767
768
  }
768
769
 
769
770
  function missionSortTime(mission) {
package/commands/now.js CHANGED
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
4
  const NOW_PATH = path.join('atris', 'now.md');
5
+ const EXECUTABLE_TASK_STATUSES = new Set(['open', 'claimed']);
5
6
 
6
7
  function formatLocalDate(date = new Date()) {
7
8
  const year = String(date.getFullYear());
@@ -79,7 +80,7 @@ function countOpenTodoItems(filePath) {
79
80
  }
80
81
  const isTaskBullet = /^-\s+(?:\[[ ]\]\s+)?\*\*.+?\*\*/.test(line);
81
82
  if (!isTaskBullet) continue;
82
- if (!hasRenderedSections || ['Backlog', 'In Progress', 'Blocked'].includes(section)) {
83
+ if (!hasRenderedSections || ['Backlog', 'In Progress'].includes(section)) {
83
84
  count += 1;
84
85
  }
85
86
  }
@@ -87,6 +88,25 @@ function countOpenTodoItems(filePath) {
87
88
  return count;
88
89
  }
89
90
 
91
+ function countTaskProjectionItems(root = process.cwd()) {
92
+ const projectionPath = path.join(root, '.atris', 'state', 'tasks.projection.json');
93
+ if (!fs.existsSync(projectionPath)) return null;
94
+ try {
95
+ const projection = JSON.parse(fs.readFileSync(projectionPath, 'utf8'));
96
+ const tasks = Array.isArray(projection?.tasks) ? projection.tasks : null;
97
+ if (!tasks) return null;
98
+ return tasks.filter(task => EXECUTABLE_TASK_STATUSES.has(String(task?.status || '').toLowerCase())).length;
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ function countOpenWorkItems(root = process.cwd(), todoPath = path.join(root, 'atris', 'TODO.md')) {
105
+ const projectionCount = countTaskProjectionItems(root);
106
+ if (projectionCount !== null) return projectionCount;
107
+ return countOpenTodoItems(todoPath);
108
+ }
109
+
90
110
  function countJournalCompletedReceipts(filePath) {
91
111
  if (!fs.existsSync(filePath)) return 0;
92
112
  const content = fs.readFileSync(filePath, 'utf8');
@@ -107,7 +127,7 @@ function renderDefaultNow(root = process.cwd()) {
107
127
  const mapHeading = readFirstHeading(path.join(atrisDir, 'MAP.md')) || 'MAP not filled yet';
108
128
  const todoPath = path.join(atrisDir, 'TODO.md');
109
129
  const journalPath = currentJournalPath(root);
110
- const openTodoCount = countOpenTodoItems(todoPath);
130
+ const openTodoCount = countOpenWorkItems(root, todoPath);
111
131
  const inboxCount = countMatches(journalPath, /^-\s+\*\*I\d+:/gm);
112
132
  const completedCount = countJournalCompletedReceipts(journalPath);
113
133
  const generated = todayIso();
@@ -161,7 +181,7 @@ function renderPortfolioNow(root = process.cwd()) {
161
181
  const generated = todayIso();
162
182
  const lines = workspaces.map((workspace) => {
163
183
  const heading = readFirstHeading(workspace.mapPath) || workspace.slug;
164
- const todoCount = countOpenTodoItems(workspace.todoPath);
184
+ const todoCount = countOpenWorkItems(workspace.root, workspace.todoPath);
165
185
  const nowState = fs.existsSync(workspace.nowPath) ? 'has now.md' : 'needs now.md';
166
186
  return `- ${workspace.slug}: ${heading}; ${todoCount} open TODO item${todoCount === 1 ? '' : 's'}; ${nowState}.`;
167
187
  });
@@ -201,6 +221,18 @@ ${workspaces.map((workspace) => `- \`${workspace.slug}/atris/MAP.md\``).join('\n
201
221
  `;
202
222
  }
203
223
 
224
+ function isGeneratedNowFile(content) {
225
+ const text = String(content || '');
226
+ const hasGeneratedSignature = (
227
+ text.includes('> Current operating truth for this workspace.') ||
228
+ text.includes('> Current operating truth for this portfolio of Atris workspaces.')
229
+ ) && text.includes('## Receipts');
230
+ const hasLegacyGeneratedCounters = /^#\s+now\s*$/m.test(text)
231
+ && /Open TODO items:\s*\d+/m.test(text)
232
+ && /Completed receipts today:\s*\d+/m.test(text);
233
+ return hasGeneratedSignature || hasLegacyGeneratedCounters;
234
+ }
235
+
204
236
  function ensureNowFile(root = process.cwd()) {
205
237
  let atrisDir = path.join(root, 'atris');
206
238
  const isWorkspace = fs.existsSync(atrisDir) && hasWorkspaceMarkers(atrisDir);
@@ -220,7 +252,7 @@ function ensureNowFile(root = process.cwd()) {
220
252
  return { created: false, path: nowPath };
221
253
  }
222
254
 
223
- function refreshNowFile(root = process.cwd()) {
255
+ function refreshNowFile(root = process.cwd(), options = {}) {
224
256
  const atrisDir = path.join(root, 'atris');
225
257
  const isWorkspace = fs.existsSync(atrisDir) && hasWorkspaceMarkers(atrisDir);
226
258
  const childWorkspaces = isWorkspace ? [] : findChildWorkspaces(root);
@@ -231,9 +263,15 @@ function refreshNowFile(root = process.cwd()) {
231
263
  fs.mkdirSync(atrisDir, { recursive: true });
232
264
  }
233
265
  const nowPath = path.join(atrisDir, 'now.md');
266
+ if (options.preserveCustom && fs.existsSync(nowPath)) {
267
+ const current = fs.readFileSync(nowPath, 'utf8');
268
+ if (!isGeneratedNowFile(current)) {
269
+ return { path: nowPath, preserved: true };
270
+ }
271
+ }
234
272
  const content = isWorkspace ? renderDefaultNow(root) : renderPortfolioNow(root);
235
273
  fs.writeFileSync(nowPath, content, 'utf8');
236
- return { path: nowPath };
274
+ return { path: nowPath, preserved: false };
237
275
  }
238
276
 
239
277
  function nowAtris(args = process.argv.slice(3), root = process.cwd()) {
@@ -295,8 +333,10 @@ module.exports = {
295
333
  ensureNowFile,
296
334
  formatLocalDate,
297
335
  countJournalCompletedReceipts,
336
+ countOpenWorkItems,
298
337
  countOpenTodoItems,
299
338
  findChildWorkspaces,
339
+ isGeneratedNowFile,
300
340
  nowAtris,
301
341
  refreshNowFile,
302
342
  renderDefaultNow,
package/commands/radar.js CHANGED
@@ -128,6 +128,45 @@ function loadTasks(root, deps) {
128
128
  }));
129
129
  }
130
130
 
131
+ function findTaskWorkspaceRoot(cwd, deps) {
132
+ if (!cwd) return null;
133
+ let current = path.resolve(cwd);
134
+ for (let depth = 0; depth < 8; depth += 1) {
135
+ if (deps.exists(path.join(current, '.atris', 'state', 'tasks.projection.json'))) return current;
136
+ const parent = path.dirname(current);
137
+ if (!parent || parent === current) break;
138
+ current = parent;
139
+ }
140
+ return null;
141
+ }
142
+
143
+ function loadTasksCached(root, deps, cache) {
144
+ if (!root) return [];
145
+ if (!cache.has(root)) cache.set(root, loadTasks(root, deps));
146
+ return cache.get(root);
147
+ }
148
+
149
+ function untaskedReason(agent, taskWorkspaceRoot, tasks) {
150
+ if (!agent.cwd) return 'cwd unknown';
151
+ if (!taskWorkspaceRoot) return 'no task projection';
152
+ if (!tasks.length) return 'empty task projection';
153
+ return 'no active task';
154
+ }
155
+
156
+ function shellQuote(value) {
157
+ return `'${String(value || '').replace(/'/g, "'\\''")}'`;
158
+ }
159
+
160
+ function untaskedAction(agent, taskWorkspaceRoot, tasks) {
161
+ const reason = untaskedReason(agent, taskWorkspaceRoot, tasks);
162
+ const pid = agent.pid || '?';
163
+ const actor = agent.agent || 'agent';
164
+ if (reason === 'cwd unknown') return `inspect pid ${pid} cwd with lsof`;
165
+ if (reason === 'no active task') return `cd ${shellQuote(taskWorkspaceRoot)} && atris task next --as ${actor}`;
166
+ if (reason === 'empty task projection') return `cd ${shellQuote(taskWorkspaceRoot)} && atris task new "<small concrete title>" --tag ops`;
167
+ return `inspect ${agent.cwd || 'unknown cwd'} for missing Atris task plane or close pid ${pid} if idle`;
168
+ }
169
+
131
170
  function readJsonFile(file, deps, fallback = null) {
132
171
  if (!deps.exists(file)) return fallback;
133
172
  return safeJson(deps.readFile(file, 'utf8'), fallback);
@@ -369,10 +408,12 @@ function taskRef(task) {
369
408
  return task ? (task.display_id || task.legacy_ref || task.id || '-') : '-';
370
409
  }
371
410
 
372
- function taskForCwd(tasks, cwd) {
373
- if (!cwd) return null;
374
- return tasks.find(task => task.workspace_root === cwd && ['claimed', 'open'].includes(task.status))
375
- || tasks.find(task => task.workspace_root === cwd && task.status === 'review')
411
+ function taskForCwd(tasks, cwd, workspaceRoot = cwd) {
412
+ if (!cwd && !workspaceRoot) return null;
413
+ const matchesWorkspace = task => !task.workspace_root || task.workspace_root === cwd || task.workspace_root === workspaceRoot;
414
+ return tasks.find(task => matchesWorkspace(task) && task.status === 'claimed')
415
+ || tasks.find(task => matchesWorkspace(task) && task.status === 'open')
416
+ || tasks.find(task => matchesWorkspace(task) && task.status === 'review')
376
417
  || null;
377
418
  }
378
419
 
@@ -425,11 +466,23 @@ function collectRadar(options = {}) {
425
466
  };
426
467
  const nowMs = options.nowMs || Date.now();
427
468
  const tasks = loadTasks(root, deps);
469
+ const taskCache = new Map([[root, tasks]]);
428
470
  const missions = loadMissions(root, deps, nowMs);
429
471
  const worktrees = loadWorktrees(root, deps);
430
472
  const agents = collectAgents(deps).map(agent => {
431
- const task = taskForCwd(tasks, agent.cwd);
432
- return { ...agent, task: taskRef(task), task_status: task?.status || null, owner: ownerForTask(task) };
473
+ const taskWorkspaceRoot = findTaskWorkspaceRoot(agent.cwd, deps);
474
+ const agentTasks = taskWorkspaceRoot ? loadTasksCached(taskWorkspaceRoot, deps, taskCache) : [];
475
+ const task = taskForCwd(agentTasks, agent.cwd, taskWorkspaceRoot);
476
+ const taskReason = task ? null : untaskedReason(agent, taskWorkspaceRoot, agentTasks);
477
+ return {
478
+ ...agent,
479
+ task: taskRef(task),
480
+ task_status: task?.status || null,
481
+ owner: ownerForTask(task),
482
+ task_workspace: taskWorkspaceRoot ? repoLabel(taskWorkspaceRoot) : null,
483
+ task_reason: taskReason,
484
+ task_action: task ? null : untaskedAction(agent, taskWorkspaceRoot, agentTasks),
485
+ };
433
486
  });
434
487
  const osState = {
435
488
  xp: loadXp(root, deps),
@@ -523,24 +576,179 @@ function renderRadar(data) {
523
576
  return lines.join('\n');
524
577
  }
525
578
 
579
+ function number(value) {
580
+ return Number.isFinite(Number(value)) ? Number(value) : 0;
581
+ }
582
+
583
+ function sortedAgents(agents = []) {
584
+ return [...agents].sort((a, b) => {
585
+ if (a.status !== b.status) return a.status === 'active' ? -1 : 1;
586
+ const cpuDelta = number(b.cpu) - number(a.cpu);
587
+ if (cpuDelta) return cpuDelta;
588
+ return String(a.repo || '').localeCompare(String(b.repo || ''));
589
+ });
590
+ }
591
+
592
+ function agentProcessNextAction(agents = [], fallback = 'no obvious process action') {
593
+ const stopped = agents.filter(agent => agent.status !== 'active').length;
594
+ if (stopped > 0) return `inspect ${stopped} stopped agent session${stopped === 1 ? '' : 's'}`;
595
+ const taskLoad = summarizeTaskLoad(agents);
596
+ const reviewBound = taskLoad.find(row => row.status.split(/,\s*/).includes('review'));
597
+ if (reviewBound) return `close or hand off ${reviewBound.sessions} session${reviewBound.sessions === 1 ? '' : 's'} still bound to review task ${reviewBound.task}`;
598
+ const pileup = taskLoad.find(row => row.sessions > 1);
599
+ if (pileup) return `inspect ${pileup.sessions} sessions on ${pileup.task} (${pileup.cpu.toFixed(1)}% CPU)`;
600
+ const untasked = agents.filter(agent => !agent.task || agent.task === '-').length;
601
+ if (untasked > 0) {
602
+ const reasons = summarizeUntaskedReasons(agents);
603
+ const summary = reasons.map(row => `${row.count} ${row.reason}`).join(', ');
604
+ return `resolve ${untasked} untasked session${untasked === 1 ? '' : 's'}${summary ? `: ${summary}` : ''}`;
605
+ }
606
+ const hot = agents.find(agent => number(agent.cpu) >= 50);
607
+ if (hot) return `inspect high-CPU ${hot.agent} ${hot.pid} in ${hot.repo || hot.cwd || 'unknown repo'}`;
608
+ return fallback;
609
+ }
610
+
611
+ function summarizeUntaskedReasons(agents = []) {
612
+ const counts = new Map();
613
+ for (const agent of agents) {
614
+ if (agent.task && agent.task !== '-') continue;
615
+ const reason = agent.task_reason || 'unmapped';
616
+ counts.set(reason, (counts.get(reason) || 0) + 1);
617
+ }
618
+ return [...counts.entries()]
619
+ .map(([reason, count]) => ({ reason, count }))
620
+ .sort((a, b) => b.count - a.count || a.reason.localeCompare(b.reason));
621
+ }
622
+
623
+ function summarizeTaskLoad(agents = []) {
624
+ const byTask = new Map();
625
+ for (const agent of agents) {
626
+ if (!agent.task || agent.task === '-') continue;
627
+ if (!byTask.has(agent.task)) {
628
+ byTask.set(agent.task, {
629
+ task: agent.task,
630
+ sessions: 0,
631
+ active: 0,
632
+ cpu: 0,
633
+ mem: 0,
634
+ statuses: new Set(),
635
+ owners: new Set(),
636
+ repos: new Set(),
637
+ pids: [],
638
+ });
639
+ }
640
+ const row = byTask.get(agent.task);
641
+ row.sessions += 1;
642
+ if (agent.status === 'active') row.active += 1;
643
+ row.cpu += number(agent.cpu);
644
+ row.mem += number(agent.mem);
645
+ if (agent.task_status) row.statuses.add(agent.task_status);
646
+ if (agent.owner && agent.owner !== '-') row.owners.add(agent.owner);
647
+ if (agent.repo) row.repos.add(agent.repo);
648
+ if (agent.pid) row.pids.push(agent.pid);
649
+ }
650
+ return [...byTask.values()]
651
+ .map(row => {
652
+ const statuses = [...row.statuses].sort();
653
+ return {
654
+ task: row.task,
655
+ sessions: row.sessions,
656
+ active: row.active,
657
+ cpu: Number(row.cpu.toFixed(1)),
658
+ mem: Number(row.mem.toFixed(1)),
659
+ status: statuses.join(', ') || '-',
660
+ owners: [...row.owners].sort(),
661
+ repos: [...row.repos].sort(),
662
+ pids: row.pids.sort((a, b) => number(a) - number(b)),
663
+ attention: row.sessions > 1 || statuses.includes('review'),
664
+ };
665
+ })
666
+ .sort((a, b) => Number(b.attention) - Number(a.attention) || b.sessions - a.sessions || b.cpu - a.cpu || a.task.localeCompare(b.task));
667
+ }
668
+
669
+ function agentTopPayload(data) {
670
+ const agents = sortedAgents(data.agents || []);
671
+ const untasked = agents.filter(agent => !agent.task || agent.task === '-').length;
672
+ const cpu = agents.reduce((sum, agent) => sum + number(agent.cpu), 0);
673
+ const mem = agents.reduce((sum, agent) => sum + number(agent.mem), 0);
674
+ const taskLoad = summarizeTaskLoad(agents);
675
+ return {
676
+ root: data.root,
677
+ generated_at: data.generated_at,
678
+ summary: {
679
+ total: agents.length,
680
+ active: agents.filter(agent => agent.status === 'active').length,
681
+ untasked,
682
+ untasked_reasons: summarizeUntaskedReasons(agents),
683
+ task_pileups: taskLoad.filter(row => row.sessions > 1).length,
684
+ review_bound_tasks: taskLoad.filter(row => row.status === 'review').length,
685
+ cpu: Number(cpu.toFixed(1)),
686
+ mem: Number(mem.toFixed(1)),
687
+ },
688
+ next_action: agentProcessNextAction(agents, data.next_action),
689
+ task_load: taskLoad,
690
+ agents,
691
+ };
692
+ }
693
+
694
+ function renderAgentTop(data) {
695
+ const payload = agentTopPayload(data);
696
+ const lines = [];
697
+ lines.push('Agent process top');
698
+ lines.push('');
699
+ lines.push(`Agents: ${payload.summary.active}/${payload.summary.total} active; ${payload.summary.untasked} untasked; CPU ${payload.summary.cpu.toFixed(1)}%; MEM ${payload.summary.mem.toFixed(1)}%`);
700
+ lines.push(`Next: ${payload.next_action}`);
701
+ lines.push('');
702
+ lines.push(`${truncate('PID', 7)} ${truncate('AGENT', 8)} ${truncate('CPU', 6)} ${truncate('MEM', 6)} ${truncate('REPO', 24)} ${truncate('BRANCH', 16)} ${truncate('TASK', 10)} ${truncate('STATE', 8)}`);
703
+ for (const agent of payload.agents.slice(0, 32)) {
704
+ lines.push(`${truncate(agent.pid, 7)} ${truncate(agent.agent, 8)} ${truncate(`${number(agent.cpu).toFixed(1)}%`, 6)} ${truncate(`${number(agent.mem).toFixed(1)}%`, 6)} ${truncate(agent.repo, 24)} ${truncate(agent.branch, 16)} ${truncate(agent.task, 10)} ${truncate(agent.status, 8)}`);
705
+ }
706
+ if (payload.agents.length > 32) lines.push(`... ${payload.agents.length - 32} more agents`);
707
+ if (payload.summary.untasked > 0) {
708
+ lines.push('');
709
+ const reasonText = payload.summary.untasked_reasons.map(row => `${row.count} ${row.reason}`).join(', ');
710
+ lines.push(`Untasked: ${payload.summary.untasked} sessions (${reasonText}).`);
711
+ for (const agent of payload.agents.filter(row => !row.task || row.task === '-').slice(0, 8)) {
712
+ lines.push(`- ${agent.pid} ${agent.repo || agent.cwd || '-'}: ${agent.task_reason || 'unmapped'} -> ${agent.task_action || 'inspect session'}`);
713
+ }
714
+ }
715
+ const taskLoadRows = payload.task_load.filter(row => row.attention).slice(0, 8);
716
+ if (taskLoadRows.length) {
717
+ lines.push('');
718
+ lines.push(`Task load: ${payload.summary.task_pileups} pileup${payload.summary.task_pileups === 1 ? '' : 's'}, ${payload.summary.review_bound_tasks} review-bound task${payload.summary.review_bound_tasks === 1 ? '' : 's'}.`);
719
+ for (const row of taskLoadRows) {
720
+ const repoText = row.repos.slice(0, 3).join(', ') || '-';
721
+ lines.push(`- ${row.task}: ${row.sessions} sessions, ${row.cpu.toFixed(1)}% CPU, ${row.status}, ${repoText}`);
722
+ }
723
+ }
724
+ return lines.join('\n');
725
+ }
726
+
526
727
  function radarCommand(args = [], options = {}) {
527
728
  if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
528
- console.log('Usage: atris radar [--json]');
729
+ console.log('Usage: atris radar [--json] [--agents]');
730
+ console.log('Usage: atris radar agents');
731
+ console.log('Usage: atris ctop');
529
732
  console.log('');
530
733
  console.log('Shows live agent processes joined with Atris tasks, missions, and worktrees.');
734
+ console.log('Use --agents or ctop for a process-first CPU/memory view.');
531
735
  return 0;
532
736
  }
533
737
  const data = collectRadar(options);
534
- if (args.includes('--json')) console.log(JSON.stringify(data, null, 2));
738
+ const agentsOnly = args.includes('--agents') || args[0] === 'agents';
739
+ if (args.includes('--json')) console.log(JSON.stringify(agentsOnly ? agentTopPayload(data) : data, null, 2));
740
+ else if (agentsOnly) console.log(renderAgentTop(data));
535
741
  else console.log(renderRadar(data));
536
742
  return 0;
537
743
  }
538
744
 
539
745
  module.exports = {
540
746
  agentTypeForCommand,
747
+ agentTopPayload,
541
748
  collectRadar,
542
749
  parsePsOutput,
543
750
  parseWorktrees,
544
751
  radarCommand,
752
+ renderAgentTop,
545
753
  renderRadar,
546
754
  };
package/commands/task.js CHANGED
@@ -79,6 +79,7 @@ atris task - durable local task state (SQLite, gitignored)
79
79
  atris task done <id> [--failed] [--proof "..."] Mark complete (or failed), optionally reviewed
80
80
  atris task finish <id> [--proof "..."] Legacy alias for done
81
81
  atris task review <id> --reward <n> Write review event + RSI episode
82
+ atris task reviews [--limit <n>] Show certified Review items for human accept/revise
82
83
  atris task status [--json] [--history] Compact live status for web/Swarlo
83
84
  atris task setup [--import-todo] Create/refresh task projection
84
85
  atris task serve [--port <n>] Open local task factory board
@@ -878,6 +879,86 @@ function taskStatusSummary(projection, { history = false } = {}) {
878
879
  return status;
879
880
  }
880
881
 
882
+ function reviewQueueLimit(args, total) {
883
+ if (hasFlag(args, '--all')) return total;
884
+ const raw = flag(args, '--limit');
885
+ const limit = raw && raw !== true ? Number(raw) : 12;
886
+ return Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 12;
887
+ }
888
+
889
+ function reviewQueueItem(task) {
890
+ const ref = taskRef(task);
891
+ return {
892
+ id: task.id,
893
+ display_id: task.display_id || null,
894
+ title: task.title,
895
+ tag: task.tag || null,
896
+ updated_at: task.updated_at || null,
897
+ review_pass_count: task.review?.agent_review_pass_count || null,
898
+ proof: task.review?.proof || null,
899
+ accept_command: `atris task accept ${ref}`,
900
+ revise_command: `atris task revise ${ref} --note "<what must change>"`,
901
+ };
902
+ }
903
+
904
+ function taskReviewQueue(projection, args = []) {
905
+ const reviewTasks = (projection.tasks || [])
906
+ .map(compactTaskForStatus)
907
+ .filter(task => task && task.status === 'review' && task.review && task.review.approval_status === 'pending')
908
+ .sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0));
909
+ const blocking = reviewTasks.filter(task => task.review?.handoff?.next_action === 'agent_review_again');
910
+ const certified = reviewTasks.filter(task => task.review?.handoff?.next_action === 'continue_work' || task.review?.agent_certified === true);
911
+ const limit = reviewQueueLimit(args, certified.length);
912
+ const items = certified.slice(0, limit).map(reviewQueueItem);
913
+ return {
914
+ schema: 'atris.task_review_queue.v1',
915
+ generated_at: projection.generated_at,
916
+ workspace_root: projection.workspace_root,
917
+ counts: {
918
+ review: reviewTasks.length,
919
+ certified: certified.length,
920
+ blocking: blocking.length,
921
+ shown: items.length,
922
+ },
923
+ items,
924
+ };
925
+ }
926
+
927
+ function cmdReviews(args) {
928
+ const taskDb = getTaskDb();
929
+ const db = taskDb.open();
930
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
931
+ const queue = taskReviewQueue(projection, args);
932
+ if (wantsJson(args)) {
933
+ printJson({
934
+ ok: true,
935
+ action: 'review_queue',
936
+ projection_path: outPath,
937
+ queue,
938
+ });
939
+ return;
940
+ }
941
+ console.log('CERTIFIED REVIEW QUEUE');
942
+ console.log(`${queue.counts.certified} certified / ${queue.counts.blocking} need agent review / ${queue.counts.review} total review`);
943
+ if (!queue.items.length) {
944
+ console.log('No certified review items.');
945
+ return;
946
+ }
947
+ queue.items.forEach((item, index) => {
948
+ const tag = item.tag ? ` [${item.tag}]` : '';
949
+ const passes = item.review_pass_count ? ` (${item.review_pass_count} reviews)` : '';
950
+ console.log('');
951
+ console.log(`${index + 1}. ${item.display_id || taskRef(item.id)}${tag}${passes}: ${item.title}`);
952
+ if (item.proof) console.log(` proof: ${item.proof}`);
953
+ console.log(` accept: ${item.accept_command}`);
954
+ console.log(` revise: ${item.revise_command}`);
955
+ });
956
+ if (queue.counts.shown < queue.counts.certified) {
957
+ console.log('');
958
+ console.log(`Showing ${queue.counts.shown}/${queue.counts.certified}; rerun with --all or --limit ${queue.counts.certified}.`);
959
+ }
960
+ }
961
+
881
962
  function humanEventType(type) {
882
963
  return String(type || 'event').replace(/_/g, ' ');
883
964
  }
@@ -2743,6 +2824,9 @@ async function run(args) {
2743
2824
  case 'finish': return cmdFinish(rest);
2744
2825
  case 'fail': return cmdDone([...rest, '--failed']);
2745
2826
  case 'review': return cmdReview(rest);
2827
+ case 'reviews':
2828
+ case 'review-queue':
2829
+ return cmdReviews(rest);
2746
2830
  case 'status': return cmdStatus(rest);
2747
2831
  case 'setup': return cmdSetup(rest);
2748
2832
  case 'serve': return cmdServe(rest);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.15.48",
3
+ "version": "3.15.50",
4
4
  "main": "bin/atris.js",
5
5
  "bin": {
6
6
  "atris": "bin/atris.js",