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.
- package/atris/wiki/index.md +2 -0
- package/bin/atris.js +5 -3
- package/commands/brain.js +77 -3
- package/commands/computer.js +76 -34
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
1292
|
-
|
|
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 (
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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
|
-
|
|
1506
|
-
|
|
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 =
|
|
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)
|
|
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 (
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
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);
|
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);
|