atris 3.15.46 → 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/AGENTS.md +2 -2
- package/atris/skills/atris/SKILL.md +1 -1
- package/atris/wiki/index.md +2 -0
- package/bin/atris.js +12 -4
- package/commands/brain.js +77 -3
- package/commands/business.js +574 -21
- package/commands/computer.js +23 -17
- package/commands/mission.js +18 -2
- package/commands/now.js +45 -5
- package/commands/radar.js +754 -0
- package/commands/sync.js +101 -1
- package/commands/task.js +84 -0
- package/lib/task-db.js +29 -3
- package/package.json +2 -1
- package/templates/business-starter/MAP.md +1 -0
- package/templates/business-starter/team/README.md +3 -0
- package/templates/business-starter/team/START_HERE.md +45 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
function safeJson(text, fallback = null) {
|
|
9
|
+
try { return JSON.parse(text); } catch { return fallback; }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function readJsonLines(file, readFile = fs.readFileSync, exists = fs.existsSync) {
|
|
13
|
+
if (!exists(file)) return [];
|
|
14
|
+
return String(readFile(file, 'utf8'))
|
|
15
|
+
.split(/\r?\n/)
|
|
16
|
+
.map(line => line.trim())
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.map(line => safeJson(line))
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function truncate(value, width) {
|
|
23
|
+
const text = String(value == null || value === '' ? '-' : value);
|
|
24
|
+
if (text.length <= width) return text.padEnd(width, ' ');
|
|
25
|
+
return `${text.slice(0, Math.max(0, width - 1))}…`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function repoLabel(cwd) {
|
|
29
|
+
if (!cwd) return '-';
|
|
30
|
+
const parts = cwd.split(/[\\/]/).filter(Boolean);
|
|
31
|
+
if (parts.length >= 2) return `${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
|
|
32
|
+
return parts[0] || cwd;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parsePsOutput(text) {
|
|
36
|
+
const rows = [];
|
|
37
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
38
|
+
const trimmed = line.trim();
|
|
39
|
+
if (!trimmed) continue;
|
|
40
|
+
const parts = trimmed.split(/\s+/);
|
|
41
|
+
if (parts.length < 10) continue;
|
|
42
|
+
const [pid, ppid, cpu, mem, stat] = parts;
|
|
43
|
+
const start = parts.slice(5, 10).join(' ');
|
|
44
|
+
const command = parts.slice(10).join(' ');
|
|
45
|
+
rows.push({ pid, ppid, cpu: Number(cpu) || 0, mem: Number(mem) || 0, stat, start, command });
|
|
46
|
+
}
|
|
47
|
+
return rows;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function agentTypeForCommand(command) {
|
|
51
|
+
const cmd = String(command || '');
|
|
52
|
+
if (/ctop|grep|node\s+.*atris\.js\s+radar/.test(cmd)) return null;
|
|
53
|
+
if (/(^|\s|\/)codex(\s|$)|codex-darwin|codex-linux|codex-win/.test(cmd)) return 'codex';
|
|
54
|
+
if (/(^|\s|\/)claude(\s|$)/.test(cmd) && !/Claude\.app/.test(cmd)) return 'claude';
|
|
55
|
+
if (/(^|\s|\/)opencode(\s|$)/.test(cmd)) return 'opencode';
|
|
56
|
+
if (/(^|\s|\/)devin(\s|$)/.test(cmd)) return 'devin';
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function processCwd(pid, deps) {
|
|
61
|
+
const { platform, execFile } = deps;
|
|
62
|
+
try {
|
|
63
|
+
if (platform === 'linux') {
|
|
64
|
+
return deps.readlink(`/proc/${pid}/cwd`);
|
|
65
|
+
}
|
|
66
|
+
if (platform === 'darwin') {
|
|
67
|
+
const out = execFile('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
68
|
+
const line = String(out).split(/\r?\n/).find(value => value.startsWith('n'));
|
|
69
|
+
return line ? line.slice(1) : '';
|
|
70
|
+
}
|
|
71
|
+
} catch {}
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function gitBranch(cwd, execFile) {
|
|
76
|
+
if (!cwd) return '';
|
|
77
|
+
try {
|
|
78
|
+
return String(execFile('git', ['-C', cwd, 'branch', '--show-current'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] })).trim();
|
|
79
|
+
} catch {
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function collectAgents(deps) {
|
|
85
|
+
let ps = '';
|
|
86
|
+
try {
|
|
87
|
+
ps = deps.execFile('ps', ['-eo', 'pid=,ppid=,pcpu=,pmem=,stat=,lstart=,command='], { encoding: 'utf8' });
|
|
88
|
+
} catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const agentRows = parsePsOutput(ps)
|
|
92
|
+
.map(row => ({ ...row, agent: agentTypeForCommand(row.command) }))
|
|
93
|
+
.filter(row => row.agent);
|
|
94
|
+
const parentPids = new Set(agentRows.map(row => String(row.ppid || '')).filter(Boolean));
|
|
95
|
+
return agentRows
|
|
96
|
+
.filter(row => !parentPids.has(String(row.pid)))
|
|
97
|
+
.map(row => {
|
|
98
|
+
const cwd = processCwd(row.pid, deps);
|
|
99
|
+
return {
|
|
100
|
+
pid: row.pid,
|
|
101
|
+
agent: row.agent,
|
|
102
|
+
status: row.stat.includes('Z') ? 'zombie' : row.stat.includes('T') ? 'stopped' : 'active',
|
|
103
|
+
cwd,
|
|
104
|
+
repo: repoLabel(cwd),
|
|
105
|
+
branch: gitBranch(cwd, deps.execFile),
|
|
106
|
+
cpu: row.cpu,
|
|
107
|
+
mem: row.mem,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function loadTasks(root, deps) {
|
|
113
|
+
const file = path.join(root, '.atris', 'state', 'tasks.projection.json');
|
|
114
|
+
if (!deps.exists(file)) return [];
|
|
115
|
+
const payload = safeJson(deps.readFile(file, 'utf8'), {});
|
|
116
|
+
const tasks = Array.isArray(payload.tasks) ? payload.tasks : [];
|
|
117
|
+
return tasks.map(task => ({
|
|
118
|
+
id: task.id,
|
|
119
|
+
display_id: task.display_id,
|
|
120
|
+
legacy_ref: task.legacy_ref,
|
|
121
|
+
title: task.title,
|
|
122
|
+
status: task.status,
|
|
123
|
+
tag: task.tag,
|
|
124
|
+
workspace_root: task.workspace_root,
|
|
125
|
+
claimed_by: task.claimed_by,
|
|
126
|
+
assigned_to: task.metadata?.assigned_to || task.assigned_to || null,
|
|
127
|
+
metadata: task.metadata || {},
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
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
|
+
|
|
170
|
+
function readJsonFile(file, deps, fallback = null) {
|
|
171
|
+
if (!deps.exists(file)) return fallback;
|
|
172
|
+
return safeJson(deps.readFile(file, 'utf8'), fallback);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function countJsonLines(file, deps) {
|
|
176
|
+
return readJsonLines(file, deps.readFile, deps.exists).length;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function loadMissions(root, deps, nowMs) {
|
|
180
|
+
const file = path.join(root, '.atris', 'state', 'missions.jsonl');
|
|
181
|
+
const byId = new Map();
|
|
182
|
+
for (const mission of readJsonLines(file, deps.readFile, deps.exists)) {
|
|
183
|
+
if (mission && mission.id) byId.set(mission.id, mission);
|
|
184
|
+
}
|
|
185
|
+
return [...byId.values()].map(mission => {
|
|
186
|
+
const lastTick = mission.last_tick_at ? Date.parse(mission.last_tick_at) : 0;
|
|
187
|
+
const stale = mission.status === 'running' && (!mission.verifier || !lastTick || nowMs - lastTick > 3 * 24 * 60 * 60 * 1000);
|
|
188
|
+
return { ...mission, stale };
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function parseWorktrees(text) {
|
|
193
|
+
const out = [];
|
|
194
|
+
let current = {};
|
|
195
|
+
for (const raw of `${text || ''}\n`.split(/\r?\n/)) {
|
|
196
|
+
const line = raw.trim();
|
|
197
|
+
if (!line) {
|
|
198
|
+
if (current.worktree) {
|
|
199
|
+
out.push({
|
|
200
|
+
path: current.worktree,
|
|
201
|
+
branch: String(current.branch || 'detached').replace(/^refs\/heads\//, ''),
|
|
202
|
+
head: current.HEAD || '',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
current = {};
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const idx = line.indexOf(' ');
|
|
209
|
+
if (idx === -1) current[line] = true;
|
|
210
|
+
else current[line.slice(0, idx)] = line.slice(idx + 1);
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function dirtyCount(worktreePath, execFile) {
|
|
216
|
+
try {
|
|
217
|
+
const out = String(execFile('git', ['-C', worktreePath, 'status', '--porcelain'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }));
|
|
218
|
+
return out.split(/\r?\n/).filter(Boolean).length;
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function loadWorktrees(root, deps) {
|
|
225
|
+
let raw = '';
|
|
226
|
+
try {
|
|
227
|
+
raw = deps.execFile('git', ['-C', root, 'worktree', 'list', '--porcelain'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
228
|
+
} catch {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
return parseWorktrees(raw).map(wt => ({ ...wt, dirty: dirtyCount(wt.path, deps.execFile) }));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function loadXp(root, deps) {
|
|
235
|
+
const projection = readJsonFile(path.join(root, '.atris', 'state', 'career_xp.projection.json'), deps, {});
|
|
236
|
+
return {
|
|
237
|
+
metric: projection.metric_label || 'AgentXP',
|
|
238
|
+
total: Number(projection.total_agent_xp ?? projection.agent_xp ?? projection.total_xp ?? 0) || 0,
|
|
239
|
+
today: Number(projection.today_agent_xp ?? projection.today_xp ?? 0) || 0,
|
|
240
|
+
level: Number(projection.level ?? projection.career?.level ?? 0) || 0,
|
|
241
|
+
integrity: projection.integrity_status || 'unknown',
|
|
242
|
+
leaderboard_eligible: Boolean(projection.leaderboard_eligible),
|
|
243
|
+
receipts: countJsonLines(path.join(root, '.atris', 'state', 'career_xp_receipts.jsonl'), deps),
|
|
244
|
+
generated_at: projection.generated_at || null,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function memberTitle(markdown, fallback) {
|
|
249
|
+
const heading = String(markdown || '').split(/\r?\n/).find(line => /^#\s+/.test(line));
|
|
250
|
+
return heading ? heading.replace(/^#\s+/, '').trim() : fallback;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function loadTeam(root, deps) {
|
|
254
|
+
const teamDir = path.join(root, 'atris', 'team');
|
|
255
|
+
if (!deps.exists(teamDir)) return { total: 0, active_goal_members: 0, members: [] };
|
|
256
|
+
let names = [];
|
|
257
|
+
try {
|
|
258
|
+
names = deps.readdir(teamDir).filter(name => !name.startsWith('_'));
|
|
259
|
+
} catch {
|
|
260
|
+
return { total: 0, active_goal_members: 0, members: [] };
|
|
261
|
+
}
|
|
262
|
+
const members = [];
|
|
263
|
+
for (const name of names) {
|
|
264
|
+
const memberFile = path.join(teamDir, name, 'MEMBER.md');
|
|
265
|
+
if (!deps.exists(memberFile)) continue;
|
|
266
|
+
const goals = readJsonFile(path.join(teamDir, name, 'goals.json'), deps, { goals: [] }) || { goals: [] };
|
|
267
|
+
const activeGoals = Array.isArray(goals.goals) ? goals.goals.filter(goal => goal.status === 'active') : [];
|
|
268
|
+
const nowFile = path.join(teamDir, name, 'now.md');
|
|
269
|
+
members.push({
|
|
270
|
+
slug: name,
|
|
271
|
+
title: memberTitle(deps.readFile(memberFile, 'utf8'), name),
|
|
272
|
+
active_goals: activeGoals.length,
|
|
273
|
+
current_goal: activeGoals[0]?.title || null,
|
|
274
|
+
has_now: deps.exists(nowFile),
|
|
275
|
+
updated_at: goals.updated_at || null,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
total: members.length,
|
|
280
|
+
active_goal_members: members.filter(member => member.active_goals > 0).length,
|
|
281
|
+
members,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function loadBrain(root, deps) {
|
|
286
|
+
const scorecards = readJsonLines(path.join(root, '.atris', 'state', 'scorecards.jsonl'), deps.readFile, deps.exists);
|
|
287
|
+
const operatorDir = path.join(root, '.atris', 'state', 'operator-scorecards');
|
|
288
|
+
let operatorScorecards = 0;
|
|
289
|
+
try {
|
|
290
|
+
if (deps.exists(operatorDir)) operatorScorecards = deps.readdir(operatorDir).filter(name => name.endsWith('.json')).length;
|
|
291
|
+
} catch {}
|
|
292
|
+
const latest = [...scorecards].reverse().find(row => row && (row.type === 'scorecard' || row.schema));
|
|
293
|
+
return {
|
|
294
|
+
scorecards: scorecards.length,
|
|
295
|
+
operator_scorecards: operatorScorecards,
|
|
296
|
+
latest_reward: latest?.reward ?? latest?.score ?? null,
|
|
297
|
+
latest_next: latest?.next_task_suggestion || latest?.next || null,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function listNames(dir, deps) {
|
|
302
|
+
try {
|
|
303
|
+
if (!deps.exists(dir)) return [];
|
|
304
|
+
return deps.readdir(dir);
|
|
305
|
+
} catch {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function countDirectoryEntries(dir, deps, predicate = () => true) {
|
|
311
|
+
return listNames(dir, deps).filter(predicate).length;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function loadBusinessCollaboration(root, deps, team = {}) {
|
|
315
|
+
const business = readJsonFile(path.join(root, '.atris', 'business.json'), deps, null);
|
|
316
|
+
const runtime = readJsonFile(path.join(root, '.atris', 'state', 'runtime.json'), deps, null);
|
|
317
|
+
const sync = readJsonFile(path.join(root, '.atris', 'state', '_sync.json'), deps, null);
|
|
318
|
+
const cache = readJsonFile(path.join(deps.homeDir || os.homedir(), '.atris', 'businesses.json'), deps, {}) || {};
|
|
319
|
+
const slug = business?.slug || sync?.workspace_slug || null;
|
|
320
|
+
const cacheEntry = slug && cache ? cache[slug] : null;
|
|
321
|
+
const hasAtris = deps.exists(path.join(root, 'atris'));
|
|
322
|
+
const hasMap = deps.exists(path.join(root, 'atris', 'MAP.md'));
|
|
323
|
+
const hasTodo = deps.exists(path.join(root, 'atris', 'TODO.md'));
|
|
324
|
+
const hasPersona = deps.exists(path.join(root, 'atris', 'PERSONA.md'));
|
|
325
|
+
const ingestPacks = countDirectoryEntries(path.join(root, 'atris', 'context', '_ingest'), deps, name => !name.startsWith('.'));
|
|
326
|
+
const starterBriefs = countDirectoryEntries(path.join(root, 'atris', 'wiki', 'briefs'), deps, name => /starter-brief\.md$/i.test(name));
|
|
327
|
+
const firstLoops = countDirectoryEntries(path.join(root, 'atris', 'wiki', 'concepts'), deps, name => /first-loop/i.test(name));
|
|
328
|
+
const reports = countDirectoryEntries(path.join(root, 'atris', 'reports'), deps, name => /\.(md|json)$/i.test(name));
|
|
329
|
+
const onePagers = countDirectoryEntries(path.join(root, 'atris', 'reports'), deps, name => /one-pager|cheat-sheet|onboarding/i.test(name));
|
|
330
|
+
const localReceipts = countDirectoryEntries(path.join(root, '.atris', 'receipts'), deps, name => /\.(json|md|txt)$/i.test(name));
|
|
331
|
+
const events = countJsonLines(path.join(root, '.atris', 'state', 'events.jsonl'), deps);
|
|
332
|
+
const episodes = countJsonLines(path.join(root, '.atris', 'state', 'episodes.jsonl'), deps);
|
|
333
|
+
const scorecards = countJsonLines(path.join(root, '.atris', 'state', 'scorecards.jsonl'), deps);
|
|
334
|
+
const computerDirs = countDirectoryEntries(path.join(root, 'atris', 'computers'), deps, name => !name.startsWith('.'));
|
|
335
|
+
const hasOnboarding = ingestPacks > 0 || starterBriefs > 0 || onePagers > 0;
|
|
336
|
+
const hasProofLoop = events > 0 || episodes > 0 || scorecards > 0 || localReceipts > 0;
|
|
337
|
+
const hasTeam = Number(team.total || 0) > 0;
|
|
338
|
+
const missing = [];
|
|
339
|
+
if (!business) missing.push('business binding');
|
|
340
|
+
if (!hasAtris || !hasMap || !hasTodo || !hasPersona) missing.push('canonical atris scaffold');
|
|
341
|
+
if (!runtime) missing.push('runtime receipt');
|
|
342
|
+
if (!sync) missing.push('sync state');
|
|
343
|
+
if (!hasTeam) missing.push('team members');
|
|
344
|
+
if (!hasOnboarding) missing.push('onboarding intake/brief');
|
|
345
|
+
if (firstLoops < 1) missing.push('first loop');
|
|
346
|
+
if (!hasProofLoop) missing.push('proof/scorecard loop');
|
|
347
|
+
return {
|
|
348
|
+
bound: Boolean(business),
|
|
349
|
+
slug,
|
|
350
|
+
name: business?.name || cacheEntry?.name || slug || null,
|
|
351
|
+
business_id: business?.business_id || cacheEntry?.business_id || null,
|
|
352
|
+
workspace_id: business?.workspace_id || cacheEntry?.workspace_id || null,
|
|
353
|
+
template: business?.workspace_template || sync?.workspace_template || 'unknown',
|
|
354
|
+
cache_bound: Boolean(cacheEntry),
|
|
355
|
+
scaffold: { atris: hasAtris, map: hasMap, todo: hasTodo, persona: hasPersona },
|
|
356
|
+
runtime: runtime ? {
|
|
357
|
+
scope: runtime.scope || null,
|
|
358
|
+
install_status: runtime.install_status || null,
|
|
359
|
+
sync_status: runtime.sync_status || null,
|
|
360
|
+
} : null,
|
|
361
|
+
onboarding: { packs: ingestPacks, starter_briefs: starterBriefs, first_loops: firstLoops, one_pagers: onePagers, reports },
|
|
362
|
+
proof: { events, episodes, scorecards, receipts: localReceipts },
|
|
363
|
+
computers: computerDirs,
|
|
364
|
+
team_members: Number(team.total || 0),
|
|
365
|
+
active_goal_members: Number(team.active_goal_members || 0),
|
|
366
|
+
share_ready: missing.length === 0,
|
|
367
|
+
missing,
|
|
368
|
+
next_action: missing.length > 0 ? `add ${missing[0]}` : 'share workspace and start first proof loop',
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function loadSwarlo(tasks) {
|
|
373
|
+
const handoffs = tasks
|
|
374
|
+
.filter(task => task.metadata?.delegate_via || task.metadata?.swarlo_channel || task.assigned_to)
|
|
375
|
+
.map(task => ({
|
|
376
|
+
task: taskRef(task),
|
|
377
|
+
status: task.status,
|
|
378
|
+
assigned_to: task.assigned_to || task.metadata?.assigned_to || null,
|
|
379
|
+
delegate_via: task.metadata?.delegate_via || null,
|
|
380
|
+
swarlo_channel: task.metadata?.swarlo_channel || null,
|
|
381
|
+
}));
|
|
382
|
+
return {
|
|
383
|
+
handoffs: handoffs.length,
|
|
384
|
+
swarlo_leases: handoffs.filter(row => row.delegate_via === 'swarlo' || row.swarlo_channel).length,
|
|
385
|
+
local_delegations: handoffs.filter(row => row.delegate_via && row.delegate_via !== 'swarlo').length,
|
|
386
|
+
rows: handoffs,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function loadLoop(missions, root, deps) {
|
|
391
|
+
const events = readJsonLines(path.join(root, '.atris', 'state', 'mission_events.jsonl'), deps.readFile, deps.exists);
|
|
392
|
+
const codexGoal = readJsonFile(path.join(root, '.atris', 'state', 'codex_goal.json'), deps, {}) || {};
|
|
393
|
+
const tickEvents = events.filter(event => event.type === 'mission_tick');
|
|
394
|
+
return {
|
|
395
|
+
running: missions.filter(mission => mission.status === 'running').length,
|
|
396
|
+
always_on: missions.filter(mission => mission.always_on).length,
|
|
397
|
+
stale: missions.filter(mission => mission.stale).length,
|
|
398
|
+
no_verifier: missions.filter(mission => mission.status === 'running' && !mission.verifier).length,
|
|
399
|
+
ticks: tickEvents.length,
|
|
400
|
+
last_event_at: events[events.length - 1]?.at || null,
|
|
401
|
+
codex_goal: codexGoal.goal?.objective || null,
|
|
402
|
+
codex_goal_mission: codexGoal.goal?.mission_id || null,
|
|
403
|
+
infinite_loop_risk: missions.some(mission => mission.stale || (mission.status === 'running' && !mission.verifier)),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function taskRef(task) {
|
|
408
|
+
return task ? (task.display_id || task.legacy_ref || task.id || '-') : '-';
|
|
409
|
+
}
|
|
410
|
+
|
|
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')
|
|
417
|
+
|| null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function ownerForTask(task) {
|
|
421
|
+
if (!task) return '-';
|
|
422
|
+
return task.assigned_to || task.claimed_by || task.metadata?.assigned_to || '-';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function summarize(tasks, missions, worktrees, agents) {
|
|
426
|
+
const count = (rows, pred) => rows.filter(pred).length;
|
|
427
|
+
return {
|
|
428
|
+
agents: { total: agents.length, active: count(agents, a => a.status === 'active'), stopped: count(agents, a => a.status !== 'active') },
|
|
429
|
+
tasks: {
|
|
430
|
+
open: count(tasks, t => t.status === 'open'),
|
|
431
|
+
claimed: count(tasks, t => t.status === 'claimed'),
|
|
432
|
+
review: count(tasks, t => t.status === 'review'),
|
|
433
|
+
certifiedReview: count(tasks, t => t.status === 'review' && t.metadata && t.metadata.agent_certified),
|
|
434
|
+
},
|
|
435
|
+
missions: { running: count(missions, m => m.status === 'running'), stale: count(missions, m => m.stale) },
|
|
436
|
+
worktrees: { total: worktrees.length, dirty: count(worktrees, w => Number(w.dirty) > 0) },
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function nextAction(tasks, missions, worktrees, agents, os = {}) {
|
|
441
|
+
const activeTask = tasks.find(t => t.status === 'claimed' || t.status === 'open');
|
|
442
|
+
if (activeTask) return `work ${taskRef(activeTask)}: ${activeTask.title || 'active task'}`;
|
|
443
|
+
const needsReview = tasks.find(t => t.status === 'review' && !(t.metadata && t.metadata.agent_certified));
|
|
444
|
+
if (needsReview) return `review ${taskRef(needsReview)}: ${needsReview.title || 'uncertified review task'}`;
|
|
445
|
+
const certified = tasks.find(t => t.status === 'review' && t.metadata && t.metadata.agent_certified);
|
|
446
|
+
if (certified) return `human accept/revise ${taskRef(certified)} or clear certified review queue`;
|
|
447
|
+
const stale = missions.find(m => m.stale);
|
|
448
|
+
if (stale) return `close or repair stale mission ${stale.id}`;
|
|
449
|
+
if (os.xp && os.xp.integrity !== 'verified') return `repair ${os.xp.metric || 'AgentXP'} integrity`;
|
|
450
|
+
const dirty = worktrees.find(w => Number(w.dirty) > 0);
|
|
451
|
+
if (dirty) return `inspect dirty worktree ${dirty.path}`;
|
|
452
|
+
if (agents.some(a => !a.task || a.task === '-')) return 'map untasked live agents to tasks or shut down idle sessions';
|
|
453
|
+
return 'no obvious action';
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function collectRadar(options = {}) {
|
|
457
|
+
const root = options.root || process.cwd();
|
|
458
|
+
const deps = {
|
|
459
|
+
execFile: options.execFileSync || execFileSync,
|
|
460
|
+
readFile: options.readFileSync || fs.readFileSync,
|
|
461
|
+
exists: options.existsSync || fs.existsSync,
|
|
462
|
+
readlink: options.readlinkSync || fs.readlinkSync,
|
|
463
|
+
readdir: options.readdirSync || fs.readdirSync,
|
|
464
|
+
platform: options.platform || os.platform(),
|
|
465
|
+
homeDir: options.homeDir || os.homedir(),
|
|
466
|
+
};
|
|
467
|
+
const nowMs = options.nowMs || Date.now();
|
|
468
|
+
const tasks = loadTasks(root, deps);
|
|
469
|
+
const taskCache = new Map([[root, tasks]]);
|
|
470
|
+
const missions = loadMissions(root, deps, nowMs);
|
|
471
|
+
const worktrees = loadWorktrees(root, deps);
|
|
472
|
+
const agents = collectAgents(deps).map(agent => {
|
|
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
|
+
};
|
|
486
|
+
});
|
|
487
|
+
const osState = {
|
|
488
|
+
xp: loadXp(root, deps),
|
|
489
|
+
team: loadTeam(root, deps),
|
|
490
|
+
brain: loadBrain(root, deps),
|
|
491
|
+
swarlo: loadSwarlo(tasks),
|
|
492
|
+
loop: loadLoop(missions, root, deps),
|
|
493
|
+
};
|
|
494
|
+
osState.business = loadBusinessCollaboration(root, deps, osState.team);
|
|
495
|
+
return { root, generated_at: new Date(nowMs).toISOString(), summary: summarize(tasks, missions, worktrees, agents), os: osState, next_action: nextAction(tasks, missions, worktrees, agents, osState), agents, tasks, missions, worktrees };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function renderRadar(data) {
|
|
499
|
+
const lines = [];
|
|
500
|
+
const s = data.summary;
|
|
501
|
+
lines.push('Operator radar');
|
|
502
|
+
lines.push('');
|
|
503
|
+
lines.push(`Agents: ${s.agents.active}/${s.agents.total} active`);
|
|
504
|
+
lines.push(`Tasks: ${s.tasks.open} open, ${s.tasks.claimed} claimed, ${s.tasks.review} review (${s.tasks.certifiedReview} certified)`);
|
|
505
|
+
lines.push(`Missions: ${s.missions.running} running, ${s.missions.stale} stale/no-verifier`);
|
|
506
|
+
lines.push(`Worktrees: ${s.worktrees.total} registered, ${s.worktrees.dirty} dirty`);
|
|
507
|
+
lines.push(`Next: ${data.next_action}`);
|
|
508
|
+
if (data.os) {
|
|
509
|
+
const xp = data.os.xp || {};
|
|
510
|
+
const team = data.os.team || {};
|
|
511
|
+
const brain = data.os.brain || {};
|
|
512
|
+
const swarlo = data.os.swarlo || {};
|
|
513
|
+
const loop = data.os.loop || {};
|
|
514
|
+
const business = data.os.business || {};
|
|
515
|
+
const bizLabel = business.bound ? `${business.slug || business.name || 'bound'} ${business.share_ready ? 'share-ready' : 'not-ready'}` : 'no-binding';
|
|
516
|
+
lines.push(`OS: ${xp.metric || 'AgentXP'} ${xp.total || 0} L${xp.level || 0} (${xp.integrity || 'unknown'}), team ${team.total || 0}/${team.active_goal_members || 0} active-goal, business ${bizLabel}, loop ${loop.stale || 0} stale, Swarlo ${swarlo.swarlo_leases || 0} leases/${swarlo.handoffs || 0} handoffs, brain ${brain.scorecards || 0}+${brain.operator_scorecards || 0} scorecards`);
|
|
517
|
+
}
|
|
518
|
+
lines.push('');
|
|
519
|
+
lines.push(`${truncate('PID', 7)} ${truncate('AGENT', 8)} ${truncate('REPO', 24)} ${truncate('BRANCH', 16)} ${truncate('TASK', 10)} ${truncate('OWNER', 14)} ${truncate('STATE', 8)}`);
|
|
520
|
+
for (const agent of data.agents.slice(0, 24)) {
|
|
521
|
+
lines.push(`${truncate(agent.pid, 7)} ${truncate(agent.agent, 8)} ${truncate(agent.repo, 24)} ${truncate(agent.branch, 16)} ${truncate(agent.task, 10)} ${truncate(agent.owner, 14)} ${truncate(agent.status, 8)}`);
|
|
522
|
+
}
|
|
523
|
+
if (data.agents.length > 24) lines.push(`... ${data.agents.length - 24} more agents`);
|
|
524
|
+
const stale = data.missions.filter(m => m.stale).slice(0, 3);
|
|
525
|
+
if (stale.length) {
|
|
526
|
+
lines.push('');
|
|
527
|
+
lines.push('Stale mission candidates:');
|
|
528
|
+
for (const mission of stale) lines.push(`- ${mission.id}: ${mission.next_action || mission.objective || 'review'}`);
|
|
529
|
+
}
|
|
530
|
+
const review = data.tasks.filter(t => t.status === 'review').slice(0, 5);
|
|
531
|
+
if (review.length) {
|
|
532
|
+
lines.push('');
|
|
533
|
+
lines.push('Review queue:');
|
|
534
|
+
for (const task of review) {
|
|
535
|
+
const passes = task.metadata?.agent_review_pass_count || 0;
|
|
536
|
+
const cert = task.metadata?.agent_certified ? 'certified' : `${passes} pass${passes === 1 ? '' : 'es'}`;
|
|
537
|
+
lines.push(`- ${taskRef(task)} ${cert}: ${task.title || 'untitled'}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const dirty = data.worktrees.filter(w => Number(w.dirty) > 0).slice(0, 5);
|
|
541
|
+
if (dirty.length) {
|
|
542
|
+
lines.push('');
|
|
543
|
+
lines.push('Dirty worktrees:');
|
|
544
|
+
for (const worktree of dirty) lines.push(`- ${worktree.dirty} files: ${worktree.path}`);
|
|
545
|
+
}
|
|
546
|
+
if (data.os) {
|
|
547
|
+
const members = data.os.team?.members?.filter(member => member.active_goals > 0).slice(0, 5) || [];
|
|
548
|
+
if (members.length) {
|
|
549
|
+
lines.push('');
|
|
550
|
+
lines.push('Team goals:');
|
|
551
|
+
for (const member of members) lines.push(`- ${member.slug}: ${member.current_goal || `${member.active_goals} active goals`}`);
|
|
552
|
+
}
|
|
553
|
+
const swarloRows = data.os.swarlo?.rows?.slice(0, 5) || [];
|
|
554
|
+
if (swarloRows.length) {
|
|
555
|
+
lines.push('');
|
|
556
|
+
lines.push('Delegation/Swarlo:');
|
|
557
|
+
for (const row of swarloRows) lines.push(`- ${row.task} ${row.delegate_via || 'assigned'} -> ${row.assigned_to || row.swarlo_channel || 'unassigned'}`);
|
|
558
|
+
}
|
|
559
|
+
const xp = data.os.xp || {};
|
|
560
|
+
lines.push('');
|
|
561
|
+
if (data.os.business) {
|
|
562
|
+
const business = data.os.business;
|
|
563
|
+
lines.push(`Business: ${business.bound ? `${business.name || business.slug} (${business.slug || 'no-slug'})` : 'no local business binding'}`);
|
|
564
|
+
if (business.bound) {
|
|
565
|
+
lines.push(`Business ready: ${business.share_ready ? 'yes' : 'no'}; team ${business.team_members || 0}/${business.active_goal_members || 0} active-goal; onboarding ${business.onboarding?.packs || 0} packs/${business.onboarding?.starter_briefs || 0} briefs/${business.onboarding?.first_loops || 0} loops; proof ${business.proof?.scorecards || 0} scorecards/${business.proof?.events || 0} events; computers ${business.computers || 0}`);
|
|
566
|
+
if (!business.share_ready) lines.push(`Business next: ${business.next_action}`);
|
|
567
|
+
} else {
|
|
568
|
+
lines.push('Business next: run `atris business init <name>` or `atris pull <slug>` before sharing work.');
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
lines.push(`${xp.metric || 'AgentXP'}: ${xp.total || 0} total, ${xp.today || 0} today, ${xp.receipts || 0} receipts, integrity ${xp.integrity || 'unknown'}`);
|
|
572
|
+
if (data.os.loop?.codex_goal) {
|
|
573
|
+
lines.push(`Codex goal: ${data.os.loop.codex_goal}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return lines.join('\n');
|
|
577
|
+
}
|
|
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
|
+
|
|
727
|
+
function radarCommand(args = [], options = {}) {
|
|
728
|
+
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
729
|
+
console.log('Usage: atris radar [--json] [--agents]');
|
|
730
|
+
console.log('Usage: atris radar agents');
|
|
731
|
+
console.log('Usage: atris ctop');
|
|
732
|
+
console.log('');
|
|
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.');
|
|
735
|
+
return 0;
|
|
736
|
+
}
|
|
737
|
+
const data = collectRadar(options);
|
|
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));
|
|
741
|
+
else console.log(renderRadar(data));
|
|
742
|
+
return 0;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
module.exports = {
|
|
746
|
+
agentTypeForCommand,
|
|
747
|
+
agentTopPayload,
|
|
748
|
+
collectRadar,
|
|
749
|
+
parsePsOutput,
|
|
750
|
+
parseWorktrees,
|
|
751
|
+
radarCommand,
|
|
752
|
+
renderAgentTop,
|
|
753
|
+
renderRadar,
|
|
754
|
+
};
|