atris 3.15.45 → 3.15.48

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.
Files changed (74) hide show
  1. package/AGENTS.md +2 -2
  2. package/atris/skills/atris/SKILL.md +1 -1
  3. package/bin/atris.js +10 -4
  4. package/commands/business.js +574 -21
  5. package/commands/computer.js +1 -1
  6. package/commands/mission.js +16 -1
  7. package/commands/radar.js +546 -0
  8. package/commands/sync.js +101 -1
  9. package/lib/runtime-bootstrap.js +17 -5
  10. package/lib/task-db.js +29 -3
  11. package/package.json +3 -1
  12. package/templates/business-starter/CLAUDE.md +62 -0
  13. package/templates/business-starter/MAP.md +81 -0
  14. package/templates/business-starter/MEMBER.md +46 -0
  15. package/templates/business-starter/TODO.md +28 -0
  16. package/templates/business-starter/atris.md +61 -0
  17. package/templates/business-starter/context/README.md +19 -0
  18. package/templates/business-starter/context/live-workspace.md +36 -0
  19. package/templates/business-starter/goals.md +33 -0
  20. package/templates/business-starter/instructions.md +40 -0
  21. package/templates/business-starter/memory.md +31 -0
  22. package/templates/business-starter/persona.md +26 -0
  23. package/templates/business-starter/policies/LESSONS.md +5 -0
  24. package/templates/business-starter/policies/REWARD.md +24 -0
  25. package/templates/business-starter/reports/README.md +17 -0
  26. package/templates/business-starter/reports/operating-recap-template.md +44 -0
  27. package/templates/business-starter/skills/README.md +21 -0
  28. package/templates/business-starter/team/README.md +20 -0
  29. package/templates/business-starter/team/START_HERE.md +45 -0
  30. package/templates/business-starter/team/_template/MEMBER.md +32 -0
  31. package/templates/business-starter/team/_template/SOUL.md +40 -0
  32. package/templates/business-starter/team/comms/MEMBER.md +34 -0
  33. package/templates/business-starter/team/comms/SOUL.md +32 -0
  34. package/templates/business-starter/team/operator/MEMBER.md +34 -0
  35. package/templates/business-starter/team/ops/MEMBER.md +34 -0
  36. package/templates/business-starter/team/ops/SOUL.md +32 -0
  37. package/templates/business-starter/team/research/MEMBER.md +34 -0
  38. package/templates/business-starter/team/research/SOUL.md +32 -0
  39. package/templates/business-starter/team/validator/MEMBER.md +34 -0
  40. package/templates/business-starter/wiki/STATUS.md +7 -0
  41. package/templates/business-starter/wiki/concepts/first-loop-template.md +34 -0
  42. package/templates/business-starter/wiki/index.md +30 -0
  43. package/templates/business-starter/wiki/log.md +11 -0
  44. package/templates/business-starter/wiki/wiki.md +26 -0
  45. package/templates/research-canonical/CLAUDE.md +70 -0
  46. package/templates/research-canonical/MAP.md +68 -0
  47. package/templates/research-canonical/MEMBER.md +46 -0
  48. package/templates/research-canonical/TODO.md +28 -0
  49. package/templates/research-canonical/atris.md +62 -0
  50. package/templates/research-canonical/context/README.md +21 -0
  51. package/templates/research-canonical/context/live-workspace.md +24 -0
  52. package/templates/research-canonical/goals.md +23 -0
  53. package/templates/research-canonical/instructions.md +40 -0
  54. package/templates/research-canonical/memory.md +31 -0
  55. package/templates/research-canonical/persona.md +26 -0
  56. package/templates/research-canonical/policies/LESSONS.md +5 -0
  57. package/templates/research-canonical/policies/REWARD.md +21 -0
  58. package/templates/research-canonical/reports/README.md +17 -0
  59. package/templates/research-canonical/skills/README.md +21 -0
  60. package/templates/research-canonical/team/README.md +11 -0
  61. package/templates/research-canonical/team/eval/MEMBER.md +16 -0
  62. package/templates/research-canonical/team/eval/SOUL.md +32 -0
  63. package/templates/research-canonical/team/experiment/MEMBER.md +16 -0
  64. package/templates/research-canonical/team/experiment/SOUL.md +32 -0
  65. package/templates/research-canonical/team/hypothesis/MEMBER.md +16 -0
  66. package/templates/research-canonical/team/hypothesis/SOUL.md +32 -0
  67. package/templates/research-canonical/team/literature/MEMBER.md +16 -0
  68. package/templates/research-canonical/team/literature/SOUL.md +32 -0
  69. package/templates/research-canonical/wiki/STATUS.md +7 -0
  70. package/templates/research-canonical/wiki/briefs/research-program.md +19 -0
  71. package/templates/research-canonical/wiki/concepts/research-loop.md +14 -0
  72. package/templates/research-canonical/wiki/index.md +25 -0
  73. package/templates/research-canonical/wiki/log.md +10 -0
  74. package/templates/research-canonical/wiki/wiki.md +26 -0
