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.
- package/AGENTS.md +35 -0
- package/FOR_AGENTS.md +5 -3
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +35 -8
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +6 -2
- package/ax +617 -31
- package/bin/atris.js +719 -266
- package/commands/activate.js +194 -88
- package/commands/aeo.js +52 -0
- package/commands/agents.js +166 -0
- package/commands/autoland.js +718 -72
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/business.js +91 -8
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/codex-goal.js +26 -2
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/drive.js +187 -0
- package/commands/engine.js +1061 -29
- package/commands/experiments.js +28 -0
- package/commands/feed.js +202 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +285 -3
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +83 -43
- package/commands/integrations.js +39 -11
- package/commands/interview.js +209 -0
- package/commands/land.js +253 -52
- package/commands/lesson.js +112 -1
- package/commands/lifecycle.js +39 -3
- package/commands/linear.js +38 -0
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +899 -66
- package/commands/mission.js +3512 -332
- package/commands/next.js +137 -0
- package/commands/now.js +240 -21
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/radar.js +259 -14
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/serve.js +54 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/status.js +50 -5
- package/commands/stream.js +861 -0
- package/commands/stripe.js +38 -0
- package/commands/study.js +693 -0
- package/commands/supabase.js +39 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +2275 -182
- package/commands/team.js +73 -0
- package/commands/truth.js +29 -3
- package/commands/unknowns.js +627 -0
- package/commands/update.js +44 -0
- package/commands/vercel.js +38 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +299 -20
- package/commands/write.js +399 -0
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +391 -43
- package/lib/autoland.js +353 -52
- package/lib/ax-auto-lane.js +79 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2431 -38
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/memory-view.js +14 -5
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/next-moves.js +327 -10
- package/lib/official-cli-integration.js +174 -0
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/review-integrity.js +147 -0
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +33 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +319 -27
- package/lib/task-proof.js +43 -1
- package/lib/task-receipt.js +93 -0
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
- package/utils/update-check.js +27 -6
- package/atris/learnings.jsonl +0 -1
package/commands/activate.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
133
|
-
learningLine = ` • 🧠 ${lines.length} learnings`;
|
|
134
|
-
}
|
|
235
|
+
learningCount = lines.length;
|
|
135
236
|
}
|
|
136
237
|
} catch {}
|
|
137
238
|
|
|
138
|
-
|
|
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 {
|
|
242
|
+
const { profilePaths } = require('../lib/clarity');
|
|
164
243
|
const root = process.cwd();
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
278
|
+
const approvalLabel = approvalCount === 1 ? 'approval' : 'approvals';
|
|
279
|
+
statusLines.push(`waiting on you: ${countWord(approvalCount)} ${approvalLabel}. see them: atris task reviews`);
|
|
172
280
|
}
|
|
173
|
-
|
|
281
|
+
}
|
|
282
|
+
if (statusLines.length) {
|
|
174
283
|
console.log('');
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
+
};
|