atris 3.52.0 → 3.55.0

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 (131) hide show
  1. package/FOR_AGENTS.md +7 -0
  2. package/README.md +1 -1
  3. package/atris/policies/ANTISLOP.md +2 -1
  4. package/atris/skills/copy-editor/SKILL.md +2 -1
  5. package/atris/skills/design/SKILL.md +1 -1
  6. package/atris/skills/engines/SKILL.md +8 -7
  7. package/atris/skills/x-search/SKILL.md +24 -22
  8. package/atris/skills/youtube/SKILL.md +89 -40
  9. package/atris/team/_template/SOUL.md +40 -0
  10. package/atris/team/validator/MEMBER.md +1 -2
  11. package/ax +592 -91
  12. package/bin/atris +21 -0
  13. package/bin/atris.js +580 -378
  14. package/commands/api-key.js +170 -0
  15. package/commands/auth.js +453 -43
  16. package/commands/autoland.js +3 -1
  17. package/commands/autopilot-front.js +58 -11
  18. package/commands/avail.js +5 -4
  19. package/commands/balance.js +55 -0
  20. package/commands/bench.js +29 -1
  21. package/commands/brain.js +63 -12
  22. package/commands/brainstorm.js +18 -0
  23. package/commands/business.js +149 -1
  24. package/commands/chat-scan.js +17 -4
  25. package/commands/close.js +5 -1
  26. package/commands/computer.js +2 -2
  27. package/commands/doctor.js +174 -0
  28. package/commands/dream.js +7 -0
  29. package/commands/drill.js +34 -2
  30. package/commands/engine.js +47 -35
  31. package/commands/errors.js +58 -8
  32. package/commands/experiments.js +14 -0
  33. package/commands/feedback.js +61 -2
  34. package/commands/fleet-report.js +27 -7
  35. package/commands/fleet.js +98 -11
  36. package/commands/founder.js +60 -21
  37. package/commands/human-missions.js +79 -4
  38. package/commands/improve.js +54 -4
  39. package/commands/init.js +68 -92
  40. package/commands/install.js +40 -0
  41. package/commands/integrations.js +399 -30
  42. package/commands/interview.js +8 -0
  43. package/commands/land.js +48 -17
  44. package/commands/learn.js +29 -4
  45. package/commands/log.js +51 -0
  46. package/commands/member.js +68 -10
  47. package/commands/mission.js +339 -74
  48. package/commands/now.js +25 -6
  49. package/commands/orb.js +97 -2
  50. package/commands/pack.js +83 -9
  51. package/commands/playbook.js +381 -0
  52. package/commands/plugin.js +16 -0
  53. package/commands/radar.js +96 -40
  54. package/commands/recap.js +128 -21
  55. package/commands/release.js +102 -2
  56. package/commands/review.js +5 -10
  57. package/commands/revisions.js +55 -0
  58. package/commands/run-front.js +12 -4
  59. package/commands/scout.js +12 -1
  60. package/commands/search.js +123 -28
  61. package/commands/sign.js +2 -2
  62. package/commands/signup.js +12 -3
  63. package/commands/site-deploy.js +17 -4
  64. package/commands/site.js +2 -2
  65. package/commands/skill-eval.js +289 -0
  66. package/commands/skill.js +56 -6
  67. package/commands/social.js +603 -0
  68. package/commands/spaceship.js +69 -6
  69. package/commands/stream.js +8 -1
  70. package/commands/study.js +32 -19
  71. package/commands/sync.js +28 -22
  72. package/commands/task.js +418 -60
  73. package/commands/teach.js +72 -63
  74. package/commands/terminal.js +25 -9
  75. package/commands/topup.js +88 -0
  76. package/commands/truth.js +52 -6
  77. package/commands/usage.js +91 -0
  78. package/commands/verify.js +3 -2
  79. package/commands/version.js +3 -1
  80. package/commands/voice.js +5 -1
  81. package/commands/who.js +69 -11
  82. package/commands/wiki.js +7 -4
  83. package/commands/wish.js +76 -5
  84. package/commands/workflow.js +327 -145
  85. package/commands/worktree.js +1 -0
  86. package/commands/write.js +5 -0
  87. package/commands/x-search.js +340 -0
  88. package/commands/xp.js +5 -1
  89. package/commands/youtube.js +551 -33
  90. package/lib/account-bound.js +47 -0
  91. package/lib/auto-accept-certified.js +10 -2
  92. package/lib/auto-lessons.js +186 -0
  93. package/lib/autoland.js +4 -2
  94. package/lib/chat-log-scan.js +7 -4
  95. package/lib/cli-json.js +77 -0
  96. package/lib/cli-scope.js +26 -0
  97. package/lib/codex-flight.js +1 -0
  98. package/lib/conductor-artifacts.js +1 -1
  99. package/lib/context-gatherer.js +11 -0
  100. package/lib/developer-api.js +116 -0
  101. package/lib/engine-ask.js +11 -3
  102. package/lib/engine-registry.js +32 -7
  103. package/lib/feedback-triage.js +274 -0
  104. package/lib/first-minute.js +398 -0
  105. package/lib/fleet.js +115 -34
  106. package/lib/functional-owner.js +22 -0
  107. package/lib/known-commands.js +31 -5
  108. package/lib/member-scaffold.js +197 -0
  109. package/lib/mission-ledger-compact.js +127 -0
  110. package/lib/mission-root.js +4 -2
  111. package/lib/mission-runtime-loop.js +30 -2
  112. package/lib/next-moves.js +34 -34
  113. package/lib/noninteractive.js +88 -0
  114. package/lib/permission-grants.js +10 -1
  115. package/lib/policy-lessons.js +4 -1
  116. package/lib/revision-metric.js +279 -0
  117. package/lib/runner-command.js +5 -4
  118. package/lib/scratch-root.js +48 -0
  119. package/lib/skill-eval-gate.js +249 -0
  120. package/lib/task-db.js +22 -2
  121. package/lib/task-proof.js +75 -9
  122. package/lib/task-receipt.js +5 -1
  123. package/lib/verifier-quality.js +69 -0
  124. package/lib/wish-audit.js +2 -2
  125. package/lib/wish-delegate.js +10 -1
  126. package/lib/workspace-scaffold.js +270 -0
  127. package/package.json +4 -2
  128. package/scripts/det/checklist-score.js +191 -0
  129. package/scripts/det/ytsearch +31 -0
  130. package/scripts/outbound-artifact-gate.js +227 -0
  131. package/utils/update-check.js +15 -0
