atris 3.15.48 → 3.15.49

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
  }
@@ -1457,12 +1457,12 @@ async function runBusinessPromptViaRunnerProxy(token, ctx, prompt, options = {})
1457
1457
  return { ok: false, error: 'runner proxy timed out', status: 0 };
1458
1458
  }
1459
1459
 
1460
- async function ensureBusinessAwake(token, ctx, maxWaitSec = 90) {
1460
+ async function ensureBusinessAwake(token, ctx, maxWaitSec = 90, options = {}) {
1461
1461
  const status = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/status`, { method: 'GET', token });
1462
1462
  if (status.ok && status.data && status.data.status === 'running' && status.data.endpoint) {
1463
1463
  return true;
1464
1464
  }
1465
- process.stdout.write(' Waking business computer... ');
1465
+ if (!options.quiet) process.stdout.write(' Waking business computer... ');
1466
1466
  await apiRequestJson(`/business/${ctx.businessId}/ai-computer/wake`, { method: 'POST', token, body: {} });
1467
1467
  const start = Date.now();
1468
1468
  while (Date.now() - start < maxWaitSec * 1000) {
@@ -1470,12 +1470,12 @@ async function ensureBusinessAwake(token, ctx, maxWaitSec = 90) {
1470
1470
  const next = await apiRequestJson(`/business/${ctx.businessId}/ai-computer/status`, { method: 'GET', token });
1471
1471
  if (next.ok && next.data && next.data.status === 'running' && next.data.endpoint) {
1472
1472
  const elapsed = Math.floor((Date.now() - start) / 1000);
1473
- console.log(`awake (${elapsed}s)`);
1473
+ if (!options.quiet) console.log(`awake (${elapsed}s)`);
1474
1474
  await bootstrapBusinessComputerRuntime(token, ctx, 'computer-auto-wake');
1475
1475
  return true;
1476
1476
  }
1477
1477
  }
1478
- console.log('timeout');
1478
+ if (!options.quiet) console.log('timeout');
1479
1479
  return false;
1480
1480
  }
1481
1481
 
@@ -2420,7 +2420,7 @@ async function computerAudit(token, ctx, limit = 10) {
2420
2420
  printBusinessChatAudit(result.data?.rows || []);
2421
2421
  }
2422
2422
 
2423
- async function streamBusinessChatResult(token, ctx, executionId, rl = null) {
2423
+ async function streamBusinessChatResult(token, ctx, executionId, rl = null, options = {}) {
2424
2424
  let fromIndex = 0;
2425
2425
  let errors = 0;
2426
2426
  let cancelling = false;
@@ -2458,7 +2458,7 @@ async function streamBusinessChatResult(token, ctx, executionId, rl = null) {
2458
2458
  const sigintTarget = rl || process;
2459
2459
  sigintTarget.on('SIGINT', onSigint);
2460
2460
 
2461
- console.log(ui.dim('Running on cloud. Ctrl-C interrupts this run.'));
2461
+ if (!options.quiet) console.log(ui.dim('Running on cloud. Ctrl-C interrupts this run.'));
2462
2462
 
2463
2463
  try {
2464
2464
  while (true) {
@@ -2486,7 +2486,7 @@ async function streamBusinessChatResult(token, ctx, executionId, rl = null) {
2486
2486
  } else if (event.type === 'result' && event.result && !sawVisibleOutput) {
2487
2487
  sawVisibleOutput = true;
2488
2488
  process.stdout.write(String(event.result));
2489
- } else if (event.type === 'tool_use' && event.tool) {
2489
+ } else if (!options.quiet && event.type === 'tool_use' && event.tool) {
2490
2490
  const arg = event.input?.file_path || event.input?.path || event.input?.pattern || event.input?.command || '';
2491
2491
  if (arg) {
2492
2492
  console.log(`\n [${event.tool}] ${String(arg).slice(0, 120)}`);
@@ -2566,7 +2566,7 @@ async function sendBusinessChat(token, ctx, message, sessionId, resetContext = f
2566
2566
  const nextSessionId = data.session_id || sessionId;
2567
2567
  if (rl) rl.pause();
2568
2568
  try {
2569
- await streamBusinessChatResult(token, ctx, data.execution_id, rl);
2569
+ await streamBusinessChatResult(token, ctx, data.execution_id, rl, { quiet: Boolean(options.quiet) });
2570
2570
  } finally {
2571
2571
  if (rl) rl.resume();
2572
2572
  }
@@ -2584,28 +2584,33 @@ async function computerChat(token, ctx, initialOptions = {}) {
2584
2584
  const chatSystemPrompt = isCodeOps
2585
2585
  ? appendSystemPrompt(initialOptions.systemPrompt, CODEOPS_WORKFLOW_PROMPT)
2586
2586
  : initialOptions.systemPrompt;
2587
+ const oneShotMessage = initialOptions.message != null;
2587
2588
  let sessionId = `biz-${ctx.businessId.slice(0, 8)}-${Date.now().toString(36)}`;
2588
2589
  const pipedInput = initialOptions.message != null ? null : await readPipedStdin();
2589
2590
  const scriptedInput = initialOptions.message != null ? String(initialOptions.message) : pipedInput;
2590
- printCloudWordmark();
2591
- const selection = await chooseCloudLane(token, ctx, initialOptions);
2591
+ if (!oneShotMessage) printCloudWordmark();
2592
+ const selection = oneShotMessage
2593
+ ? { worker: initialOptions.worker, model: initialOptions.model }
2594
+ : await chooseCloudLane(token, ctx, initialOptions);
2592
2595
  if (selection.cancelled) return;
2593
2596
  let worker = activeWorker(selection.worker);
2594
2597
  let model = selection.model || null;
2595
2598
  let awaitingLoginCode = false;
2596
- let billingLabel = await describeBillingMode(token, ctx, worker);
2597
- let authSummary = activeWorker(worker) === 'claude' ? await describeClaudeAuth(token, ctx) : null;
2599
+ let billingLabel = oneShotMessage ? null : await describeBillingMode(token, ctx, worker);
2600
+ let authSummary = oneShotMessage || activeWorker(worker) !== 'claude' ? null : await describeClaudeAuth(token, ctx);
2598
2601
 
2599
- const awake = await ensureBusinessAwake(token, ctx);
2602
+ const awake = await ensureBusinessAwake(token, ctx, 90, { quiet: oneShotMessage });
2600
2603
  if (!awake) {
2601
2604
  console.error(' Computer did not become ready in time.');
2602
2605
  return;
2603
2606
  }
2604
2607
 
2605
- if (isCodeOps) {
2606
- printCodeOpsStartPanel(ctx, worker, model, billingLabel, authSummary);
2607
- } else {
2608
- printCloudStartPanel(ctx, worker, model, billingLabel, authSummary);
2608
+ if (!oneShotMessage) {
2609
+ if (isCodeOps) {
2610
+ printCodeOpsStartPanel(ctx, worker, model, billingLabel, authSummary);
2611
+ } else {
2612
+ printCloudStartPanel(ctx, worker, model, billingLabel, authSummary);
2613
+ }
2609
2614
  }
2610
2615
 
2611
2616
  if (scriptedInput !== null) {
@@ -2640,6 +2645,7 @@ async function computerChat(token, ctx, initialOptions = {}) {
2640
2645
  model,
2641
2646
  systemPrompt: chatSystemPrompt,
2642
2647
  allowedTools: initialOptions.allowedTools,
2648
+ quiet: oneShotMessage,
2643
2649
  });
2644
2650
  }
2645
2651
  return;
@@ -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.49",
4
4
  "main": "bin/atris.js",
5
5
  "bin": {
6
6
  "atris": "bin/atris.js",