atris 3.35.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 +37 -0
- 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 +22 -4
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +2 -0
- package/ax +147 -19
- package/bin/atris.js +565 -265
- package/commands/activate.js +194 -88
- package/commands/agents.js +166 -0
- package/commands/autoland.js +459 -107
- 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/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- 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/engine.js +993 -32
- package/commands/experiments.js +28 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/gm.js +23 -0
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +72 -44
- package/commands/interview.js +67 -1
- package/commands/land.js +152 -52
- package/commands/lifecycle.js +39 -3
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +511 -34
- package/commands/mission.js +3029 -339
- package/commands/next.js +137 -0
- package/commands/now.js +220 -25
- 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/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/stream.js +861 -0
- package/commands/study.js +693 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +1346 -117
- package/commands/team.js +73 -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 +234 -13
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +331 -34
- package/lib/autoland.js +319 -54
- 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 +2219 -67
- 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/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/next-moves.js +327 -10
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -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/router-brain.js +352 -0
- package/lib/runner-command.js +10 -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 +108 -29
- package/lib/task-proof.js +23 -1
- 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/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/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 };
|
package/commands/gm.js
CHANGED
|
@@ -629,12 +629,35 @@ function render(state) {
|
|
|
629
629
|
for (const command of state.next_commands) console.log(`- ${command}`);
|
|
630
630
|
}
|
|
631
631
|
|
|
632
|
+
// `gm <name>` is how people greet a team member in chat ("gm maze"). When the
|
|
633
|
+
// positional names a real member (and not the manager persona), the same phrase
|
|
634
|
+
// works in the terminal: wake that member instead of entering manager mode.
|
|
635
|
+
function wakeableMember(args) {
|
|
636
|
+
const name = slugify(positional(args)[0]);
|
|
637
|
+
if (!name || name === 'game-manager' || flag(args, '--manager')) return null;
|
|
638
|
+
const memberDir = path.join(process.cwd(), 'atris', 'team', name);
|
|
639
|
+
if (!fs.existsSync(path.join(memberDir, 'MEMBER.md'))) return null;
|
|
640
|
+
return name;
|
|
641
|
+
}
|
|
642
|
+
|
|
632
643
|
async function gmCommand(...args) {
|
|
633
644
|
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
634
645
|
showHelp();
|
|
635
646
|
return;
|
|
636
647
|
}
|
|
637
648
|
|
|
649
|
+
const member = wakeableMember(args);
|
|
650
|
+
if (member) {
|
|
651
|
+
const { isMemberAwake } = require('../lib/member-switches');
|
|
652
|
+
if (!isMemberAwake(member)) {
|
|
653
|
+
console.log(`${member} is asleep`);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const { memberCommand } = require('./member');
|
|
657
|
+
const passthrough = args.filter(arg => arg !== member);
|
|
658
|
+
return memberCommand('wake', member, ...passthrough);
|
|
659
|
+
}
|
|
660
|
+
|
|
638
661
|
const state = gmState(args);
|
|
639
662
|
if (hasFlag(args, '--json')) {
|
|
640
663
|
console.log(JSON.stringify(state, null, 2));
|
package/commands/goal.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const SCOREBOARD_FILE = path.join('.atris', 'state', 'scoreboard.json');
|
|
7
|
+
const TASK_PROJECTION_FILE = path.join('.atris', 'state', 'tasks.projection.json');
|
|
8
|
+
|
|
9
|
+
function normalizeText(value) {
|
|
10
|
+
return String(value || '')
|
|
11
|
+
.replace(/[\u2013\u2014]/g, '-')
|
|
12
|
+
.replace(/\s+/g, ' ')
|
|
13
|
+
.trim()
|
|
14
|
+
.toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function scoreboardPath(root = process.cwd()) {
|
|
18
|
+
return path.join(root, SCOREBOARD_FILE);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function emptyScoreboard() {
|
|
22
|
+
return { goal: null, metrics: [], updated_at: null };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readScoreboard(root = process.cwd()) {
|
|
26
|
+
const file = scoreboardPath(root);
|
|
27
|
+
if (!fs.existsSync(file)) return emptyScoreboard();
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
30
|
+
return {
|
|
31
|
+
goal: normalizeText(parsed?.goal) || null,
|
|
32
|
+
metrics: Array.isArray(parsed?.metrics)
|
|
33
|
+
? parsed.metrics
|
|
34
|
+
.map((metric) => ({
|
|
35
|
+
name: normalizeText(metric?.name),
|
|
36
|
+
value: normalizeText(metric?.value),
|
|
37
|
+
}))
|
|
38
|
+
.filter((metric) => metric.name && metric.value)
|
|
39
|
+
: [],
|
|
40
|
+
updated_at: parsed?.updated_at || null,
|
|
41
|
+
};
|
|
42
|
+
} catch {
|
|
43
|
+
return emptyScoreboard();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writeScoreboard(scoreboard, root = process.cwd()) {
|
|
48
|
+
const file = scoreboardPath(root);
|
|
49
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
50
|
+
const next = {
|
|
51
|
+
goal: normalizeText(scoreboard.goal),
|
|
52
|
+
metrics: Array.isArray(scoreboard.metrics) ? scoreboard.metrics : [],
|
|
53
|
+
updated_at: new Date().toISOString(),
|
|
54
|
+
};
|
|
55
|
+
const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
56
|
+
fs.writeFileSync(temp, `${JSON.stringify(next)}\n`, 'utf8');
|
|
57
|
+
fs.renameSync(temp, file);
|
|
58
|
+
return next;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function timestampMs(value) {
|
|
62
|
+
if (value === null || value === undefined || value === '') return null;
|
|
63
|
+
const numeric = Number(value);
|
|
64
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
65
|
+
const parsed = new Date(value).getTime();
|
|
66
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function localDateKey(value) {
|
|
70
|
+
const ms = timestampMs(value);
|
|
71
|
+
if (ms === null) return null;
|
|
72
|
+
const date = new Date(ms);
|
|
73
|
+
const year = date.getFullYear();
|
|
74
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
75
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
76
|
+
return `${year}-${month}-${day}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function taskDoneTime(task) {
|
|
80
|
+
if (task?.done_at) return task.done_at;
|
|
81
|
+
const events = Array.isArray(task?.events) ? task.events : [];
|
|
82
|
+
const done = [...events].reverse().find((event) => ['completed', 'done', 'accepted'].includes(
|
|
83
|
+
normalizeText(event?.event_type),
|
|
84
|
+
));
|
|
85
|
+
return done?.created_at || null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readTaskMovement(root = process.cwd(), now = new Date()) {
|
|
89
|
+
const file = path.join(root, TASK_PROJECTION_FILE);
|
|
90
|
+
if (!fs.existsSync(file)) return { available: false, landed: 0, review: 0 };
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
93
|
+
if (!Array.isArray(parsed?.tasks)) return { available: false, landed: 0, review: 0 };
|
|
94
|
+
const today = localDateKey(now);
|
|
95
|
+
return {
|
|
96
|
+
available: true,
|
|
97
|
+
landed: parsed.tasks.filter((task) => (
|
|
98
|
+
normalizeText(task?.status) === 'done'
|
|
99
|
+
&& localDateKey(taskDoneTime(task)) === today
|
|
100
|
+
)).length,
|
|
101
|
+
review: parsed.tasks.filter((task) => normalizeText(task?.status) === 'review').length,
|
|
102
|
+
};
|
|
103
|
+
} catch {
|
|
104
|
+
return { available: false, landed: 0, review: 0 };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readMissionMovement(root = process.cwd(), now = new Date()) {
|
|
109
|
+
const file = path.join(root, '.atris', 'state', 'missions.jsonl');
|
|
110
|
+
if (!fs.existsSync(file)) return { available: false, completed: 0 };
|
|
111
|
+
try {
|
|
112
|
+
const { listMissions } = require('./mission');
|
|
113
|
+
const today = localDateKey(now);
|
|
114
|
+
const completed = listMissions(root).filter((mission) => (
|
|
115
|
+
['complete', 'completed'].includes(normalizeText(mission?.status))
|
|
116
|
+
&& localDateKey(mission?.completed_at || mission?.updated_at) === today
|
|
117
|
+
)).length;
|
|
118
|
+
return { available: true, completed };
|
|
119
|
+
} catch {
|
|
120
|
+
return { available: false, completed: 0 };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function plural(count, singular) {
|
|
125
|
+
return `${count} ${singular}${count === 1 ? '' : 's'}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function todayText(tasks, missions) {
|
|
129
|
+
const parts = [];
|
|
130
|
+
if (tasks.available) {
|
|
131
|
+
parts.push(`${plural(tasks.landed, 'task')} landed`);
|
|
132
|
+
parts.push(`${tasks.review} waiting for your ok`);
|
|
133
|
+
}
|
|
134
|
+
if (missions.available) parts.push(`${plural(missions.completed, 'mission')} completed`);
|
|
135
|
+
return parts.length ? parts.join(', ') : 'no movement recorded today';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildGoalView(root = process.cwd(), now = new Date()) {
|
|
139
|
+
const scoreboard = readScoreboard(root);
|
|
140
|
+
const tasks = readTaskMovement(root, now);
|
|
141
|
+
const missions = readMissionMovement(root, now);
|
|
142
|
+
const today = todayText(tasks, missions);
|
|
143
|
+
const next = tasks.review > 0
|
|
144
|
+
? 'atris task reviews'
|
|
145
|
+
: scoreboard.goal
|
|
146
|
+
? 'atris task day'
|
|
147
|
+
: 'atris goal set "<sentence>"';
|
|
148
|
+
return {
|
|
149
|
+
goal: scoreboard.goal,
|
|
150
|
+
metrics: scoreboard.metrics,
|
|
151
|
+
today,
|
|
152
|
+
next,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function row(label, value) {
|
|
157
|
+
return ` ${label.padEnd(10)}${value}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function renderGoalView(view) {
|
|
161
|
+
const goal = view.goal || 'goal not set. set it: atris goal set "<sentence>"';
|
|
162
|
+
const lines = [row('goal', goal)];
|
|
163
|
+
if (view.metrics.length > 0) {
|
|
164
|
+
lines.push(row('distance', view.metrics.map((metric) => `${metric.name}: ${metric.value}`).join(' | ')));
|
|
165
|
+
}
|
|
166
|
+
lines.push(row('today', view.today));
|
|
167
|
+
lines.push(row('next', view.next));
|
|
168
|
+
return lines.join('\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function printHelp() {
|
|
172
|
+
console.log('usage: atris goal [--json]');
|
|
173
|
+
console.log(' atris goal set "<sentence>"');
|
|
174
|
+
console.log(' atris goal metric <name> <value>');
|
|
175
|
+
console.log(' atris goal metric <name> --rm');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function run(args = [], context = {}) {
|
|
179
|
+
const root = context.cwd || process.cwd();
|
|
180
|
+
const now = context.now || new Date();
|
|
181
|
+
const command = args[0];
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
if (args.includes('--help') || args.includes('-h') || command === 'help') {
|
|
185
|
+
printHelp();
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (command === 'set') {
|
|
190
|
+
const goal = normalizeText(args.slice(1).join(' '));
|
|
191
|
+
if (!goal) {
|
|
192
|
+
console.error('error: goal sentence is required');
|
|
193
|
+
return 2;
|
|
194
|
+
}
|
|
195
|
+
const scoreboard = readScoreboard(root);
|
|
196
|
+
writeScoreboard({ ...scoreboard, goal }, root);
|
|
197
|
+
console.log('goal set.');
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (command === 'metric') {
|
|
202
|
+
const name = normalizeText(args[1]);
|
|
203
|
+
if (!name) {
|
|
204
|
+
console.error('error: metric name is required');
|
|
205
|
+
return 2;
|
|
206
|
+
}
|
|
207
|
+
const scoreboard = readScoreboard(root);
|
|
208
|
+
const metrics = scoreboard.metrics.filter((metric) => metric.name !== name);
|
|
209
|
+
if (args.includes('--rm')) {
|
|
210
|
+
writeScoreboard({ ...scoreboard, metrics }, root);
|
|
211
|
+
console.log(`metric ${name} removed.`);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
const value = normalizeText(args.slice(2).join(' '));
|
|
215
|
+
if (!value) {
|
|
216
|
+
console.error('error: metric value is required');
|
|
217
|
+
return 2;
|
|
218
|
+
}
|
|
219
|
+
metrics.push({ name, value });
|
|
220
|
+
writeScoreboard({ ...scoreboard, metrics }, root);
|
|
221
|
+
console.log(`metric ${name} set.`);
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (command && command !== '--json') {
|
|
226
|
+
console.error(`error: unknown goal command: ${normalizeText(command)}`);
|
|
227
|
+
return 2;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const view = buildGoalView(root, now);
|
|
231
|
+
console.log(args.includes('--json') ? JSON.stringify(view, null, 2) : renderGoalView(view));
|
|
232
|
+
return 0;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error(`error: ${normalizeText(error?.message || error)}`);
|
|
235
|
+
return 2;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = {
|
|
240
|
+
SCOREBOARD_FILE,
|
|
241
|
+
buildGoalView,
|
|
242
|
+
readScoreboard,
|
|
243
|
+
renderGoalView,
|
|
244
|
+
run,
|
|
245
|
+
scoreboardPath,
|
|
246
|
+
writeScoreboard,
|
|
247
|
+
};
|