@@ -0,0 +1,381 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const { execFile } = require('child_process');
7
+
8
+ const TRIAL_PROMPTS = [
9
+ 'Write a brief status update telling your manager the deploy finished and one alert needs review.',
10
+ 'Explain to a teammate in a few sentences why the cache change made startup faster.',
11
+ 'Write a short note announcing that exports now run on a schedule.',
12
+ 'Summarize for a non-engineer what changed in this week\'s release.',
13
+ 'Write a two-paragraph answer on whether we should switch to the new logging library.',
14
+ 'Draft a short reply declining a meeting and proposing async notes instead.',
15
+ ];
16
+ const TRIAL_DRAFTS_PER_ARM = 2;
17
+ const TRIAL_BET = 0.5;
18
+ const JARGON_WORDS = /\b(?:leverage|seamless|robust|delve|synergy)\b/i;
19
+
20
+ function playbookFile(root = process.cwd()) {
21
+ return path.join(root, '.atris', 'state', 'playbook.json');
22
+ }
23
+
24
+ function readPlaybook(root = process.cwd()) {
25
+ try {
26
+ const value = JSON.parse(fs.readFileSync(playbookFile(root), 'utf8'));
27
+ return { version: 1, rules: Array.isArray(value.rules) ? value.rules : [] };
28
+ } catch (err) {
29
+ if (err.code === 'ENOENT') return { version: 1, rules: [] };
30
+ throw err;
31
+ }
32
+ }
33
+
34
+ function writePlaybook(playbook, root = process.cwd()) {
35
+ const file = playbookFile(root);
36
+ fs.mkdirSync(path.dirname(file), { recursive: true });
37
+ fs.writeFileSync(file, `${JSON.stringify(playbook, null, 2)}\n`);
38
+ }
39
+
40
+ function optionValue(args, name) {
41
+ const index = args.indexOf(name);
42
+ if (index === -1) return null;
43
+ if (!args[index + 1] || args[index + 1].startsWith('--')) {
44
+ throw new Error(`${name} needs a value`);
45
+ }
46
+ return args[index + 1];
47
+ }
48
+
49
+ function printHelp(output) {
50
+ output('usage: atris playbook [show] [--family <family>] [--json]');
51
+ output(' atris playbook add <family> <rule> [--source <source>]');
52
+ output(' atris playbook inject [--family <family>]');
53
+ output(' atris playbook remove <id>');
54
+ output(' atris playbook verify <id> --with <score> --without <score> [--method <method>]');
55
+ output(' atris playbook trial [--engine <cmd>] [--max-pairs <n>] [--alpha <a>]');
56
+ }
57
+
58
+ function filteredRules(playbook, family) {
59
+ return family ? playbook.rules.filter((entry) => entry.family === family) : playbook.rules;
60
+ }
61
+
62
+ function mean(values) {
63
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
64
+ }
65
+
66
+ function sentenceCount(paragraph) {
67
+ const value = paragraph.trim();
68
+ if (!value) return 0;
69
+ const endings = value.match(/[.!?]+(?=\s|$)/g) || [];
70
+ return endings.length + (/[.!?]["')\]]*\s*$/.test(value) ? 0 : 1);
71
+ }
72
+
73
+ function gradeText(text) {
74
+ const value = String(text || '').trim();
75
+ const paragraphs = value ? value.split(/\n\s*\n/) : [''];
76
+ const checks = [
77
+ !value.includes('\u2014'),
78
+ paragraphs.every((paragraph) => sentenceCount(paragraph) <= 2),
79
+ !JARGON_WORDS.test(value),
80
+ !/\b[A-Z]{4,}\b/.test(value),
81
+ !value.includes('!'),
82
+ ];
83
+ return checks.filter(Boolean).length / checks.length;
84
+ }
85
+
86
+ function bettingEvidence(differences, direction) {
87
+ const logEvidence = differences.reduce((sum, difference) => (
88
+ sum + Math.log(1 + (direction * TRIAL_BET * difference))
89
+ ), 0);
90
+ return Math.exp(Math.min(logEvidence, Math.log(Number.MAX_VALUE)));
91
+ }
92
+
93
+ function trialPrompt(rules, prompt) {
94
+ const system = [
95
+ 'You are a sharp, plain-spoken assistant. Answer directly and briefly.',
96
+ rules.length ? `Follow every writing rule that applies:\n${rules.map((rule) => `- ${rule}`).join('\n')}` : '',
97
+ ].filter(Boolean).join('\n\n');
98
+ return { system, argv: `${system}\n\nPrompt:\n${prompt}` };
99
+ }
100
+
101
+ function runShellEngine(command, prompt) {
102
+ return new Promise((resolve, reject) => {
103
+ execFile('/bin/sh', ['-c', `${command} "$1"`, 'atris-playbook-engine', prompt], {
104
+ timeout: 120000,
105
+ maxBuffer: 1024 * 1024,
106
+ }, (err, stdout) => {
107
+ if (err) {
108
+ reject(new Error(`trial engine failed: ${err.message || err}`));
109
+ return;
110
+ }
111
+ const reply = String(stdout || '').trim();
112
+ if (!reply) {
113
+ reject(new Error('trial engine returned an empty reply'));
114
+ return;
115
+ }
116
+ resolve(reply);
117
+ });
118
+ });
119
+ }
120
+
121
+ async function runLocalEngine(system, prompt, fetchImpl) {
122
+ let lastError = null;
123
+ for (let attempt = 0; attempt < 3; attempt += 1) {
124
+ try {
125
+ const response = await fetchImpl('http://localhost:11434/v1/chat/completions', {
126
+ method: 'POST',
127
+ headers: { 'content-type': 'application/json' },
128
+ body: JSON.stringify({
129
+ model: 'qwen2.5:7b',
130
+ temperature: 0.8,
131
+ messages: [
132
+ { role: 'system', content: system },
133
+ { role: 'user', content: prompt },
134
+ ],
135
+ }),
136
+ signal: AbortSignal.timeout(120000),
137
+ });
138
+ if (!response.ok) {
139
+ lastError = new Error(`local trial engine returned status ${response.status}`);
140
+ continue;
141
+ }
142
+ const body = await response.json();
143
+ const reply = String(body.choices?.[0]?.message?.content || '').trim();
144
+ if (reply) return reply;
145
+ lastError = new Error('local trial engine returned an empty reply');
146
+ } catch (err) {
147
+ lastError = err;
148
+ }
149
+ }
150
+ throw new Error(`local trial engine failed: ${lastError?.message || 'no reply'}`);
151
+ }
152
+
153
+ async function armScore(rules, prompt, options) {
154
+ const prepared = trialPrompt(rules, prompt);
155
+ const drafts = [];
156
+ for (let index = 0; index < TRIAL_DRAFTS_PER_ARM; index += 1) {
157
+ drafts.push(options.engine
158
+ ? await runShellEngine(options.engine, prepared.argv)
159
+ : await runLocalEngine(prepared.system, prompt, options.fetchImpl));
160
+ }
161
+ return Math.max(...drafts.map(gradeText));
162
+ }
163
+
164
+ function numberedPrompt(index) {
165
+ const base = TRIAL_PROMPTS[index % TRIAL_PROMPTS.length];
166
+ const cycle = Math.floor(index / TRIAL_PROMPTS.length);
167
+ return cycle === 0 ? base : `${base} Variation ${cycle}.`;
168
+ }
169
+
170
+ function trialStats(rule, alphaBudget) {
171
+ const differences = Array.isArray(rule.trial_diffs) ? rule.trial_diffs : [];
172
+ if (differences.length === 0) return null;
173
+ return {
174
+ pairs: differences.length,
175
+ difference: mean(differences),
176
+ killEvidence: bettingEvidence(differences, 1),
177
+ certifyEvidence: bettingEvidence(differences, -1),
178
+ threshold: 1 / alphaBudget,
179
+ };
180
+ }
181
+
182
+ function applyTrialVerdict(rule, stats, options) {
183
+ const quarantined = stats.killEvidence >= stats.threshold;
184
+ const certified = stats.certifyEvidence >= stats.threshold;
185
+ if (!quarantined && !certified) return null;
186
+ const withScores = Array.isArray(rule.trial_scores_with) ? rule.trial_scores_with : [];
187
+ const withoutScores = Array.isArray(rule.trial_scores_without) ? rule.trial_scores_without : [];
188
+ rule.quarantined = quarantined;
189
+ rule.verified = {
190
+ method: 'paired-trial',
191
+ score_with: withScores.length ? mean(withScores) : null,
192
+ score_without: withoutScores.length ? mean(withoutScores) : null,
193
+ pairs: stats.pairs,
194
+ mean_difference: stats.difference,
195
+ kill_evidence: stats.killEvidence,
196
+ certify_evidence: stats.certifyEvidence,
197
+ evidence_threshold: stats.threshold,
198
+ alpha: options.alphaBudget,
199
+ at: options.now(),
200
+ };
201
+ return quarantined ? 'quarantined' : 'certified';
202
+ }
203
+
204
+ async function runRuleTrial(rule, playbook, options) {
205
+ const activeRules = playbook.rules.filter((entry) => entry !== rule && !entry.quarantined);
206
+ const rulesWithout = activeRules.map((entry) => entry.rule);
207
+ const rulesWith = [...rulesWithout, rule.rule];
208
+ rule.trial_diffs = Array.isArray(rule.trial_diffs) ? rule.trial_diffs : [];
209
+ rule.trial_scores_with = Array.isArray(rule.trial_scores_with) ? rule.trial_scores_with : [];
210
+ rule.trial_scores_without = Array.isArray(rule.trial_scores_without) ? rule.trial_scores_without : [];
211
+ options.output(`on trial: ${rule.id} (${rule.trial_diffs.length} prior pairs)`);
212
+
213
+ let stats = trialStats(rule, options.alphaBudget);
214
+ let verdict = stats ? applyTrialVerdict(rule, stats, options) : null;
215
+ while (!verdict && rule.trial_diffs.length < options.maxPairs) {
216
+ const start = rule.trial_diffs.length;
217
+ const prompt = numberedPrompt(start);
218
+ const [scoreWith, scoreWithout] = await Promise.all([
219
+ armScore(rulesWith, prompt, options),
220
+ armScore(rulesWithout, prompt, options),
221
+ ]);
222
+ rule.trial_scores_with.push(scoreWith);
223
+ rule.trial_scores_without.push(scoreWithout);
224
+ rule.trial_diffs.push(Number((scoreWithout - scoreWith).toFixed(6)));
225
+ stats = trialStats(rule, options.alphaBudget);
226
+ verdict = applyTrialVerdict(rule, stats, options);
227
+ writePlaybook(playbook, options.root);
228
+ }
229
+
230
+ if (verdict) {
231
+ writePlaybook(playbook, options.root);
232
+ options.output(`rule ${verdict}: ${rule.id} after ${stats.pairs} pairs`);
233
+ return;
234
+ }
235
+ options.output(`rule undecided after ${rule.trial_diffs.length} pairs: ${rule.id}`);
236
+ }
237
+
238
+ async function runTrials(playbook, rest, options) {
239
+ const rawMaxPairs = optionValue(rest, '--max-pairs');
240
+ const rawAlpha = optionValue(rest, '--alpha');
241
+ const maxPairs = rawMaxPairs === null ? 30 : Number(rawMaxPairs);
242
+ const alpha = rawAlpha === null ? 0.05 : Number(rawAlpha);
243
+ if (!Number.isInteger(maxPairs) || maxPairs < 1) throw new Error('--max-pairs needs a positive integer');
244
+ if (!Number.isFinite(alpha) || alpha <= 0 || alpha >= 1) throw new Error('--alpha needs a number between 0 and 1');
245
+ const engine = optionValue(rest, '--engine');
246
+ const unresolved = playbook.rules.filter((rule) => !rule.verified && rule.quarantined !== true);
247
+ if (unresolved.length === 0) {
248
+ options.output('no unresolved rules');
249
+ return 0;
250
+ }
251
+ const alphaBudget = alpha / Math.max(1, playbook.rules.length);
252
+ for (const rule of unresolved) {
253
+ await runRuleTrial(rule, playbook, {
254
+ ...options,
255
+ engine,
256
+ maxPairs,
257
+ alphaBudget,
258
+ });
259
+ }
260
+ return 0;
261
+ }
262
+
263
+ function playbookCommand(args = [], options = {}) {
264
+ const root = options.root || process.cwd();
265
+ const output = options.output || console.log;
266
+ const now = options.now || (() => new Date().toISOString());
267
+ const makeId = options.makeId || (() => crypto.randomUUID().slice(0, 8));
268
+ const subcommand = args[0] && !args[0].startsWith('--') ? args[0] : 'show';
269
+ const rest = subcommand === 'show' && args[0] !== 'show' ? args : args.slice(1);
270
+
271
+ if (args.includes('--help') || args.includes('-h') || subcommand === 'help') {
272
+ printHelp(output);
273
+ return 0;
274
+ }
275
+
276
+ const playbook = readPlaybook(root);
277
+ if (subcommand === 'trial') {
278
+ return runTrials(playbook, rest, {
279
+ root,
280
+ output,
281
+ now,
282
+ fetchImpl: options.fetch || globalThis.fetch,
283
+ });
284
+ }
285
+ if (subcommand === 'show') {
286
+ const family = optionValue(rest, '--family');
287
+ const rules = filteredRules(playbook, family);
288
+ if (rest.includes('--json')) {
289
+ output(JSON.stringify({ version: 1, rules }, null, 2));
290
+ return 0;
291
+ }
292
+ if (rules.length === 0) {
293
+ output('no style rules saved');
294
+ return 0;
295
+ }
296
+ const families = [...new Set(rules.map((entry) => entry.family))];
297
+ for (const name of families) {
298
+ output(`${name}:`);
299
+ for (const entry of rules.filter((rule) => rule.family === name)) {
300
+ const state = entry.quarantined ? ' (quarantined)' : '';
301
+ output(`- ${entry.id}: ${entry.rule}${state}`);
302
+ }
303
+ }
304
+ return 0;
305
+ }
306
+
307
+ if (subcommand === 'add') {
308
+ const family = rest[0];
309
+ const rule = rest[1];
310
+ if (!family || !rule || family.startsWith('--') || rule.startsWith('--')) {
311
+ throw new Error('add needs a family and rule');
312
+ }
313
+ const duplicate = playbook.rules.find((entry) => entry.family === family && entry.rule === rule);
314
+ if (duplicate) {
315
+ output(`rule already exists: ${duplicate.id}`);
316
+ return 0;
317
+ }
318
+ const entry = {
319
+ id: makeId(),
320
+ family,
321
+ rule,
322
+ source: optionValue(rest, '--source') || '',
323
+ verified: null,
324
+ created_at: now(),
325
+ };
326
+ playbook.rules.push(entry);
327
+ writePlaybook(playbook, root);
328
+ output(`rule added: ${entry.id}`);
329
+ return 0;
330
+ }
331
+
332
+ if (subcommand === 'inject') {
333
+ const family = optionValue(rest, '--family');
334
+ const rules = filteredRules(playbook, family).filter((entry) => !entry.quarantined);
335
+ if (rules.length === 0) return 0;
336
+ output('Workspace style rules (follow every rule that applies):');
337
+ for (const entry of rules) output(`- ${entry.rule}`);
338
+ return 0;
339
+ }
340
+
341
+ if (subcommand === 'remove') {
342
+ const id = rest[0];
343
+ if (!id) throw new Error('remove needs a rule id');
344
+ const index = playbook.rules.findIndex((entry) => entry.id === id);
345
+ if (index === -1) throw new Error(`rule not found: ${id}`);
346
+ playbook.rules.splice(index, 1);
347
+ writePlaybook(playbook, root);
348
+ output(`rule removed: ${id}`);
349
+ return 0;
350
+ }
351
+
352
+ if (subcommand === 'verify') {
353
+ const id = rest[0];
354
+ const rawScoreWith = optionValue(rest, '--with');
355
+ const rawScoreWithout = optionValue(rest, '--without');
356
+ const scoreWith = Number(rawScoreWith);
357
+ const scoreWithout = Number(rawScoreWithout);
358
+ if (!id) throw new Error('verify needs a rule id');
359
+ if (rawScoreWith === null || rawScoreWithout === null ||
360
+ !Number.isFinite(scoreWith) || !Number.isFinite(scoreWithout)) {
361
+ throw new Error('verify needs numeric --with and --without scores');
362
+ }
363
+ const entry = playbook.rules.find((rule) => rule.id === id);
364
+ if (!entry) throw new Error(`rule not found: ${id}`);
365
+ entry.verified = {
366
+ method: optionValue(rest, '--method') || 'replay',
367
+ score_with: scoreWith,
368
+ score_without: scoreWithout,
369
+ at: now(),
370
+ };
371
+ if (scoreWith < scoreWithout) entry.quarantined = true;
372
+ else delete entry.quarantined;
373
+ writePlaybook(playbook, root);
374
+ output(entry.quarantined ? `rule quarantined: ${id}` : `rule verified: ${id}`);
375
+ return 0;
376
+ }
377
+
378
+ throw new Error(`unknown playbook command: ${subcommand}`);
379
+ }
380
+
381
+ module.exports = { playbookCommand, playbookFile, readPlaybook, gradeText };
@@ -334,6 +334,10 @@ function showPluginInfo() {
334
334
  // --- PUBLISH subcommand ---
335
335
 
336
336
  function publishPlugin(...args) {
337
+ if (args.some((arg) => arg === '--help' || arg === '-h' || arg === 'help')) {
338
+ console.error('Usage: atris plugin publish [--repo=path]');
339
+ process.exit(2);
340
+ }
337
341
  const projectDir = process.cwd();
338
342
  const skillsDir = path.join(projectDir, 'atris', 'skills');
339
343
 
@@ -446,6 +450,18 @@ function publishPlugin(...args) {
446
450
  // --- Main Dispatcher ---
447
451
 
448
452
  function pluginCommand(subcommand, ...args) {
453
+ if (!subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help'
454
+ || args.some((a) => a === '--help' || a === '-h' || a === 'help')) {
455
+ console.log('');
456
+ console.log('Usage: atris plugin <subcommand>');
457
+ console.log('');
458
+ console.log('Subcommands:');
459
+ console.log(' build [--output=path] - Package skills into .plugin for Cowork');
460
+ console.log(' publish [--repo=path] - Sync skills to marketplace repo and push');
461
+ console.log(' info - Preview what will be included');
462
+ console.log('');
463
+ process.exit(0);
464
+ }
449
465
  switch (subcommand) {
450
466
  case 'build':
451
467
  buildPlugin(...args);
package/commands/radar.js CHANGED
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
6
  const { execFileSync } = require('child_process');
7
+ const { parseScopeFlag, pathUnderRoot } = require('../lib/cli-scope');
7
8
 
8
9
  function safeJson(text, fallback = null) {
9
10
  try { return JSON.parse(text); } catch { return fallback; }
@@ -54,23 +55,39 @@ function agentTypeForCommand(command) {
54
55
  if (/(^|\s|\/)claude(\s|$)/.test(cmd) && !/Claude\.app/.test(cmd)) return 'claude';
55
56
  if (/(^|\s|\/)opencode(\s|$)/.test(cmd)) return 'opencode';
56
57
  if (/(^|\s|\/)devin(\s|$)/.test(cmd)) return 'devin';
58
+ if (/(^|\s|\/)agy(\s|$)/.test(cmd)) return 'agy';
57
59
  if (/(^|\s|\/)droid(\s|$)/.test(cmd)) return 'droid';
58
60
  return null;
59
61
  }
60
62
 
61
- function processCwd(pid, deps) {
63
+ // Resolve cwd for many pids at once: one lsof call on darwin instead of one
64
+ // per process. Returns a Map of pid (string) -> cwd; missing pids stay absent.
65
+ function processCwds(pids, deps) {
62
66
  const { platform, execFile } = deps;
63
- try {
64
- if (platform === 'linux') {
65
- return deps.readlink(`/proc/${pid}/cwd`);
67
+ const byPid = new Map();
68
+ if (!pids.length) return byPid;
69
+ if (platform === 'linux') {
70
+ for (const pid of pids) {
71
+ try { byPid.set(String(pid), deps.readlink(`/proc/${pid}/cwd`)); } catch {}
66
72
  }
67
- if (platform === 'darwin') {
68
- const out = execFile('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
69
- const line = String(out).split(/\r?\n/).find(value => value.startsWith('n'));
70
- return line ? line.slice(1) : '';
73
+ return byPid;
74
+ }
75
+ if (platform === 'darwin') {
76
+ let out = '';
77
+ try {
78
+ out = execFile('lsof', ['-a', '-d', 'cwd', '-Fn', '-p', pids.map(String).join(',')], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
79
+ } catch (error) {
80
+ // lsof exits non-zero when any pid in the batch is gone; keep the rows it
81
+ // did print instead of losing every cwd.
82
+ out = error && error.stdout ? error.stdout : '';
71
83
  }
72
- } catch {}
73
- return '';
84
+ let currentPid = '';
85
+ for (const line of String(out).split(/\r?\n/)) {
86
+ if (line.startsWith('p')) currentPid = line.slice(1).trim();
87
+ else if (line.startsWith('n') && currentPid) byPid.set(currentPid, line.slice(1));
88
+ }
89
+ }
90
+ return byPid;
74
91
  }
75
92
 
76
93
  function gitBranch(cwd, execFile) {
@@ -93,22 +110,28 @@ function collectAgents(deps) {
93
110
  .map(row => ({ ...row, agent: agentTypeForCommand(row.command) }))
94
111
  .filter(row => row.agent);
95
112
  const parentPids = new Set(agentRows.map(row => String(row.ppid || '')).filter(Boolean));
96
- return agentRows
97
- .filter(row => !parentPids.has(String(row.pid)))
98
- .map(row => {
99
- const cwd = processCwd(row.pid, deps);
100
- return {
101
- pid: row.pid,
102
- agent: row.agent,
103
- command: row.command,
104
- status: row.stat.includes('Z') ? 'zombie' : row.stat.includes('T') ? 'stopped' : 'active',
105
- cwd,
106
- repo: repoLabel(cwd),
107
- branch: gitBranch(cwd, deps.execFile),
108
- cpu: row.cpu,
109
- mem: row.mem,
110
- };
111
- });
113
+ const topRows = agentRows.filter(row => !parentPids.has(String(row.pid)));
114
+ const cwdByPid = processCwds(topRows.map(row => row.pid), deps);
115
+ const branchByCwd = new Map();
116
+ const branchFor = (cwd) => {
117
+ if (!cwd) return '';
118
+ if (!branchByCwd.has(cwd)) branchByCwd.set(cwd, gitBranch(cwd, deps.execFile));
119
+ return branchByCwd.get(cwd);
120
+ };
121
+ return topRows.map(row => {
122
+ const cwd = cwdByPid.get(String(row.pid)) || '';
123
+ return {
124
+ pid: row.pid,
125
+ agent: row.agent,
126
+ command: row.command,
127
+ status: row.stat.includes('Z') ? 'zombie' : row.stat.includes('T') ? 'stopped' : 'active',
128
+ cwd,
129
+ repo: repoLabel(cwd),
130
+ branch: branchFor(cwd),
131
+ cpu: row.cpu,
132
+ mem: row.mem,
133
+ };
134
+ });
112
135
  }
113
136
 
114
137
  function loadTasks(root, deps) {
@@ -287,8 +310,11 @@ function loadTeam(root, deps) {
287
310
  };
288
311
  }
289
312
 
290
- function loadBrain(root, deps) {
291
- const scorecards = readJsonLines(path.join(root, '.atris', 'state', 'scorecards.jsonl'), deps.readFile, deps.exists);
313
+ function loadScorecards(root, deps) {
314
+ return readJsonLines(path.join(root, '.atris', 'state', 'scorecards.jsonl'), deps.readFile, deps.exists);
315
+ }
316
+
317
+ function loadBrain(root, deps, scorecards = loadScorecards(root, deps)) {
292
318
  const operatorDir = path.join(root, '.atris', 'state', 'operator-scorecards');
293
319
  let operatorScorecards = 0;
294
320
  try {
@@ -316,7 +342,7 @@ function countDirectoryEntries(dir, deps, predicate = () => true) {
316
342
  return listNames(dir, deps).filter(predicate).length;
317
343
  }
318
344
 
319
- function loadBusinessCollaboration(root, deps, team = {}) {
345
+ function loadBusinessCollaboration(root, deps, team = {}, scorecardCount = countJsonLines(path.join(root, '.atris', 'state', 'scorecards.jsonl'), deps)) {
320
346
  const business = readJsonFile(path.join(root, '.atris', 'business.json'), deps, null);
321
347
  const runtime = readJsonFile(path.join(root, '.atris', 'state', 'runtime.json'), deps, null);
322
348
  const sync = readJsonFile(path.join(root, '.atris', 'state', '_sync.json'), deps, null);
@@ -335,7 +361,7 @@ function loadBusinessCollaboration(root, deps, team = {}) {
335
361
  const localReceipts = countDirectoryEntries(path.join(root, '.atris', 'receipts'), deps, name => /\.(json|md|txt)$/i.test(name));
336
362
  const events = countJsonLines(path.join(root, '.atris', 'state', 'events.jsonl'), deps);
337
363
  const episodes = countJsonLines(path.join(root, '.atris', 'state', 'episodes.jsonl'), deps);
338
- const scorecards = countJsonLines(path.join(root, '.atris', 'state', 'scorecards.jsonl'), deps);
364
+ const scorecards = scorecardCount;
339
365
  const computerDirs = countDirectoryEntries(path.join(root, 'atris', 'computers'), deps, name => !name.startsWith('.'));
340
366
  const hasOnboarding = ingestPacks > 0 || starterBriefs > 0 || onePagers > 0;
341
367
  const hasProofLoop = events > 0 || episodes > 0 || scorecards > 0 || localReceipts > 0;
@@ -770,6 +796,7 @@ function nextAction(tasks, missions, worktrees, agents, os = {}) {
770
796
 
771
797
  function collectRadar(options = {}) {
772
798
  const root = options.root || process.cwd();
799
+ const scopeKind = options.scope === 'global' ? 'global' : 'workspace';
773
800
  const deps = {
774
801
  execFile: options.execFileSync || execFileSync,
775
802
  readFile: options.readFileSync || fs.readFileSync,
@@ -785,13 +812,23 @@ function collectRadar(options = {}) {
785
812
  const identityCache = new Map();
786
813
  const missions = loadMissions(root, deps, nowMs);
787
814
  identityCache.set(root, { missions, missionLocks: loadMissionLocks(path.join(root, '.atris', 'state'), deps), memberLocks: loadMemberLoopLocks(path.join(root, '.atris', 'state'), deps) });
788
- const worktrees = loadWorktrees(root, deps);
789
- const agents = collectAgents(deps).map(agent => {
815
+ let worktrees = loadWorktrees(root, deps);
816
+ if (scopeKind !== 'global') {
817
+ // Refuse other-repo dirty worktrees in the default workspace view.
818
+ worktrees = worktrees.filter((wt) => pathUnderRoot(wt.path, root));
819
+ }
820
+ const branchIdentityCache = new Map();
821
+ const branchIdentityFor = (cwd) => {
822
+ if (!cwd) return null;
823
+ if (!branchIdentityCache.has(cwd)) branchIdentityCache.set(cwd, branchIdentityAtCwd(cwd, deps.execFile));
824
+ return branchIdentityCache.get(cwd);
825
+ };
826
+ let agents = collectAgents(deps).map(agent => {
790
827
  const taskWorkspaceRoot = findTaskWorkspaceRoot(agent.cwd, deps);
791
828
  const agentTasks = taskWorkspaceRoot ? loadTasksCached(taskWorkspaceRoot, deps, taskCache) : [];
792
829
  const identity = taskWorkspaceRoot ? loadWorkspaceIdentity(taskWorkspaceRoot, deps, identityCache, nowMs) : { missions: [], missionLocks: [], memberLocks: [] };
793
830
  const sidecar = loadAgentWorktreeSidecar(agent.cwd, deps);
794
- const branchIdentity = branchIdentityAtCwd(agent.cwd, deps.execFile);
831
+ const branchIdentity = branchIdentityFor(agent.cwd);
795
832
  const sessionId = parseSessionIdFromCommand(agent.command);
796
833
  const interactive = agent.agent === 'claude' && isInteractiveClaudeCommand(agent.command);
797
834
  const resolved = resolveAgentTaskBinding({
@@ -823,21 +860,38 @@ function collectRadar(options = {}) {
823
860
  task_action: task ? taskSessionAction(agent, task, taskWorkspaceRoot) : untaskedAction(agent, taskWorkspaceRoot, agentTasks, resolved),
824
861
  };
825
862
  });
863
+ if (scopeKind !== 'global') {
864
+ agents = agents.filter((agent) => pathUnderRoot(agent.cwd, root)
865
+ || (agent.task_workspace && pathUnderRoot(findTaskWorkspaceRoot(agent.cwd, deps), root)));
866
+ }
867
+ const scorecards = loadScorecards(root, deps);
826
868
  const osState = {
827
869
  xp: loadXp(root, deps),
828
870
  team: loadTeam(root, deps),
829
- brain: loadBrain(root, deps),
871
+ brain: loadBrain(root, deps, scorecards),
830
872
  swarlo: loadSwarlo(tasks),
831
873
  loop: loadLoop(missions, root, deps),
832
874
  };
833
- osState.business = loadBusinessCollaboration(root, deps, osState.team);
834
- 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 };
875
+ osState.business = loadBusinessCollaboration(root, deps, osState.team, scorecards.length);
876
+ return {
877
+ scope: scopeKind,
878
+ root,
879
+ generated_at: new Date(nowMs).toISOString(),
880
+ summary: summarize(tasks, missions, worktrees, agents),
881
+ os: osState,
882
+ next_action: nextAction(tasks, missions, worktrees, agents, osState),
883
+ agents,
884
+ tasks,
885
+ missions,
886
+ worktrees,
887
+ };
835
888
  }
836
889
 
837
890
  function renderRadar(data) {
838
891
  const lines = [];
839
892
  const s = data.summary;
840
893
  lines.push('Operator radar');
894
+ lines.push(`Scope: ${data.scope || 'workspace'}${data.scope === 'global' ? ' (machine)' : ' (this workspace)'}`);
841
895
  lines.push('');
842
896
  lines.push(`Agents: ${s.agents.active}/${s.agents.total} active`);
843
897
  lines.push(`Tasks: ${s.tasks.open} open, ${s.tasks.claimed} claimed, ${s.tasks.review} review (${s.tasks.certifiedReview} certified)`);
@@ -1178,17 +1232,19 @@ function renderAgentTop(data) {
1178
1232
 
1179
1233
  function radarCommand(args = [], options = {}) {
1180
1234
  if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
1181
- console.log('Usage: atris radar [--json] [--agents]');
1235
+ console.log('Usage: atris radar [--json] [--agents] [--global]');
1182
1236
  console.log('Usage: atris radar agents');
1183
1237
  console.log('Usage: atris ctop');
1184
1238
  console.log('');
1185
1239
  console.log('Shows live agent processes joined with Atris tasks, missions, and worktrees.');
1240
+ console.log('Default scope is this workspace. Pass --global for the whole machine.');
1186
1241
  console.log('Use --agents or ctop for a process-first CPU/memory view.');
1187
1242
  return 0;
1188
1243
  }
1189
- const data = collectRadar(options);
1190
- const agentsOnly = args.includes('--agents') || args[0] === 'agents';
1191
- if (args.includes('--json')) console.log(JSON.stringify(agentsOnly ? agentTopPayload(data) : data, null, 2));
1244
+ const scope = parseScopeFlag(args);
1245
+ const data = collectRadar({ ...options, scope: scope.kind });
1246
+ const agentsOnly = scope.args.includes('--agents') || scope.args[0] === 'agents';
1247
+ if (scope.args.includes('--json') || args.includes('--json')) console.log(JSON.stringify(agentsOnly ? { ...agentTopPayload(data), scope: data.scope } : data, null, 2));
1192
1248
  else if (agentsOnly) console.log(renderAgentTop(data));
1193
1249
  else console.log(renderRadar(data));
1194
1250
  return 0;