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/experiments.js
CHANGED
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
summarizeReview,
|
|
18
18
|
writeArtifact,
|
|
19
19
|
} = require('../lib/endstate');
|
|
20
|
+
const daily = require('../lib/experiments/daily');
|
|
20
21
|
|
|
21
22
|
const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
22
23
|
const ROOT_FILES = ['README.md', 'validate.py', 'benchmark_validate.py', 'benchmark_runtime.py'];
|
|
@@ -520,8 +521,31 @@ function experimentsReplay(target = 'endstate') {
|
|
|
520
521
|
experimentsCompare('endstate');
|
|
521
522
|
}
|
|
522
523
|
|
|
524
|
+
function experimentsDaily(...args) {
|
|
525
|
+
ensureAtrisWorkspace();
|
|
526
|
+
const code = daily.runDaily(process.cwd(), args);
|
|
527
|
+
if (code) process.exit(code);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function experimentsQueue(subcommand, ...args) {
|
|
531
|
+
ensureAtrisWorkspace();
|
|
532
|
+
const root = process.cwd();
|
|
533
|
+
let code = 1;
|
|
534
|
+
if (subcommand === 'add') code = daily.queueAdd(root, args);
|
|
535
|
+
else if (subcommand === 'list') code = daily.queueList(root, args);
|
|
536
|
+
else {
|
|
537
|
+
console.error('Usage: atris experiments queue <add|list> ...');
|
|
538
|
+
code = 1;
|
|
539
|
+
}
|
|
540
|
+
if (code) process.exit(code);
|
|
541
|
+
}
|
|
542
|
+
|
|
523
543
|
function experimentsCommand(subcommand, ...args) {
|
|
524
544
|
switch (subcommand) {
|
|
545
|
+
case 'daily':
|
|
546
|
+
return experimentsDaily(...args);
|
|
547
|
+
case 'queue':
|
|
548
|
+
return experimentsQueue(args[0], ...args.slice(1));
|
|
525
549
|
case 'init':
|
|
526
550
|
case 'new':
|
|
527
551
|
return experimentsInit(args[0]);
|
|
@@ -546,6 +570,8 @@ function experimentsCommand(subcommand, ...args) {
|
|
|
546
570
|
console.log(' compare endstate Compare the latest baseline and stack receipts');
|
|
547
571
|
console.log(' replay endstate Validate, dry-run, and compare the public benchmark flow');
|
|
548
572
|
console.log(' benchmark [mode] Run validate/runtime/all benchmark harness');
|
|
573
|
+
console.log(' daily [--dry-run] [--force] [--json]');
|
|
574
|
+
console.log(' queue add|list Manage the daily experiment queue');
|
|
549
575
|
console.log('');
|
|
550
576
|
console.log('Examples:');
|
|
551
577
|
console.log(' atris experiments init');
|
|
@@ -564,4 +590,6 @@ module.exports = {
|
|
|
564
590
|
ensureExperimentsFramework,
|
|
565
591
|
buildBenchmarkArtifact,
|
|
566
592
|
parseRunOptions,
|
|
593
|
+
experimentsDaily,
|
|
594
|
+
experimentsQueue,
|
|
567
595
|
};
|
package/commands/feed.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Feed: the business group feed from the terminal — read receipts, post receipts.
|
|
4
|
+
// Auth: existing atris login. Business: nearest .atris/business.json above cwd.
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { ensureValidCredentials } = require('../utils/auth');
|
|
9
|
+
const { apiRequestJson } = require('../utils/api');
|
|
10
|
+
|
|
11
|
+
function showHelp() {
|
|
12
|
+
console.log('');
|
|
13
|
+
console.log('Usage: atris feed [list] [n] [--author <name>] [--since <7d|24h>] [--full] [--json]');
|
|
14
|
+
console.log(' atris feed post "<content>"');
|
|
15
|
+
console.log('');
|
|
16
|
+
console.log('Description:');
|
|
17
|
+
console.log(' Read and write the business group feed (the same feed as the web GM dashboard).');
|
|
18
|
+
console.log(' Posts should be receipts and state changes only: what shipped, what blocked, what changed.');
|
|
19
|
+
console.log('');
|
|
20
|
+
console.log('Options:');
|
|
21
|
+
console.log(' --author <name> Only posts whose author matches (name, email prefix, or id prefix).');
|
|
22
|
+
console.log(' --since <window> Only posts newer than e.g. 7d, 48h.');
|
|
23
|
+
console.log(' --full Print full post bodies instead of one line each.');
|
|
24
|
+
console.log(' --json Machine-readable output.');
|
|
25
|
+
console.log(' --help, -h Show this help.');
|
|
26
|
+
console.log('');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findBusiness(startDir) {
|
|
30
|
+
let dir = startDir;
|
|
31
|
+
while (dir !== path.dirname(dir)) {
|
|
32
|
+
const file = path.join(dir, '.atris', 'business.json');
|
|
33
|
+
if (fs.existsSync(file)) {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
36
|
+
if (parsed.business_id) return { businessId: parsed.business_id, name: parsed.name || parsed.slug, root: dir };
|
|
37
|
+
} catch { /* fall through to parent dirs */ }
|
|
38
|
+
}
|
|
39
|
+
dir = path.dirname(dir);
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function loadAuthorAliases(workspaceRoot) {
|
|
45
|
+
const file = path.join(workspaceRoot, '.atris', 'feed_authors.json');
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
48
|
+
} catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseSince(raw) {
|
|
54
|
+
const match = /^(\d+)([dh])$/.exec(String(raw || '').trim());
|
|
55
|
+
if (!match) return null;
|
|
56
|
+
const amount = parseInt(match[1], 10);
|
|
57
|
+
const ms = match[2] === 'd' ? amount * 24 * 3600 * 1000 : amount * 3600 * 1000;
|
|
58
|
+
return Date.now() - ms;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function authorLabel(post, aliases, selfId, selfEmail) {
|
|
62
|
+
if (post.post_type === 'agent_post') return post.agent_name || post.author_display_name || 'agent';
|
|
63
|
+
if (post.author_display_name) return post.author_display_name;
|
|
64
|
+
const id = post.author_id || '';
|
|
65
|
+
if (aliases[id]) return aliases[id];
|
|
66
|
+
if (selfId && id === selfId) return (selfEmail || 'me').split('@')[0];
|
|
67
|
+
return id ? id.slice(0, 8) : '?';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function fetchPosts(businessId, token, want) {
|
|
71
|
+
const pageSize = 100;
|
|
72
|
+
let all = [];
|
|
73
|
+
for (let offset = 0; all.length < want; offset += pageSize) {
|
|
74
|
+
const res = await apiRequestJson(
|
|
75
|
+
`/business/${businessId}/feed?limit=${pageSize}&offset=${offset}`,
|
|
76
|
+
{ token, timeoutMs: 20000 }
|
|
77
|
+
);
|
|
78
|
+
const statusCode = res.statusCode || res.status;
|
|
79
|
+
if (statusCode && statusCode >= 400) {
|
|
80
|
+
throw new Error(`Feed fetch failed (${statusCode}): ${JSON.stringify(res.data)}`);
|
|
81
|
+
}
|
|
82
|
+
const page = Array.isArray(res.data?.posts) ? res.data.posts : Array.isArray(res.data) ? res.data : [];
|
|
83
|
+
all = all.concat(page);
|
|
84
|
+
if (page.length < pageSize) break;
|
|
85
|
+
}
|
|
86
|
+
return all;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function feedCommand(args = []) {
|
|
90
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
91
|
+
showHelp();
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const business = findBusiness(process.cwd());
|
|
96
|
+
if (!business) {
|
|
97
|
+
console.error('✗ No .atris/business.json found above this directory. Run from a bound workspace.');
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const auth = await ensureValidCredentials(apiRequestJson);
|
|
102
|
+
if (auth.error || !auth.credentials?.token) {
|
|
103
|
+
console.error(`✗ Not logged in (${auth.error || 'no token'}). Run: atris login`);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
const token = auth.credentials.token;
|
|
107
|
+
const selfId = auth.credentials.user_id || auth.user?.id || null;
|
|
108
|
+
const selfEmail = auth.credentials.email || auth.user?.email || null;
|
|
109
|
+
|
|
110
|
+
const positional = args.filter((a) => !a.startsWith('--'));
|
|
111
|
+
const sub = positional[0] || 'list';
|
|
112
|
+
|
|
113
|
+
if (sub === 'post') {
|
|
114
|
+
const content = positional.slice(1).join(' ').trim();
|
|
115
|
+
if (!content) {
|
|
116
|
+
console.error('✗ Nothing to post. Usage: atris feed post "<content>"');
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
const res = await apiRequestJson(`/business/${business.businessId}/feed`, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
token,
|
|
122
|
+
body: { content: content.slice(0, 10000) },
|
|
123
|
+
timeoutMs: 20000,
|
|
124
|
+
});
|
|
125
|
+
if (!res.data?.id) {
|
|
126
|
+
console.error(`✗ Post failed: ${JSON.stringify(res.data)}`);
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
console.log(`✓ Posted to ${business.name || 'business'} feed (${res.data.id})`);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (sub !== 'list') {
|
|
134
|
+
console.error(`✗ Unknown subcommand: ${sub}`);
|
|
135
|
+
showHelp();
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const countArg = parseInt(positional[1], 10);
|
|
140
|
+
const count = Number.isFinite(countArg) ? Math.max(1, Math.min(300, countArg)) : 15;
|
|
141
|
+
const authorFlag = args.includes('--author') ? String(args[args.indexOf('--author') + 1] || '').toLowerCase() : null;
|
|
142
|
+
const sinceFlag = args.includes('--since') ? parseSince(args[args.indexOf('--since') + 1]) : null;
|
|
143
|
+
if (args.includes('--since') && sinceFlag === null) {
|
|
144
|
+
console.error('✗ Bad --since window. Use forms like 7d or 48h.');
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
const full = args.includes('--full');
|
|
148
|
+
const json = args.includes('--json');
|
|
149
|
+
|
|
150
|
+
const aliases = loadAuthorAliases(business.root);
|
|
151
|
+
// Over-fetch when filtering so the filtered result can still reach `count`.
|
|
152
|
+
const fetchWant = authorFlag || sinceFlag ? 300 : count;
|
|
153
|
+
let posts = await fetchPosts(business.businessId, token, fetchWant);
|
|
154
|
+
|
|
155
|
+
if (sinceFlag) posts = posts.filter((p) => Date.parse(p.created_at) >= sinceFlag);
|
|
156
|
+
if (authorFlag) {
|
|
157
|
+
posts = posts.filter((p) => {
|
|
158
|
+
const label = authorLabel(p, aliases, selfId, selfEmail).toLowerCase();
|
|
159
|
+
return label.includes(authorFlag) || String(p.author_id || '').startsWith(authorFlag);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
posts = posts.slice(0, count);
|
|
163
|
+
|
|
164
|
+
if (json) {
|
|
165
|
+
console.log(JSON.stringify(posts.map((p) => ({
|
|
166
|
+
id: p.id,
|
|
167
|
+
author: authorLabel(p, aliases, selfId, selfEmail),
|
|
168
|
+
type: p.post_type,
|
|
169
|
+
created_at: p.created_at,
|
|
170
|
+
content: p.content,
|
|
171
|
+
})), null, 2));
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!posts.length) {
|
|
176
|
+
console.log('No posts matched.');
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const p of posts) {
|
|
181
|
+
const when = String(p.created_at || '').slice(0, 16).replace('T', ' ');
|
|
182
|
+
const who = authorLabel(p, aliases, selfId, selfEmail);
|
|
183
|
+
if (full) {
|
|
184
|
+
console.log(`─── ${who} — ${when}`);
|
|
185
|
+
console.log(String(p.content || '').trim());
|
|
186
|
+
console.log('');
|
|
187
|
+
} else {
|
|
188
|
+
const oneLine = String(p.content || '').replace(/\s+/g, ' ').slice(0, 120);
|
|
189
|
+
console.log(`[${when}] ${who}: ${oneLine}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = {
|
|
196
|
+
feedCommand,
|
|
197
|
+
findBusiness,
|
|
198
|
+
loadAuthorAliases,
|
|
199
|
+
parseSince,
|
|
200
|
+
authorLabel,
|
|
201
|
+
fetchPosts,
|
|
202
|
+
};
|
package/commands/feedback.js
CHANGED
|
@@ -39,10 +39,15 @@ function getAuth() {
|
|
|
39
39
|
return { token: creds.token, email: creds.email || 'unknown' };
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function rejectMissingFeedbackMessage() {
|
|
43
|
+
console.error('Error: feedback message is required.');
|
|
44
|
+
printHelp(console.error);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
async function submitFeedback(message, opts = {}) {
|
|
43
|
-
if (!message) {
|
|
44
|
-
|
|
45
|
-
process.exit(1);
|
|
49
|
+
if (!message || !String(message).trim()) {
|
|
50
|
+
rejectMissingFeedbackMessage();
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
const { token } = getAuth();
|
|
@@ -65,15 +70,20 @@ async function submitFeedback(message, opts = {}) {
|
|
|
65
70
|
body,
|
|
66
71
|
});
|
|
67
72
|
|
|
68
|
-
if (!result.ok) {
|
|
69
|
-
|
|
73
|
+
if (!result.ok || !result.data?.feedback_id) {
|
|
74
|
+
const parts = [];
|
|
75
|
+
if (result.error) parts.push(result.error);
|
|
76
|
+
if (result.data && typeof result.data === 'object') {
|
|
77
|
+
const serverMsg = result.data.detail || result.data.error || result.data.message;
|
|
78
|
+
if (serverMsg && serverMsg !== result.error) parts.push(String(serverMsg));
|
|
79
|
+
}
|
|
80
|
+
if (result.text && !result.data) parts.push(result.text.trim());
|
|
81
|
+
if (!parts.length) parts.push('Failed to submit feedback (no feedback_id returned)');
|
|
82
|
+
console.error(`Error: ${parts.join(' — ')}`);
|
|
70
83
|
process.exit(1);
|
|
71
84
|
}
|
|
72
85
|
|
|
73
|
-
console.log(
|
|
74
|
-
if (result.data?.feedback_id) {
|
|
75
|
-
console.log(` ID: ${result.data.feedback_id}`);
|
|
76
|
-
}
|
|
86
|
+
console.log(`Feedback submitted. ID: ${result.data.feedback_id}`);
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
async function fetchFeedbackItems({ token, businessId, limit = 100, status = null } = {}) {
|
|
@@ -279,7 +289,7 @@ function directFeedbackMessage(args) {
|
|
|
279
289
|
}
|
|
280
290
|
|
|
281
291
|
function rejectUnknownFeedbackCommand(subcommand) {
|
|
282
|
-
|
|
292
|
+
fs.writeSync(2, `Unknown feedback command: ${subcommand || '(empty)'}\n`);
|
|
283
293
|
printHelp(console.error);
|
|
284
294
|
process.exit(1);
|
|
285
295
|
}
|
|
@@ -293,9 +303,14 @@ async function feedbackCommand() {
|
|
|
293
303
|
process.exit(1);
|
|
294
304
|
}
|
|
295
305
|
|
|
306
|
+
if (args.length === 0) {
|
|
307
|
+
await listFeedback();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
296
311
|
const subcommand = args[0];
|
|
297
312
|
|
|
298
|
-
if (
|
|
313
|
+
if (subcommand === 'list') {
|
|
299
314
|
await listFeedback();
|
|
300
315
|
return;
|
|
301
316
|
}
|
|
@@ -306,7 +321,10 @@ async function feedbackCommand() {
|
|
|
306
321
|
}
|
|
307
322
|
|
|
308
323
|
if (subcommand === 'submit') {
|
|
309
|
-
const message = args.slice(1).join(' ');
|
|
324
|
+
const message = args.slice(1).join(' ').trim();
|
|
325
|
+
if (!message) {
|
|
326
|
+
rejectMissingFeedbackMessage();
|
|
327
|
+
}
|
|
310
328
|
await submitFeedback(message, { businessId });
|
|
311
329
|
return;
|
|
312
330
|
}
|
|
@@ -328,6 +346,10 @@ async function feedbackCommand() {
|
|
|
328
346
|
return;
|
|
329
347
|
}
|
|
330
348
|
|
|
349
|
+
if (args.length === 1 && !String(args[0]).trim()) {
|
|
350
|
+
rejectMissingFeedbackMessage();
|
|
351
|
+
}
|
|
352
|
+
|
|
331
353
|
const message = directFeedbackMessage(args);
|
|
332
354
|
if (message) {
|
|
333
355
|
await submitFeedback(message, { businessId });
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* atris fleet-report [business] [--all-alive] [--wake] [--dry-run]
|
|
3
|
+
*
|
|
4
|
+
* Deliver the daily report to business AI computers so every computer
|
|
5
|
+
* that is alive can see it at /workspace/atris/reports/daily-YYYY-MM-DD.md.
|
|
6
|
+
*
|
|
7
|
+
* Why push from the fleet side instead of a cron on the box: computers
|
|
8
|
+
* hold no API token on disk (Iron Dome P4 - /workspace persists, secrets
|
|
9
|
+
* never touch disk), so the fleet layer fetches the report with the
|
|
10
|
+
* owner token and writes it over the existing /terminal primitive.
|
|
11
|
+
*
|
|
12
|
+
* atris fleet-report agentgrads --wake # one computer, wake if asleep
|
|
13
|
+
* atris fleet-report --all-alive # every running computer (cron mode)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { loadCredentials } = require('../utils/auth');
|
|
17
|
+
const { apiRequestJson } = require('../utils/api');
|
|
18
|
+
|
|
19
|
+
function sleep(ms) {
|
|
20
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function listBusinesses(token) {
|
|
24
|
+
const res = await apiRequestJson('/business/', { method: 'GET', token });
|
|
25
|
+
if (!res.ok) throw new Error(`Could not list businesses: ${res.status || res.error}`);
|
|
26
|
+
return res.data || [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function computerStatus(token, businessId) {
|
|
30
|
+
const res = await apiRequestJson(`/business/${businessId}/ai-computer/status`, {
|
|
31
|
+
method: 'GET',
|
|
32
|
+
token,
|
|
33
|
+
});
|
|
34
|
+
return res.ok ? res.data : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function wakeComputer(token, businessId, maxWaitSec = 90) {
|
|
38
|
+
await apiRequestJson(`/business/${businessId}/ai-computer/wake`, { method: 'POST', token });
|
|
39
|
+
const start = Date.now();
|
|
40
|
+
while (Date.now() - start < maxWaitSec * 1000) {
|
|
41
|
+
await sleep(3000);
|
|
42
|
+
const status = await computerStatus(token, businessId);
|
|
43
|
+
if (status && status.status === 'running' && status.endpoint) return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function fetchDailyReport(token, businessId) {
|
|
49
|
+
const res = await apiRequestJson(`/business/${businessId}/daily-report`, {
|
|
50
|
+
method: 'GET',
|
|
51
|
+
token,
|
|
52
|
+
timeoutMs: 60000,
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) throw new Error(`daily-report fetch failed: ${res.status || res.error}`);
|
|
55
|
+
return res.data;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function writeReportToComputer(token, businessId, workspaceId, report) {
|
|
59
|
+
const date = report.date || new Date().toISOString().slice(0, 10);
|
|
60
|
+
const remotePath = `/workspace/atris/reports/daily-${date}.md`;
|
|
61
|
+
const b64 = Buffer.from(report.markdown || '', 'utf8').toString('base64');
|
|
62
|
+
const command =
|
|
63
|
+
`mkdir -p /workspace/atris/reports && ` +
|
|
64
|
+
`echo '${b64}' | base64 -d > ${remotePath} && ` +
|
|
65
|
+
`ln -sf ${remotePath} /workspace/atris/reports/latest.md && ` +
|
|
66
|
+
`echo WROTE:${remotePath}`;
|
|
67
|
+
if (command.length > 10000) {
|
|
68
|
+
throw new Error(`report too large for one terminal write (${command.length} chars)`);
|
|
69
|
+
}
|
|
70
|
+
const res = await apiRequestJson(`/business/${businessId}/workspaces/${workspaceId}/terminal`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
token,
|
|
73
|
+
body: { command, timeout: 30 },
|
|
74
|
+
timeoutMs: 45000,
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok) throw new Error(`terminal write failed: ${res.status || res.error}`);
|
|
77
|
+
const out = (res.data && (res.data.stdout || res.data.output)) || '';
|
|
78
|
+
if (!out.includes('WROTE:')) throw new Error(`terminal write not confirmed: ${out.slice(0, 200)}`);
|
|
79
|
+
return remotePath;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function deliverToBusiness(token, biz, { wake, dryRun }) {
|
|
83
|
+
const label = biz.slug || biz.name || biz.id;
|
|
84
|
+
const status = await computerStatus(token, biz.id);
|
|
85
|
+
const running = status && status.status === 'running' && status.endpoint;
|
|
86
|
+
|
|
87
|
+
if (!running && !wake) {
|
|
88
|
+
console.log(` ${label}: computer not alive, skipping`);
|
|
89
|
+
return { business: label, delivered: false, reason: 'asleep' };
|
|
90
|
+
}
|
|
91
|
+
if (!running && wake) {
|
|
92
|
+
process.stdout.write(` ${label}: waking... `);
|
|
93
|
+
const ok = await wakeComputer(token, biz.id);
|
|
94
|
+
console.log(ok ? 'awake' : 'wake timeout');
|
|
95
|
+
if (!ok) return { business: label, delivered: false, reason: 'wake-timeout' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const report = await fetchDailyReport(token, biz.id);
|
|
99
|
+
const board = report.scoreboard || null;
|
|
100
|
+
const pnl = board
|
|
101
|
+
? ` | mrr $${board.revenue_mrr_usd} cost $${(board.compute_cost_usd + (board.ec2_cost_usd || 0)).toFixed(2)}/d profit $${board.profit_daily_usd}/d`
|
|
102
|
+
: '';
|
|
103
|
+
if (dryRun) {
|
|
104
|
+
console.log(` ${label}: dry-run, report ${String(report.markdown || '').length} chars${pnl}`);
|
|
105
|
+
return { business: label, delivered: false, reason: 'dry-run', scoreboard: board };
|
|
106
|
+
}
|
|
107
|
+
const remotePath = await writeReportToComputer(token, biz.id, biz.workspace_id, report);
|
|
108
|
+
console.log(` ${label}: delivered ${remotePath}${pnl}`);
|
|
109
|
+
return { business: label, delivered: true, path: remotePath, scoreboard: board };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function leaderboard(token, businesses) {
|
|
113
|
+
const rows = [];
|
|
114
|
+
const pool = [...businesses];
|
|
115
|
+
const workers = Array.from({ length: 5 }, async () => {
|
|
116
|
+
let biz;
|
|
117
|
+
while ((biz = pool.shift())) {
|
|
118
|
+
try {
|
|
119
|
+
const report = await fetchDailyReport(token, biz.id);
|
|
120
|
+
const b = report.scoreboard;
|
|
121
|
+
if (b) rows.push({ slug: biz.slug || biz.name, ...b });
|
|
122
|
+
} catch {
|
|
123
|
+
/* business without a readable report stays off the board */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
await Promise.all(workers);
|
|
128
|
+
|
|
129
|
+
rows.sort(
|
|
130
|
+
(a, b) =>
|
|
131
|
+
(b.revenue_collected_30d_usd || 0) - (a.revenue_collected_30d_usd || 0) ||
|
|
132
|
+
(b.profit_daily_usd || 0) - (a.profit_daily_usd || 0) ||
|
|
133
|
+
(b.revenue_mrr_usd || 0) - (a.revenue_mrr_usd || 0)
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
console.log('Computer leaderboard - collected 30d, then profit/day');
|
|
137
|
+
const fmt = (n) => `$${(n || 0).toFixed(2)}`;
|
|
138
|
+
rows.forEach((r, i) => {
|
|
139
|
+
console.log(
|
|
140
|
+
` ${String(i + 1).padStart(2)}. ${r.slug.padEnd(22)} collected ${fmt(
|
|
141
|
+
r.revenue_collected_30d_usd
|
|
142
|
+
).padStart(10)} mrr ${fmt(r.revenue_mrr_usd).padStart(10)} profit/d ${fmt(
|
|
143
|
+
r.profit_daily_usd
|
|
144
|
+
).padStart(9)}`
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
if (rows.length === 0) console.log(' (no scoreboards yet)');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function fleetReport() {
|
|
151
|
+
const args = process.argv.slice(3);
|
|
152
|
+
const allAlive = args.includes('--all-alive');
|
|
153
|
+
const wake = args.includes('--wake');
|
|
154
|
+
const dryRun = args.includes('--dry-run');
|
|
155
|
+
const board = args.includes('--leaderboard');
|
|
156
|
+
const slug = args.find((a) => !a.startsWith('--'));
|
|
157
|
+
|
|
158
|
+
if (!allAlive && !slug && !board) {
|
|
159
|
+
console.log('Usage: atris fleet-report <business> [--wake] | --all-alive [--dry-run] | --leaderboard');
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const creds = loadCredentials();
|
|
164
|
+
if (!creds || !creds.token) {
|
|
165
|
+
console.error('Not logged in. Run: atris login');
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
const token = creds.token;
|
|
169
|
+
|
|
170
|
+
const businesses = await listBusinesses(token);
|
|
171
|
+
if (board) {
|
|
172
|
+
await leaderboard(token, businesses);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const targets = allAlive
|
|
176
|
+
? businesses
|
|
177
|
+
: businesses.filter(
|
|
178
|
+
(b) => b.slug === slug || (b.name || '').toLowerCase() === slug.toLowerCase()
|
|
179
|
+
);
|
|
180
|
+
if (targets.length === 0) {
|
|
181
|
+
console.error(`No business matching "${slug}"`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log(`Fleet daily report - ${targets.length} target(s)`);
|
|
186
|
+
const results = [];
|
|
187
|
+
for (const biz of targets) {
|
|
188
|
+
try {
|
|
189
|
+
results.push(await deliverToBusiness(token, biz, { wake, dryRun }));
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.log(` ${biz.slug || biz.name}: FAILED - ${err.message}`);
|
|
192
|
+
results.push({ business: biz.slug || biz.name, delivered: false, reason: err.message });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const delivered = results.filter((r) => r.delivered).length;
|
|
196
|
+
const boards = results.map((r) => r.scoreboard).filter(Boolean);
|
|
197
|
+
if (boards.length > 0) {
|
|
198
|
+
const mrr = boards.reduce((s, b) => s + (b.revenue_mrr_usd || 0), 0);
|
|
199
|
+
const profit = boards.reduce((s, b) => s + (b.profit_daily_usd || 0), 0);
|
|
200
|
+
console.log(`Fleet: $${mrr} MRR, $${profit.toFixed(2)}/day profit across ${boards.length} scoreboards`);
|
|
201
|
+
}
|
|
202
|
+
console.log(`Done: ${delivered}/${targets.length} delivered`);
|
|
203
|
+
process.exitCode = delivered > 0 || dryRun || results.every((r) => r.reason === 'asleep') ? 0 : 1;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = { fleetReport };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const { createOfficialCliCommand } = require('../lib/official-cli-integration');
|
|
2
|
+
|
|
3
|
+
const githubCommand = createOfficialCliCommand({
|
|
4
|
+
name: 'github',
|
|
5
|
+
binary: 'gh',
|
|
6
|
+
versionArgs: ['--version'],
|
|
7
|
+
authArgs: ['auth', 'status'],
|
|
8
|
+
installHint: 'https://cli.github.com/',
|
|
9
|
+
loginHint: 'gh auth login',
|
|
10
|
+
commands: [
|
|
11
|
+
{
|
|
12
|
+
usage: 'pr list',
|
|
13
|
+
match: ['pr', 'list'],
|
|
14
|
+
forward: ['pr', 'list'],
|
|
15
|
+
description: 'list pull requests',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
usage: 'pr create',
|
|
19
|
+
match: ['pr', 'create'],
|
|
20
|
+
forward: ['pr', 'create'],
|
|
21
|
+
description: 'create a pull request',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
usage: 'pr checks',
|
|
25
|
+
match: ['pr', 'checks'],
|
|
26
|
+
forward: ['pr', 'checks'],
|
|
27
|
+
description: 'show pull request checks',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
usage: 'pr view',
|
|
31
|
+
match: ['pr', 'view'],
|
|
32
|
+
forward: ['pr', 'view'],
|
|
33
|
+
description: 'show pull request details',
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
module.exports = { githubCommand };
|