@@ -1289,7 +1289,7 @@ async function bootstrapBusinessComputerRuntime(token, ctx, boundary = 'computer
1289
1289
  const result = await runBusinessTerminalCommand(token, ctx, command, 120);
1290
1290
  if (!result.ok) {
1291
1291
  console.log(' Runtime: bootstrap could not run.');
1292
- console.log(` Recovery: atris computer run "npm install -g atris@latest" --business ${ctx.slug || ctx.businessId} --workspace ${ctx.workspaceId}`);
1292
+ console.log(` Recovery: atris computer run "npm install --prefix /workspace/.atris-npm atris@latest && /workspace/.atris-npm/node_modules/.bin/atris update" --business ${ctx.slug || ctx.businessId} --workspace ${ctx.workspaceId}`);
1293
1293
  return { ok: false, result };
1294
1294
  }
1295
1295
 
@@ -622,9 +622,23 @@ function writeReceipt(mission, result, root = process.cwd()) {
622
622
  return path.relative(root, receiptPath);
623
623
  }
624
624
 
625
+ function shellQuote(value) {
626
+ return `'${String(value || '').replace(/'/g, `'\\''`)}'`;
627
+ }
628
+
629
+ function resolveVerifierCommand(command) {
630
+ const raw = String(command || '');
631
+ const leading = raw.match(/^\s*/)?.[0] || '';
632
+ const trimmed = raw.trimStart();
633
+ if (!trimmed || !/^atris(?:\s|$)/.test(trimmed)) return raw;
634
+ const cliPath = path.resolve(__dirname, '..', 'bin', 'atris.js');
635
+ return `${leading}${shellQuote(process.execPath)} ${shellQuote(cliPath)}${trimmed.slice('atris'.length)}`;
636
+ }
637
+
625
638
  function runVerifier(command, root = process.cwd()) {
626
639
  if (!command) return null;
627
- const result = spawnSync(command, {
640
+ const resolvedCommand = resolveVerifierCommand(command);
641
+ const result = spawnSync(resolvedCommand, {
628
642
  cwd: root,
629
643
  shell: true,
630
644
  encoding: 'utf8',
@@ -633,6 +647,7 @@ function runVerifier(command, root = process.cwd()) {
633
647
  });
634
648
  return {
635
649
  command,
650
+ resolved_command: resolvedCommand === command ? null : resolvedCommand,
636
651
  status: result.status,
637
652
  signal: result.signal || null,
638
653
  passed: result.status === 0,
@@ -0,0 +1,546 @@
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 readJsonFile(file, deps, fallback = null) {
132
+ if (!deps.exists(file)) return fallback;
133
+ return safeJson(deps.readFile(file, 'utf8'), fallback);
134
+ }
135
+
136
+ function countJsonLines(file, deps) {
137
+ return readJsonLines(file, deps.readFile, deps.exists).length;
138
+ }
139
+
140
+ function loadMissions(root, deps, nowMs) {
141
+ const file = path.join(root, '.atris', 'state', 'missions.jsonl');
142
+ const byId = new Map();
143
+ for (const mission of readJsonLines(file, deps.readFile, deps.exists)) {
144
+ if (mission && mission.id) byId.set(mission.id, mission);
145
+ }
146
+ return [...byId.values()].map(mission => {
147
+ const lastTick = mission.last_tick_at ? Date.parse(mission.last_tick_at) : 0;
148
+ const stale = mission.status === 'running' && (!mission.verifier || !lastTick || nowMs - lastTick > 3 * 24 * 60 * 60 * 1000);
149
+ return { ...mission, stale };
150
+ });
151
+ }
152
+
153
+ function parseWorktrees(text) {
154
+ const out = [];
155
+ let current = {};
156
+ for (const raw of `${text || ''}\n`.split(/\r?\n/)) {
157
+ const line = raw.trim();
158
+ if (!line) {
159
+ if (current.worktree) {
160
+ out.push({
161
+ path: current.worktree,
162
+ branch: String(current.branch || 'detached').replace(/^refs\/heads\//, ''),
163
+ head: current.HEAD || '',
164
+ });
165
+ }
166
+ current = {};
167
+ continue;
168
+ }
169
+ const idx = line.indexOf(' ');
170
+ if (idx === -1) current[line] = true;
171
+ else current[line.slice(0, idx)] = line.slice(idx + 1);
172
+ }
173
+ return out;
174
+ }
175
+
176
+ function dirtyCount(worktreePath, execFile) {
177
+ try {
178
+ const out = String(execFile('git', ['-C', worktreePath, 'status', '--porcelain'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }));
179
+ return out.split(/\r?\n/).filter(Boolean).length;
180
+ } catch {
181
+ return null;
182
+ }
183
+ }
184
+
185
+ function loadWorktrees(root, deps) {
186
+ let raw = '';
187
+ try {
188
+ raw = deps.execFile('git', ['-C', root, 'worktree', 'list', '--porcelain'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
189
+ } catch {
190
+ return [];
191
+ }
192
+ return parseWorktrees(raw).map(wt => ({ ...wt, dirty: dirtyCount(wt.path, deps.execFile) }));
193
+ }
194
+
195
+ function loadXp(root, deps) {
196
+ const projection = readJsonFile(path.join(root, '.atris', 'state', 'career_xp.projection.json'), deps, {});
197
+ return {
198
+ metric: projection.metric_label || 'AgentXP',
199
+ total: Number(projection.total_agent_xp ?? projection.agent_xp ?? projection.total_xp ?? 0) || 0,
200
+ today: Number(projection.today_agent_xp ?? projection.today_xp ?? 0) || 0,
201
+ level: Number(projection.level ?? projection.career?.level ?? 0) || 0,
202
+ integrity: projection.integrity_status || 'unknown',
203
+ leaderboard_eligible: Boolean(projection.leaderboard_eligible),
204
+ receipts: countJsonLines(path.join(root, '.atris', 'state', 'career_xp_receipts.jsonl'), deps),
205
+ generated_at: projection.generated_at || null,
206
+ };
207
+ }
208
+
209
+ function memberTitle(markdown, fallback) {
210
+ const heading = String(markdown || '').split(/\r?\n/).find(line => /^#\s+/.test(line));
211
+ return heading ? heading.replace(/^#\s+/, '').trim() : fallback;
212
+ }
213
+
214
+ function loadTeam(root, deps) {
215
+ const teamDir = path.join(root, 'atris', 'team');
216
+ if (!deps.exists(teamDir)) return { total: 0, active_goal_members: 0, members: [] };
217
+ let names = [];
218
+ try {
219
+ names = deps.readdir(teamDir).filter(name => !name.startsWith('_'));
220
+ } catch {
221
+ return { total: 0, active_goal_members: 0, members: [] };
222
+ }
223
+ const members = [];
224
+ for (const name of names) {
225
+ const memberFile = path.join(teamDir, name, 'MEMBER.md');
226
+ if (!deps.exists(memberFile)) continue;
227
+ const goals = readJsonFile(path.join(teamDir, name, 'goals.json'), deps, { goals: [] }) || { goals: [] };
228
+ const activeGoals = Array.isArray(goals.goals) ? goals.goals.filter(goal => goal.status === 'active') : [];
229
+ const nowFile = path.join(teamDir, name, 'now.md');
230
+ members.push({
231
+ slug: name,
232
+ title: memberTitle(deps.readFile(memberFile, 'utf8'), name),
233
+ active_goals: activeGoals.length,
234
+ current_goal: activeGoals[0]?.title || null,
235
+ has_now: deps.exists(nowFile),
236
+ updated_at: goals.updated_at || null,
237
+ });
238
+ }
239
+ return {
240
+ total: members.length,
241
+ active_goal_members: members.filter(member => member.active_goals > 0).length,
242
+ members,
243
+ };
244
+ }
245
+
246
+ function loadBrain(root, deps) {
247
+ const scorecards = readJsonLines(path.join(root, '.atris', 'state', 'scorecards.jsonl'), deps.readFile, deps.exists);
248
+ const operatorDir = path.join(root, '.atris', 'state', 'operator-scorecards');
249
+ let operatorScorecards = 0;
250
+ try {
251
+ if (deps.exists(operatorDir)) operatorScorecards = deps.readdir(operatorDir).filter(name => name.endsWith('.json')).length;
252
+ } catch {}
253
+ const latest = [...scorecards].reverse().find(row => row && (row.type === 'scorecard' || row.schema));
254
+ return {
255
+ scorecards: scorecards.length,
256
+ operator_scorecards: operatorScorecards,
257
+ latest_reward: latest?.reward ?? latest?.score ?? null,
258
+ latest_next: latest?.next_task_suggestion || latest?.next || null,
259
+ };
260
+ }
261
+
262
+ function listNames(dir, deps) {
263
+ try {
264
+ if (!deps.exists(dir)) return [];
265
+ return deps.readdir(dir);
266
+ } catch {
267
+ return [];
268
+ }
269
+ }
270
+
271
+ function countDirectoryEntries(dir, deps, predicate = () => true) {
272
+ return listNames(dir, deps).filter(predicate).length;
273
+ }
274
+
275
+ function loadBusinessCollaboration(root, deps, team = {}) {
276
+ const business = readJsonFile(path.join(root, '.atris', 'business.json'), deps, null);
277
+ const runtime = readJsonFile(path.join(root, '.atris', 'state', 'runtime.json'), deps, null);
278
+ const sync = readJsonFile(path.join(root, '.atris', 'state', '_sync.json'), deps, null);
279
+ const cache = readJsonFile(path.join(deps.homeDir || os.homedir(), '.atris', 'businesses.json'), deps, {}) || {};
280
+ const slug = business?.slug || sync?.workspace_slug || null;
281
+ const cacheEntry = slug && cache ? cache[slug] : null;
282
+ const hasAtris = deps.exists(path.join(root, 'atris'));
283
+ const hasMap = deps.exists(path.join(root, 'atris', 'MAP.md'));
284
+ const hasTodo = deps.exists(path.join(root, 'atris', 'TODO.md'));
285
+ const hasPersona = deps.exists(path.join(root, 'atris', 'PERSONA.md'));
286
+ const ingestPacks = countDirectoryEntries(path.join(root, 'atris', 'context', '_ingest'), deps, name => !name.startsWith('.'));
287
+ const starterBriefs = countDirectoryEntries(path.join(root, 'atris', 'wiki', 'briefs'), deps, name => /starter-brief\.md$/i.test(name));
288
+ const firstLoops = countDirectoryEntries(path.join(root, 'atris', 'wiki', 'concepts'), deps, name => /first-loop/i.test(name));
289
+ const reports = countDirectoryEntries(path.join(root, 'atris', 'reports'), deps, name => /\.(md|json)$/i.test(name));
290
+ const onePagers = countDirectoryEntries(path.join(root, 'atris', 'reports'), deps, name => /one-pager|cheat-sheet|onboarding/i.test(name));
291
+ const localReceipts = countDirectoryEntries(path.join(root, '.atris', 'receipts'), deps, name => /\.(json|md|txt)$/i.test(name));
292
+ const events = countJsonLines(path.join(root, '.atris', 'state', 'events.jsonl'), deps);
293
+ const episodes = countJsonLines(path.join(root, '.atris', 'state', 'episodes.jsonl'), deps);
294
+ const scorecards = countJsonLines(path.join(root, '.atris', 'state', 'scorecards.jsonl'), deps);
295
+ const computerDirs = countDirectoryEntries(path.join(root, 'atris', 'computers'), deps, name => !name.startsWith('.'));
296
+ const hasOnboarding = ingestPacks > 0 || starterBriefs > 0 || onePagers > 0;
297
+ const hasProofLoop = events > 0 || episodes > 0 || scorecards > 0 || localReceipts > 0;
298
+ const hasTeam = Number(team.total || 0) > 0;
299
+ const missing = [];
300
+ if (!business) missing.push('business binding');
301
+ if (!hasAtris || !hasMap || !hasTodo || !hasPersona) missing.push('canonical atris scaffold');
302
+ if (!runtime) missing.push('runtime receipt');
303
+ if (!sync) missing.push('sync state');
304
+ if (!hasTeam) missing.push('team members');
305
+ if (!hasOnboarding) missing.push('onboarding intake/brief');
306
+ if (firstLoops < 1) missing.push('first loop');
307
+ if (!hasProofLoop) missing.push('proof/scorecard loop');
308
+ return {
309
+ bound: Boolean(business),
310
+ slug,
311
+ name: business?.name || cacheEntry?.name || slug || null,
312
+ business_id: business?.business_id || cacheEntry?.business_id || null,
313
+ workspace_id: business?.workspace_id || cacheEntry?.workspace_id || null,
314
+ template: business?.workspace_template || sync?.workspace_template || 'unknown',
315
+ cache_bound: Boolean(cacheEntry),
316
+ scaffold: { atris: hasAtris, map: hasMap, todo: hasTodo, persona: hasPersona },
317
+ runtime: runtime ? {
318
+ scope: runtime.scope || null,
319
+ install_status: runtime.install_status || null,
320
+ sync_status: runtime.sync_status || null,
321
+ } : null,
322
+ onboarding: { packs: ingestPacks, starter_briefs: starterBriefs, first_loops: firstLoops, one_pagers: onePagers, reports },
323
+ proof: { events, episodes, scorecards, receipts: localReceipts },
324
+ computers: computerDirs,
325
+ team_members: Number(team.total || 0),
326
+ active_goal_members: Number(team.active_goal_members || 0),
327
+ share_ready: missing.length === 0,
328
+ missing,
329
+ next_action: missing.length > 0 ? `add ${missing[0]}` : 'share workspace and start first proof loop',
330
+ };
331
+ }
332
+
333
+ function loadSwarlo(tasks) {
334
+ const handoffs = tasks
335
+ .filter(task => task.metadata?.delegate_via || task.metadata?.swarlo_channel || task.assigned_to)
336
+ .map(task => ({
337
+ task: taskRef(task),
338
+ status: task.status,
339
+ assigned_to: task.assigned_to || task.metadata?.assigned_to || null,
340
+ delegate_via: task.metadata?.delegate_via || null,
341
+ swarlo_channel: task.metadata?.swarlo_channel || null,
342
+ }));
343
+ return {
344
+ handoffs: handoffs.length,
345
+ swarlo_leases: handoffs.filter(row => row.delegate_via === 'swarlo' || row.swarlo_channel).length,
346
+ local_delegations: handoffs.filter(row => row.delegate_via && row.delegate_via !== 'swarlo').length,
347
+ rows: handoffs,
348
+ };
349
+ }
350
+
351
+ function loadLoop(missions, root, deps) {
352
+ const events = readJsonLines(path.join(root, '.atris', 'state', 'mission_events.jsonl'), deps.readFile, deps.exists);
353
+ const codexGoal = readJsonFile(path.join(root, '.atris', 'state', 'codex_goal.json'), deps, {}) || {};
354
+ const tickEvents = events.filter(event => event.type === 'mission_tick');
355
+ return {
356
+ running: missions.filter(mission => mission.status === 'running').length,
357
+ always_on: missions.filter(mission => mission.always_on).length,
358
+ stale: missions.filter(mission => mission.stale).length,
359
+ no_verifier: missions.filter(mission => mission.status === 'running' && !mission.verifier).length,
360
+ ticks: tickEvents.length,
361
+ last_event_at: events[events.length - 1]?.at || null,
362
+ codex_goal: codexGoal.goal?.objective || null,
363
+ codex_goal_mission: codexGoal.goal?.mission_id || null,
364
+ infinite_loop_risk: missions.some(mission => mission.stale || (mission.status === 'running' && !mission.verifier)),
365
+ };
366
+ }
367
+
368
+ function taskRef(task) {
369
+ return task ? (task.display_id || task.legacy_ref || task.id || '-') : '-';
370
+ }
371
+
372
+ function taskForCwd(tasks, cwd) {
373
+ if (!cwd) return null;
374
+ return tasks.find(task => task.workspace_root === cwd && ['claimed', 'open'].includes(task.status))
375
+ || tasks.find(task => task.workspace_root === cwd && task.status === 'review')
376
+ || null;
377
+ }
378
+
379
+ function ownerForTask(task) {
380
+ if (!task) return '-';
381
+ return task.assigned_to || task.claimed_by || task.metadata?.assigned_to || '-';
382
+ }
383
+
384
+ function summarize(tasks, missions, worktrees, agents) {
385
+ const count = (rows, pred) => rows.filter(pred).length;
386
+ return {
387
+ agents: { total: agents.length, active: count(agents, a => a.status === 'active'), stopped: count(agents, a => a.status !== 'active') },
388
+ tasks: {
389
+ open: count(tasks, t => t.status === 'open'),
390
+ claimed: count(tasks, t => t.status === 'claimed'),
391
+ review: count(tasks, t => t.status === 'review'),
392
+ certifiedReview: count(tasks, t => t.status === 'review' && t.metadata && t.metadata.agent_certified),
393
+ },
394
+ missions: { running: count(missions, m => m.status === 'running'), stale: count(missions, m => m.stale) },
395
+ worktrees: { total: worktrees.length, dirty: count(worktrees, w => Number(w.dirty) > 0) },
396
+ };
397
+ }
398
+
399
+ function nextAction(tasks, missions, worktrees, agents, os = {}) {
400
+ const activeTask = tasks.find(t => t.status === 'claimed' || t.status === 'open');
401
+ if (activeTask) return `work ${taskRef(activeTask)}: ${activeTask.title || 'active task'}`;
402
+ const needsReview = tasks.find(t => t.status === 'review' && !(t.metadata && t.metadata.agent_certified));
403
+ if (needsReview) return `review ${taskRef(needsReview)}: ${needsReview.title || 'uncertified review task'}`;
404
+ const certified = tasks.find(t => t.status === 'review' && t.metadata && t.metadata.agent_certified);
405
+ if (certified) return `human accept/revise ${taskRef(certified)} or clear certified review queue`;
406
+ const stale = missions.find(m => m.stale);
407
+ if (stale) return `close or repair stale mission ${stale.id}`;
408
+ if (os.xp && os.xp.integrity !== 'verified') return `repair ${os.xp.metric || 'AgentXP'} integrity`;
409
+ const dirty = worktrees.find(w => Number(w.dirty) > 0);
410
+ if (dirty) return `inspect dirty worktree ${dirty.path}`;
411
+ if (agents.some(a => !a.task || a.task === '-')) return 'map untasked live agents to tasks or shut down idle sessions';
412
+ return 'no obvious action';
413
+ }
414
+
415
+ function collectRadar(options = {}) {
416
+ const root = options.root || process.cwd();
417
+ const deps = {
418
+ execFile: options.execFileSync || execFileSync,
419
+ readFile: options.readFileSync || fs.readFileSync,
420
+ exists: options.existsSync || fs.existsSync,
421
+ readlink: options.readlinkSync || fs.readlinkSync,
422
+ readdir: options.readdirSync || fs.readdirSync,
423
+ platform: options.platform || os.platform(),
424
+ homeDir: options.homeDir || os.homedir(),
425
+ };
426
+ const nowMs = options.nowMs || Date.now();
427
+ const tasks = loadTasks(root, deps);
428
+ const missions = loadMissions(root, deps, nowMs);
429
+ const worktrees = loadWorktrees(root, deps);
430
+ const agents = collectAgents(deps).map(agent => {
431
+ const task = taskForCwd(tasks, agent.cwd);
432
+ return { ...agent, task: taskRef(task), task_status: task?.status || null, owner: ownerForTask(task) };
433
+ });
434
+ const osState = {
435
+ xp: loadXp(root, deps),
436
+ team: loadTeam(root, deps),
437
+ brain: loadBrain(root, deps),
438
+ swarlo: loadSwarlo(tasks),
439
+ loop: loadLoop(missions, root, deps),
440
+ };
441
+ osState.business = loadBusinessCollaboration(root, deps, osState.team);
442
+ 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 };
443
+ }
444
+
445
+ function renderRadar(data) {
446
+ const lines = [];
447
+ const s = data.summary;
448
+ lines.push('Operator radar');
449
+ lines.push('');
450
+ lines.push(`Agents: ${s.agents.active}/${s.agents.total} active`);
451
+ lines.push(`Tasks: ${s.tasks.open} open, ${s.tasks.claimed} claimed, ${s.tasks.review} review (${s.tasks.certifiedReview} certified)`);
452
+ lines.push(`Missions: ${s.missions.running} running, ${s.missions.stale} stale/no-verifier`);
453
+ lines.push(`Worktrees: ${s.worktrees.total} registered, ${s.worktrees.dirty} dirty`);
454
+ lines.push(`Next: ${data.next_action}`);
455
+ if (data.os) {
456
+ const xp = data.os.xp || {};
457
+ const team = data.os.team || {};
458
+ const brain = data.os.brain || {};
459
+ const swarlo = data.os.swarlo || {};
460
+ const loop = data.os.loop || {};
461
+ const business = data.os.business || {};
462
+ const bizLabel = business.bound ? `${business.slug || business.name || 'bound'} ${business.share_ready ? 'share-ready' : 'not-ready'}` : 'no-binding';
463
+ 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`);
464
+ }
465
+ lines.push('');
466
+ lines.push(`${truncate('PID', 7)} ${truncate('AGENT', 8)} ${truncate('REPO', 24)} ${truncate('BRANCH', 16)} ${truncate('TASK', 10)} ${truncate('OWNER', 14)} ${truncate('STATE', 8)}`);
467
+ for (const agent of data.agents.slice(0, 24)) {
468
+ 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)}`);
469
+ }
470
+ if (data.agents.length > 24) lines.push(`... ${data.agents.length - 24} more agents`);
471
+ const stale = data.missions.filter(m => m.stale).slice(0, 3);
472
+ if (stale.length) {
473
+ lines.push('');
474
+ lines.push('Stale mission candidates:');
475
+ for (const mission of stale) lines.push(`- ${mission.id}: ${mission.next_action || mission.objective || 'review'}`);
476
+ }
477
+ const review = data.tasks.filter(t => t.status === 'review').slice(0, 5);
478
+ if (review.length) {
479
+ lines.push('');
480
+ lines.push('Review queue:');
481
+ for (const task of review) {
482
+ const passes = task.metadata?.agent_review_pass_count || 0;
483
+ const cert = task.metadata?.agent_certified ? 'certified' : `${passes} pass${passes === 1 ? '' : 'es'}`;
484
+ lines.push(`- ${taskRef(task)} ${cert}: ${task.title || 'untitled'}`);
485
+ }
486
+ }
487
+ const dirty = data.worktrees.filter(w => Number(w.dirty) > 0).slice(0, 5);
488
+ if (dirty.length) {
489
+ lines.push('');
490
+ lines.push('Dirty worktrees:');
491
+ for (const worktree of dirty) lines.push(`- ${worktree.dirty} files: ${worktree.path}`);
492
+ }
493
+ if (data.os) {
494
+ const members = data.os.team?.members?.filter(member => member.active_goals > 0).slice(0, 5) || [];
495
+ if (members.length) {
496
+ lines.push('');
497
+ lines.push('Team goals:');
498
+ for (const member of members) lines.push(`- ${member.slug}: ${member.current_goal || `${member.active_goals} active goals`}`);
499
+ }
500
+ const swarloRows = data.os.swarlo?.rows?.slice(0, 5) || [];
501
+ if (swarloRows.length) {
502
+ lines.push('');
503
+ lines.push('Delegation/Swarlo:');
504
+ for (const row of swarloRows) lines.push(`- ${row.task} ${row.delegate_via || 'assigned'} -> ${row.assigned_to || row.swarlo_channel || 'unassigned'}`);
505
+ }
506
+ const xp = data.os.xp || {};
507
+ lines.push('');
508
+ if (data.os.business) {
509
+ const business = data.os.business;
510
+ lines.push(`Business: ${business.bound ? `${business.name || business.slug} (${business.slug || 'no-slug'})` : 'no local business binding'}`);
511
+ if (business.bound) {
512
+ 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}`);
513
+ if (!business.share_ready) lines.push(`Business next: ${business.next_action}`);
514
+ } else {
515
+ lines.push('Business next: run `atris business init <name>` or `atris pull <slug>` before sharing work.');
516
+ }
517
+ }
518
+ lines.push(`${xp.metric || 'AgentXP'}: ${xp.total || 0} total, ${xp.today || 0} today, ${xp.receipts || 0} receipts, integrity ${xp.integrity || 'unknown'}`);
519
+ if (data.os.loop?.codex_goal) {
520
+ lines.push(`Codex goal: ${data.os.loop.codex_goal}`);
521
+ }
522
+ }
523
+ return lines.join('\n');
524
+ }
525
+
526
+ function radarCommand(args = [], options = {}) {
527
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
528
+ console.log('Usage: atris radar [--json]');
529
+ console.log('');
530
+ console.log('Shows live agent processes joined with Atris tasks, missions, and worktrees.');
531
+ return 0;
532
+ }
533
+ const data = collectRadar(options);
534
+ if (args.includes('--json')) console.log(JSON.stringify(data, null, 2));
535
+ else console.log(renderRadar(data));
536
+ return 0;
537
+ }
538
+
539
+ module.exports = {
540
+ agentTypeForCommand,
541
+ collectRadar,
542
+ parsePsOutput,
543
+ parseWorktrees,
544
+ radarCommand,
545
+ renderRadar,
546
+ };