atris 3.34.0 → 3.36.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 (163) hide show
  1. package/AGENTS.md +35 -0
  2. package/FOR_AGENTS.md +5 -3
  3. package/README.md +5 -3
  4. package/atris/GETTING_STARTED.md +1 -1
  5. package/atris/atris.md +3 -0
  6. package/atris/policies/day-loop-voice.md +102 -0
  7. package/atris/policies/outbound-artifact-gate.md +2 -0
  8. package/atris/skills/design/SKILL.md +56 -32
  9. package/atris/skills/endgame/SKILL.md +12 -6
  10. package/atris/skills/engines/SKILL.md +35 -8
  11. package/atris/skills/fable-method/SKILL.md +66 -0
  12. package/atris/skills/improve/SKILL.md +65 -45
  13. package/atris/skills/render-cli/SKILL.md +88 -0
  14. package/atris/skills/youtube/SKILL.md +10 -1
  15. package/atris.md +6 -2
  16. package/ax +617 -31
  17. package/bin/atris.js +719 -266
  18. package/commands/activate.js +194 -88
  19. package/commands/aeo.js +52 -0
  20. package/commands/agents.js +166 -0
  21. package/commands/autoland.js +718 -72
  22. package/commands/autopilot-front.js +20 -2
  23. package/commands/autopilot.js +118 -2
  24. package/commands/avail.js +407 -0
  25. package/commands/bench.js +188 -0
  26. package/commands/brain.js +3 -0
  27. package/commands/brief.js +651 -0
  28. package/commands/business-sync.js +192 -6
  29. package/commands/business.js +91 -8
  30. package/commands/clean.js +50 -24
  31. package/commands/close.js +1083 -0
  32. package/commands/cloud.js +245 -0
  33. package/commands/codex-goal.js +26 -2
  34. package/commands/compile.js +292 -1
  35. package/commands/computer.js +150 -3
  36. package/commands/dream.js +365 -0
  37. package/commands/drill.js +371 -0
  38. package/commands/drive.js +187 -0
  39. package/commands/engine.js +1061 -29
  40. package/commands/experiments.js +28 -0
  41. package/commands/feed.js +202 -0
  42. package/commands/feedback.js +34 -12
  43. package/commands/fleet-report.js +206 -0
  44. package/commands/github.js +38 -0
  45. package/commands/gm.js +285 -3
  46. package/commands/goal.js +247 -0
  47. package/commands/improve.js +642 -26
  48. package/commands/init.js +83 -43
  49. package/commands/integrations.js +39 -11
  50. package/commands/interview.js +209 -0
  51. package/commands/land.js +253 -52
  52. package/commands/lesson.js +112 -1
  53. package/commands/lifecycle.js +39 -3
  54. package/commands/linear.js +38 -0
  55. package/commands/log.js +84 -1
  56. package/commands/loops.js +220 -16
  57. package/commands/meet.js +220 -0
  58. package/commands/member.js +899 -66
  59. package/commands/mission.js +3512 -332
  60. package/commands/next.js +137 -0
  61. package/commands/now.js +240 -21
  62. package/commands/one-lap.js +776 -0
  63. package/commands/orb.js +314 -0
  64. package/commands/pack-craft.js +179 -0
  65. package/commands/pack.js +823 -0
  66. package/commands/play.js +3 -2
  67. package/commands/probe.js +30 -3
  68. package/commands/pulse.js +241 -46
  69. package/commands/push.js +260 -82
  70. package/commands/radar.js +259 -14
  71. package/commands/rainmaker.js +49 -0
  72. package/commands/report.js +415 -0
  73. package/commands/scout.js +147 -0
  74. package/commands/search.js +363 -0
  75. package/commands/serve.js +54 -0
  76. package/commands/skill.js +47 -3
  77. package/commands/slop.js +50 -2
  78. package/commands/soul.js +1 -1
  79. package/commands/status.js +50 -5
  80. package/commands/stream.js +861 -0
  81. package/commands/stripe.js +38 -0
  82. package/commands/study.js +693 -0
  83. package/commands/supabase.js +39 -0
  84. package/commands/sync.js +67 -54
  85. package/commands/task.js +2275 -182
  86. package/commands/team.js +73 -0
  87. package/commands/truth.js +29 -3
  88. package/commands/unknowns.js +627 -0
  89. package/commands/update.js +44 -0
  90. package/commands/vercel.js +38 -0
  91. package/commands/verify.js +96 -0
  92. package/commands/watch.js +303 -0
  93. package/commands/wish.js +500 -0
  94. package/commands/workflow.js +11 -5
  95. package/commands/worktree.js +299 -20
  96. package/commands/write.js +399 -0
  97. package/commands/xp.js +29 -11
  98. package/lib/auto-accept-certified.js +391 -43
  99. package/lib/autoland.js +353 -52
  100. package/lib/ax-auto-lane.js +79 -0
  101. package/lib/bench/context.js +147 -0
  102. package/lib/bench/engines.js +141 -0
  103. package/lib/bench/report.js +140 -0
  104. package/lib/bench/runner.js +512 -0
  105. package/lib/brief-ledger.js +350 -0
  106. package/lib/cloud-mission.js +259 -0
  107. package/lib/codex-flight.js +154 -0
  108. package/lib/default-runner.js +45 -0
  109. package/lib/default-verifier.js +70 -0
  110. package/lib/engine-registry.js +232 -0
  111. package/lib/experiments/daily.js +640 -0
  112. package/lib/fleet.js +2431 -38
  113. package/lib/improve-vitals-html.js +171 -0
  114. package/lib/known-commands.js +58 -0
  115. package/lib/loop-doctor.js +416 -0
  116. package/lib/member-switches.js +144 -0
  117. package/lib/memory-view.js +14 -5
  118. package/lib/mission-room.js +1 -0
  119. package/lib/mission-root.js +52 -0
  120. package/lib/mission-runtime-loop.js +7 -0
  121. package/lib/next-moves.js +327 -10
  122. package/lib/official-cli-integration.js +174 -0
  123. package/lib/one-lap-validator.js +60 -0
  124. package/lib/orb-context.js +477 -0
  125. package/lib/orb-scorecard.js +224 -0
  126. package/lib/outbound-send-gate.js +165 -0
  127. package/lib/permission-grants.js +293 -0
  128. package/lib/policy-lessons.js +52 -1
  129. package/lib/pulse.js +277 -3
  130. package/lib/receipt-block.js +168 -0
  131. package/lib/receipt-evidence.js +65 -4
  132. package/lib/review-integrity.js +147 -0
  133. package/lib/router-brain.js +352 -0
  134. package/lib/runner-command.js +33 -0
  135. package/lib/self-drive.js +258 -0
  136. package/lib/short-name.js +103 -0
  137. package/lib/spawn-env.js +18 -0
  138. package/lib/state-detection.js +56 -1
  139. package/lib/sync-status.js +59 -0
  140. package/lib/task-db.js +319 -27
  141. package/lib/task-proof.js +43 -1
  142. package/lib/task-receipt.js +93 -0
  143. package/lib/team-presence.js +260 -0
  144. package/lib/tool-result-encode.js +7 -0
  145. package/lib/trust-tiers.js +90 -0
  146. package/lib/usage.js +107 -0
  147. package/lib/voice-gate.js +163 -0
  148. package/lib/wish-audit.js +1368 -0
  149. package/lib/wish-delegate.js +1840 -0
  150. package/lib/wish-design.js +110 -0
  151. package/lib/wish-stats.js +183 -0
  152. package/lib/wish-store.js +354 -0
  153. package/lib/zip.js +221 -0
  154. package/package.json +3 -1
  155. package/templates/loops/atris/loops/LOOPS.md +55 -0
  156. package/templates/loops/atris/loops/TICK.md +24 -0
  157. package/templates/loops/atris/loops/feedback.md +22 -0
  158. package/templates/loops/atris/loops/quality.md +22 -0
  159. package/templates/loops/atris/wiki/systems/loops.md +41 -0
  160. package/utils/api.js +5 -1
  161. package/utils/auth.js +57 -21
  162. package/utils/update-check.js +27 -6
  163. package/atris/learnings.jsonl +0 -1
