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.
- package/atris/wiki/index.md +2 -0
- package/bin/atris.js +5 -3
- package/commands/brain.js +77 -3
- package/commands/computer.js +23 -17
- package/commands/mission.js +2 -1
- package/commands/now.js +45 -5
- package/commands/radar.js +216 -8
- package/commands/task.js +84 -0
- package/package.json +1 -1
package/atris/wiki/index.md
CHANGED
|
@@ -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
|
-
|
|
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'
|
|
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:
|
|
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
|
}
|
package/commands/computer.js
CHANGED
|
@@ -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 =
|
|
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)
|
|
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 (
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
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;
|
package/commands/mission.js
CHANGED
|
@@ -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 &&
|
|
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'
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
375
|
-
|
|
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
|
|
432
|
-
|
|
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
|
-
|
|
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);
|