@@ -3,6 +3,148 @@ const path = require('path');
3
3
  const { getLogPath, ensureLogDirectory, createLogFile } = require('../lib/journal');
4
4
  const { detectWorkspaceState, loadContext } = require('../lib/state-detection');
5
5
  const { readWikiStatus } = require('../lib/wiki');
6
+ const { gateForHuman, numberWord } = require('../lib/voice-gate');
7
+
8
+ const CLARITY_FIELDS = [
9
+ { key: 'focus', label: 'Focus' },
10
+ { key: 'voice', label: 'Voice' },
11
+ { key: 'cadence', label: 'Cadence' },
12
+ { key: 'done', label: 'Done means' },
13
+ { key: 'leash', label: 'Leash' },
14
+ ];
15
+
16
+ function escapeRegExp(value) {
17
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
18
+ }
19
+
20
+ function readClarityMarkdownProfile(mdPath) {
21
+ try {
22
+ const content = fs.readFileSync(mdPath, 'utf8');
23
+ const profile = {};
24
+ for (const { key, label } of CLARITY_FIELDS) {
25
+ const match = content.match(new RegExp(`^- ${escapeRegExp(label)}: (.+)$`, 'm'));
26
+ if (match?.[1]?.trim()) profile[key] = match[1].trim();
27
+ }
28
+ return profile;
29
+ } catch {
30
+ return {};
31
+ }
32
+ }
33
+
34
+ function readActivationClarityProfile(root, profilePaths) {
35
+ const paths = profilePaths(root);
36
+ if (fs.existsSync(paths.json)) {
37
+ try {
38
+ return { profile: JSON.parse(fs.readFileSync(paths.json, 'utf8')), mdPath: paths.md };
39
+ } catch {
40
+ return { profile: {}, mdPath: paths.md };
41
+ }
42
+ }
43
+ if (fs.existsSync(paths.md)) {
44
+ return { profile: readClarityMarkdownProfile(paths.md), mdPath: paths.md };
45
+ }
46
+ return { profile: {}, mdPath: paths.md };
47
+ }
48
+
49
+ function formatClarityLine(profile, mdRelPath) {
50
+ const parts = CLARITY_FIELDS
51
+ .map(({ key }) => (profile?.[key] ? `${key} ${profile[key]}` : null))
52
+ .filter(Boolean);
53
+ if (!parts.length) return null;
54
+ return `clarity: ${parts.join(', ')} (see ${mdRelPath})`;
55
+ }
56
+
57
+ function countWord(value) {
58
+ const count = Math.max(0, Math.trunc(Number(value) || 0));
59
+ return numberWord(count);
60
+ }
61
+
62
+ function sentenceFragment(value) {
63
+ return String(value || '')
64
+ .replace(/[`*_#>]+/g, '')
65
+ .replace(/\s+/g, ' ')
66
+ .trim()
67
+ .replace(/[.!?]+$/, '');
68
+ }
69
+
70
+ function humanSentence(value) {
71
+ return gateForHuman(value).text.toLowerCase();
72
+ }
73
+
74
+ // Titles arrive with tracker tags and stacked clauses; the narration keeps
75
+ // one readable clause so the operator can take it in at a glance.
76
+ function focusFragment(value, max = 72) {
77
+ const first = sentenceFragment(value)
78
+ .replace(/\[[^\]]*\]/g, '')
79
+ .replace(/\s+/g, ' ')
80
+ .trim()
81
+ .split(';')[0]
82
+ .trim();
83
+ if (first.length <= max) return first;
84
+ const cut = first.slice(0, max);
85
+ return `${cut.slice(0, cut.lastIndexOf(' '))}...`;
86
+ }
87
+
88
+ // A truncated fragment already ends with '...'; adding the template's period
89
+ // would render four dots.
90
+ function closeSentence(fragment) {
91
+ return fragment.endsWith('...') ? fragment : `${fragment}.`;
92
+ }
93
+
94
+ // The handoff block is markdown for agents; the operator gets its context
95
+ // line as one plain sentence.
96
+ function handoffSentence(handoffContent) {
97
+ if (!handoffContent) return null;
98
+ const contextMatch = String(handoffContent).match(/\*\*Context:\*\*\s*(.+)/);
99
+ const note = focusFragment(contextMatch?.[1] || handoffContent, 100);
100
+ if (!note) return null;
101
+ return humanSentence(`last session left a note: ${closeSentence(note)}`);
102
+ }
103
+
104
+ function completionSentence(completions) {
105
+ if (!Array.isArray(completions) || !completions.length) return null;
106
+ const count = completions.length;
107
+ const biggest = sentenceFragment(completions[0]?.desc);
108
+ if (!biggest) return null;
109
+ const landed = count === 1 ? 'one thing landed' : `${countWord(count)} things landed`;
110
+ return humanSentence(`since last time: ${landed}; the biggest: ${biggest}.`);
111
+ }
112
+
113
+ function activeWorkSentence(state, context) {
114
+ const tasks = Array.isArray(context?.inProgressTasks) ? context.inProgressTasks : [];
115
+ if (tasks.length) {
116
+ const count = tasks.length;
117
+ const focus = focusFragment(tasks[0]);
118
+ const subject = count === 1 ? 'task is' : 'tasks are';
119
+ return humanSentence(`right now: ${countWord(count)} ${subject} in progress; the focus is ${closeSentence(focus)}`);
120
+ }
121
+
122
+ const features = Array.isArray(context?.inProgressFeatures) ? context.inProgressFeatures : [];
123
+ const featureCount = Number(context?.inProgressFeaturesCount) || features.length;
124
+ if (featureCount > 0 && features.length) {
125
+ const focus = focusFragment(features[0]).replace(/[-_]+/g, ' ');
126
+ const subject = featureCount === 1 ? 'feature is' : 'features are';
127
+ return humanSentence(`right now: ${countWord(featureCount)} ${subject} in progress; the focus is ${closeSentence(focus)}`);
128
+ }
129
+
130
+ const inboxCount = Number(context?.inboxCount) || 0;
131
+ const inboxItem = sentenceFragment(context?.inboxItems?.[0]);
132
+ if (inboxCount > 0 && inboxItem) {
133
+ const subject = inboxCount === 1 ? 'inbox item is' : 'inbox items are';
134
+ return humanSentence(`right now: ${countWord(inboxCount)} ${subject} waiting; first: ${closeSentence(inboxItem)}`);
135
+ }
136
+
137
+ if (state?.state === 'blocked' && state.reason) {
138
+ return humanSentence(`right now: work is blocked by ${sentenceFragment(state.reason)}.`);
139
+ }
140
+ if (state?.state === 'ready') {
141
+ return humanSentence('right now: no active work is in progress.');
142
+ }
143
+ if (state?.state) {
144
+ return humanSentence(`right now: the workspace is ${sentenceFragment(state.state)}.`);
145
+ }
146
+ return null;
147
+ }
6
148
 
7
149
  function activateAtris() {
8
150
  const workspaceDir = process.cwd();
@@ -56,7 +198,7 @@ function activateAtris() {
56
198
  allLogs.sort().reverse();
57
199
 
58
200
  // Extract C# items from logs until we have 3. Dedupe by ID across files
59
- // since per-day numbering reuses C1, C2, etc. — the same ID appearing
201
+ // since per-day numbering reuses C1, C2, etc. the same ID appearing
60
202
  // in two day-files would otherwise render as duplicate rows.
61
203
  const seenIds = new Set();
62
204
  for (const logPath of allLogs) {
@@ -65,7 +207,7 @@ function activateAtris() {
65
207
  const completedSection = content.match(/## Completed ✅\n([\s\S]*?)(?=\n## |$)/);
66
208
  if (completedSection) {
67
209
  // Match `- **C#: Title**` (title between the bold markers). If extra
68
- // prose follows after `**`, ignore it — the activation panel shows
210
+ // prose follows after `**`, ignore it. the activation view shows
69
211
  // titles only, truncated to 59 chars.
70
212
  const matches = completedSection[1].matchAll(/- \*\*C(\d+):\s*([^*\n]+?)\*\*/gm);
71
213
  for (const match of matches) {
@@ -84,106 +226,70 @@ function activateAtris() {
84
226
  ? todoFile
85
227
  : (fs.existsSync(legacyTaskContextsFile) ? legacyTaskContextsFile : null);
86
228
 
87
- const summaryParts = [];
88
- if (context.inProgressFeatures?.length) summaryParts.push(`⚡ ${context.inProgressFeatures.length} in progress`);
89
- if (context.backlogTasks?.length) summaryParts.push(`📋 ${context.backlogTasks.length} backlog`);
90
- if (context.inboxItems?.length) summaryParts.push(`📥 ${context.inboxItems.length} inbox`);
91
- const summaryLine = summaryParts.length ? summaryParts.join(' | ') : 'Clean slate';
92
-
93
- console.log('');
94
- console.log('┌─────────────────────────────────────────────────────────────┐');
95
- console.log('│ Atris Activate — Context Loaded │');
96
- console.log('└─────────────────────────────────────────────────────────────┘');
97
-
98
- // Display handoff prominently if present
99
- if (handoffContent) {
100
- console.log('');
101
- console.log('┌─────────────────────────────────────────────────────────────┐');
102
- console.log('│ 📋 HANDOFF FROM LAST SESSION │');
103
- console.log('├─────────────────────────────────────────────────────────────┤');
104
- const lines = handoffContent.split('\n').slice(0, 5); // Max 5 lines
105
- lines.forEach(line => {
106
- const padded = line.substring(0, 59).padEnd(59);
107
- console.log(`│ ${padded} │`);
108
- });
109
- console.log('└─────────────────────────────────────────────────────────────┘');
110
- }
111
-
112
- // Display recent completions if any
113
- if (recentCompletions.length > 0) {
114
- console.log('');
115
- console.log('┌─────────────────────────────────────────────────────────────┐');
116
- console.log('│ ✅ RECENT COMPLETIONS │');
117
- console.log('├─────────────────────────────────────────────────────────────┤');
118
- recentCompletions.forEach(c => {
119
- const line = `${c.id}: ${c.desc}`;
120
- const padded = line.substring(0, 59).padEnd(59);
121
- console.log(`│ ${padded} │`);
122
- });
123
- console.log('└─────────────────────────────────────────────────────────────┘');
124
- }
125
-
126
- // Show learning count if learnings exist
127
- let learningLine = '';
229
+ // Keep loading learnings for parity with the existing activation path.
230
+ let learningCount = 0;
128
231
  try {
129
232
  const learningsPath = path.join(targetDir, 'learnings.jsonl');
130
233
  if (fs.existsSync(learningsPath)) {
131
234
  const lines = fs.readFileSync(learningsPath, 'utf8').trim().split('\n').filter(Boolean);
132
- if (lines.length > 0) {
133
- learningLine = ` • 🧠 ${lines.length} learnings`;
134
- }
235
+ learningCount = lines.length;
135
236
  }
136
237
  } catch {}
137
238
 
138
- console.log('');
139
- console.log(`📅 ${dateFormatted} • State: ${state.state}${learningLine}`);
140
- console.log(` ${summaryLine}`);
141
- if (wikiStatus) {
142
- const healthLine = wikiStatus.bullets.find((line) => line.startsWith('- Health:'));
143
- const wikiSummary = (healthLine || wikiStatus.bullets[0] || '- wiki scaffold ready').replace(/^- /, '');
144
- console.log(` 🧠 Wiki: ${wikiSummary}`);
145
- }
146
- console.log('');
147
- console.log('Core files:');
148
- console.log(`- ${fs.existsSync(personaFile) ? rel(personaFile) : 'atris/PERSONA.md (missing)'}`);
149
- console.log(`- ${fs.existsSync(mapFile) ? rel(mapFile) : 'atris/MAP.md (missing)'}`);
150
- console.log(`- ${taskFilePath ? rel(taskFilePath) : 'atris/TODO.md (missing)'}`);
151
- console.log(`- ${rel(logFile)}`);
152
- if (wikiStatus?.statusPath) {
153
- console.log(`- ${rel(wikiStatus.statusPath)}`);
154
- }
155
- if (wikiStatus?.bullets?.length) {
156
- console.log('');
157
- console.log('Wiki:');
158
- wikiStatus.bullets.forEach((line) => console.log(`- ${line.replace(/^- /, '')}`));
159
- }
160
- console.log('');
239
+ let moves = [];
161
240
  try {
162
241
  const { nextMoves } = require('../lib/next-moves');
163
- const { readProfile, isEmptyProfile } = require('../lib/clarity');
242
+ const { profilePaths } = require('../lib/clarity');
164
243
  const root = process.cwd();
165
- const moves = nextMoves(root, 3);
166
- console.log('Your next moves:');
167
- if (moves.length) {
168
- moves.forEach((m, i) => console.log(` ${i + 1}. ${m.title}`));
169
- console.log(' steer them: atris moves');
244
+ moves = nextMoves(root, 3);
245
+ const { profile, mdPath } = readActivationClarityProfile(root, profilePaths);
246
+ formatClarityLine(profile, rel(mdPath));
247
+ } catch { /* alive onboarding is best-effort; never block activate */ }
248
+
249
+ let briefData = null;
250
+ try {
251
+ const { buildBriefData } = require('./brief');
252
+ briefData = buildBriefData(workspaceDir);
253
+ } catch { /* brief is best-effort */ }
254
+ try {
255
+ const { syncStatus } = require('../lib/sync-status');
256
+ syncStatus(workspaceDir);
257
+ } catch { /* sync status is best-effort */ }
258
+
259
+ void dateFormatted;
260
+ void learningCount;
261
+ void wikiStatus;
262
+ fs.existsSync(personaFile);
263
+ fs.existsSync(mapFile);
264
+ void taskFilePath;
265
+
266
+ console.log(humanSentence('atris is up.'));
267
+
268
+ const statusLines = [
269
+ handoffSentence(handoffContent),
270
+ completionSentence(recentCompletions),
271
+ activeWorkSentence(state, context),
272
+ ].filter(Boolean);
273
+ if (Array.isArray(briefData?.waiting)) {
274
+ const approvalCount = briefData.waiting.length;
275
+ if (approvalCount === 0) {
276
+ statusLines.push('nothing is waiting on you.');
170
277
  } else {
171
- console.log(' none queued. add to ROADMAP.md under "## Open loop items", or jot one with atris log');
278
+ const approvalLabel = approvalCount === 1 ? 'approval' : 'approvals';
279
+ statusLines.push(`waiting on you: ${countWord(approvalCount)} ${approvalLabel}. see them: atris task reviews`);
172
280
  }
173
- const profile = readProfile(root);
281
+ }
282
+ if (statusLines.length) {
174
283
  console.log('');
175
- if (isEmptyProfile(profile)) {
176
- console.log('Tip: run atris clarity once so agents learn how you work.');
177
- } else {
178
- console.log('How you work (atris clarity):');
179
- ['focus', 'voice', 'cadence', 'done', 'leash']
180
- .filter((k) => profile[k])
181
- .forEach((k) => console.log(` ${k}: ${profile[k]}`));
182
- }
284
+ statusLines.forEach((line) => console.log(line));
285
+ }
286
+
287
+ const nextMove = moves[0] || briefData?.moves?.[0];
288
+ const nextMoveTitle = focusFragment(nextMove?.title, 100);
289
+ if (nextMoveTitle) {
183
290
  console.log('');
184
- } catch { /* alive onboarding is best-effort; never block activate */ }
185
- console.log('Next: atris plan → do → review (or atris log)');
186
- console.log('');
291
+ console.log(humanSentence(`today's move: ${closeSentence(nextMoveTitle)}`));
292
+ }
187
293
  }
188
294
 
189
295
  module.exports = { activateAtris };
package/commands/aeo.js CHANGED
@@ -157,6 +157,9 @@ function pickSlug(args) {
157
157
 
158
158
  function printHelp() {
159
159
  console.log('Usage:');
160
+ console.log(' Live scan (no login needed):');
161
+ console.log(' atris aeo scan <url> [--vs <competitor-url>] [--json]');
162
+ console.log('');
160
163
  console.log(' Backend / EC2:');
161
164
  console.log(' atris aeo init [--workspace <slug>]');
162
165
  console.log(' atris aeo draft "<topic>" [--workspace <slug>] [--queries q1,q2] [--slug X] [--url URL]');
@@ -178,6 +181,54 @@ function printHelp() {
178
181
  console.log(' atris aeo discover https://atris.ai/aeo --canonical-url https://atris.ai/aeo');
179
182
  }
180
183
 
184
+ const FREE_SCAN_BASE = process.env.ATRIS_API_BASE || 'https://api.atris.ai';
185
+
186
+ async function aeoScan(args) {
187
+ const asJson = args.includes('--json');
188
+ const vs = readArg(args, '--vs');
189
+ const url = args.filter((a) => !a.startsWith('--') && a !== vs)[0];
190
+ if (!url) {
191
+ console.error('Usage: atris aeo scan <url> [--vs <competitor-url>] [--json]');
192
+ process.exit(1);
193
+ }
194
+ const target = /^https?:\/\//i.test(url) ? url : `https://${url}`;
195
+ const qs = new URLSearchParams({ url: target });
196
+ if (vs) qs.set('vs', /^https?:\/\//i.test(vs) ? vs : `https://${vs}`);
197
+ const endpoint = `${FREE_SCAN_BASE}/api/business/aeo/free-scan?${qs}`;
198
+ let data;
199
+ try {
200
+ const res = await fetch(endpoint, { signal: AbortSignal.timeout(60000) });
201
+ if (!res.ok) {
202
+ const body = await res.text().catch(() => '');
203
+ console.error(`Scan failed (${res.status}): ${body.slice(0, 200)}`);
204
+ process.exit(1);
205
+ }
206
+ data = await res.json();
207
+ } catch (e) {
208
+ console.error(`Scan failed: ${e.message}`);
209
+ process.exit(1);
210
+ }
211
+ if (asJson) { console.log(JSON.stringify(data, null, 2)); return; }
212
+ console.log(`\n ${data.name || target}`);
213
+ console.log(` Score: ${data.score}/100 (${data.grade}) — ${data.status}`);
214
+ if (data.gap_statement) console.log(`\n ${data.gap_statement}`);
215
+ const gaps = data.gaps || [];
216
+ if (gaps.length) {
217
+ console.log('\n Gaps:');
218
+ for (const g of gaps) console.log(` ✗ ${g.title || g.key}: ${g.fix || ''}`);
219
+ }
220
+ const comp = data.competitors || [];
221
+ if (comp.length) {
222
+ console.log('\n Head-to-head:');
223
+ for (const c of comp) console.log(` ${pad(String(c.score ?? '?'), 4)} ${c.url || c.name}`);
224
+ }
225
+ if (data.draft_llms_txt) {
226
+ console.log('\n Draft llms.txt included — save it with:');
227
+ console.log(` atris aeo scan ${url} --json | jq -r .draft_llms_txt > llms.txt`);
228
+ }
229
+ console.log('');
230
+ }
231
+
181
232
  async function aeoInit(args) {
182
233
  const slug = pickSlug(args);
183
234
  if (!slug) {
@@ -545,6 +596,7 @@ async function run(args = []) {
545
596
  const sub = args[0];
546
597
  if (!sub || sub === 'help' || sub === '--help' || sub === '-h') return printHelp();
547
598
  const rest = args.slice(1);
599
+ if (sub === 'scan') return aeoScan(rest);
548
600
  if (sub === 'init') return aeoInit(rest);
549
601
  if (sub === 'draft') return aeoDraft(rest);
550
602
  if (sub === 'log') return aeoLog(rest);
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+
3
+ // atris agents — one glanceable view of every member's state.
4
+ // Reads .atris/state/missions.jsonl (schema atris.mission.v1) plus the
5
+ // directories in atris/team/, and prints who is stuck, waiting on you,
6
+ // working, or resting. Built from a dream card: "validator cannot plan
7
+ // missions without seeing which agents are idle or blocked."
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { listMissions } = require('./mission');
12
+ const { normalizeOwnerSlug } = require('../lib/functional-owner');
13
+ const { shortRecordLabel } = require('../lib/short-name');
14
+
15
+ const WORKING_STATUSES = new Set(['running', 'planning']);
16
+ const STUCK_STATUSES = new Set(['blocked', 'stuck', 'failed']);
17
+ const RESTING_STATUSES = new Set(['complete', 'stopped', 'paused']);
18
+
19
+ const STATE_RANK = {
20
+ stuck: 0,
21
+ 'waiting on you': 1,
22
+ working: 2,
23
+ resting: 3,
24
+ idle: 4,
25
+ };
26
+
27
+ function listTeamDirs(root) {
28
+ const teamDir = path.join(root, 'atris', 'team');
29
+ try {
30
+ return fs.readdirSync(teamDir, { withFileTypes: true })
31
+ .filter((entry) => entry.isDirectory())
32
+ .map((entry) => entry.name)
33
+ .filter((name) => name !== '_template' && !name.startsWith('.'));
34
+ } catch {
35
+ return [];
36
+ }
37
+ }
38
+
39
+ // Given a member's missions (already newest-first), pick one state word.
40
+ // Priority: a stuck mission always wins (that is the whole point of this
41
+ // view). Otherwise, if the newest mission is ready, they are waiting on a
42
+ // human. Otherwise, any running/planning mission means they are working.
43
+ // Anything left over (complete/stopped/paused, or nothing at all) rests.
44
+ function ownerState(missions) {
45
+ if (!missions.length) return { state: 'idle', current: null };
46
+ const stuckMission = missions.find((m) => STUCK_STATUSES.has(m.status));
47
+ if (stuckMission) return { state: 'stuck', current: stuckMission };
48
+ const latest = missions[0];
49
+ if (latest.status === 'ready') return { state: 'waiting on you', current: latest };
50
+ const workingMission = missions.find((m) => WORKING_STATUSES.has(m.status));
51
+ if (workingMission) return { state: 'working', current: workingMission };
52
+ const restingMission = missions.find((m) => RESTING_STATUSES.has(m.status)) || latest;
53
+ return { state: 'resting', current: restingMission };
54
+ }
55
+
56
+ function missionLabel(mission) {
57
+ if (!mission) return null;
58
+ return shortRecordLabel(mission, mission.objective || mission.slug || mission.id, { wordLimit: 4 });
59
+ }
60
+
61
+ // Build one row per member: team directories plus anyone who owns a
62
+ // mission but has no directory yet. Returns rows sorted stuck first, then
63
+ // waiting on you, then working, then resting/idle.
64
+ function buildAgentRows(root = process.cwd()) {
65
+ const missions = listMissions(root);
66
+ const displayNames = new Map(); // normalized slug -> name to print
67
+ const missionsByOwner = new Map(); // normalized slug -> missions (newest-first)
68
+
69
+ for (const name of listTeamDirs(root)) {
70
+ const slug = normalizeOwnerSlug(name);
71
+ if (!slug) continue;
72
+ if (!displayNames.has(slug)) displayNames.set(slug, name);
73
+ if (!missionsByOwner.has(slug)) missionsByOwner.set(slug, []);
74
+ }
75
+
76
+ for (const mission of missions) {
77
+ const raw = String(mission.owner || '').trim();
78
+ if (!raw) continue;
79
+ const slug = normalizeOwnerSlug(raw);
80
+ if (!slug) continue;
81
+ if (!displayNames.has(slug)) displayNames.set(slug, raw);
82
+ if (!missionsByOwner.has(slug)) missionsByOwner.set(slug, []);
83
+ missionsByOwner.get(slug).push(mission);
84
+ }
85
+
86
+ const rows = [];
87
+ for (const [slug, ownerMissions] of missionsByOwner.entries()) {
88
+ const { state, current } = ownerState(ownerMissions);
89
+ rows.push({
90
+ owner: displayNames.get(slug) || slug,
91
+ state,
92
+ label: missionLabel(current),
93
+ });
94
+ }
95
+
96
+ rows.sort((a, b) => {
97
+ const rankDiff = STATE_RANK[a.state] - STATE_RANK[b.state];
98
+ if (rankDiff !== 0) return rankDiff;
99
+ return a.owner.localeCompare(b.owner);
100
+ });
101
+
102
+ return rows;
103
+ }
104
+
105
+ function formatRow(row) {
106
+ return row.label ? `${row.owner} - ${row.state} - ${row.label}` : `${row.owner} - ${row.state}`;
107
+ }
108
+
109
+ // Split rows into the always-shown head (stuck, waiting on you, working)
110
+ // and the foldable tail (resting, idle). When `all` is false, the tail
111
+ // collapses into a single count line so a big roster stays glanceable.
112
+ function renderAgentLines(rows, { all = false } = {}) {
113
+ const head = [];
114
+ const tail = [];
115
+ for (const row of rows) {
116
+ if (row.state === 'resting' || row.state === 'idle') tail.push(row);
117
+ else head.push(row);
118
+ }
119
+
120
+ const lines = head.map(formatRow);
121
+ if (all) {
122
+ lines.push(...tail.map(formatRow));
123
+ } else if (tail.length) {
124
+ const noun = tail.length === 1 ? 'member' : 'members';
125
+ lines.push(`${tail.length} ${noun} resting or idle. See them with atris agents --all`);
126
+ }
127
+
128
+ if (!lines.length) lines.push('No agents found. Add someone to atris/team or start a mission.');
129
+ return lines;
130
+ }
131
+
132
+ function printHelp() {
133
+ console.log('Usage: atris agents [--all] [--json]');
134
+ console.log('Shows every member: stuck, waiting on you, working, or resting.');
135
+ }
136
+
137
+ function agentsCommand(args = [], { root = process.cwd() } = {}) {
138
+ if (args.includes('--help') || args.includes('-h')) {
139
+ printHelp();
140
+ return 0;
141
+ }
142
+ const all = args.includes('--all');
143
+ const rows = buildAgentRows(root);
144
+
145
+ if (args.includes('--json')) {
146
+ console.log(JSON.stringify({
147
+ schema: 'atris.agents.v1',
148
+ all,
149
+ agents: rows,
150
+ }));
151
+ return 0;
152
+ }
153
+
154
+ for (const line of renderAgentLines(rows, { all })) {
155
+ console.log(line);
156
+ }
157
+ return 0;
158
+ }
159
+
160
+ module.exports = {
161
+ agentsCommand,
162
+ buildAgentRows,
163
+ renderAgentLines,
164
+ ownerState,
165
+ missionLabel,
166
+ };