atris 3.37.0 → 3.38.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/README.md +2 -0
- package/atris/PERSONA.md +3 -7
- package/atris.md +10 -2
- package/ax +158 -33
- package/bin/atris.js +49 -2
- package/commands/activate.js +16 -3
- package/commands/agent-spawn.js +0 -7
- package/commands/autoland.js +92 -14
- package/commands/autopilot-front.js +0 -4
- package/commands/autopilot.js +0 -12
- package/commands/brain.js +0 -7
- package/commands/brief.js +0 -2
- package/commands/business-sync.js +0 -1
- package/commands/business.js +107 -12
- package/commands/clean.js +31 -2
- package/commands/close.js +46 -11
- package/commands/cloud.js +0 -1
- package/commands/codex-goal.js +0 -2
- package/commands/computer.js +6 -5
- package/commands/console.js +46 -9
- package/commands/deck.js +29 -8
- package/commands/dream.js +0 -7
- package/commands/drill.js +0 -1
- package/commands/engine.js +10 -18
- package/commands/experiments.js +0 -4
- package/commands/fleet-report.js +12 -4
- package/commands/game.js +261 -0
- package/commands/gm.js +10 -2
- package/commands/goal.js +0 -6
- package/commands/harvest.js +0 -1
- package/commands/improve.js +29 -8
- package/commands/init.js +18 -34
- package/commands/integrations.js +0 -7
- package/commands/land.js +71 -8
- package/commands/launchpad.js +0 -2
- package/commands/lesson.js +17 -7
- package/commands/loop-front.js +0 -2
- package/commands/loops.js +6 -5
- package/commands/meet.js +0 -2
- package/commands/member.js +68 -3
- package/commands/mission.js +359 -102
- package/commands/next.js +0 -1
- package/commands/now.js +0 -12
- package/commands/one-lap.js +8 -8
- package/commands/orb.js +0 -4
- package/commands/pack-craft.js +0 -1
- package/commands/pack.js +670 -77
- package/commands/play.js +0 -2
- package/commands/probe.js +10 -4
- package/commands/pulse.js +73 -48
- package/commands/push.js +0 -4
- package/commands/radar.js +14 -11
- package/commands/recover.js +340 -0
- package/commands/report.js +0 -1
- package/commands/router.js +319 -0
- package/commands/run-front.js +0 -2
- package/commands/search.js +0 -3
- package/commands/skill.js +7 -2
- package/commands/slop.js +225 -7
- package/commands/status.js +6 -0
- package/commands/stream.js +0 -2
- package/commands/study.js +0 -3
- package/commands/sync-checkout.js +51 -0
- package/commands/sync.js +11 -40
- package/commands/task.js +125 -39
- package/commands/teach.js +86 -0
- package/commands/unknowns.js +0 -10
- package/commands/update.js +0 -1
- package/commands/visualize.js +0 -5
- package/commands/watch.js +0 -6
- package/commands/wiki.js +41 -11
- package/commands/wish.js +0 -1
- package/commands/worktree.js +87 -14
- package/commands/xp.js +1 -6
- package/commands/youtube.js +0 -2
- package/lib/accept-verify-gate.js +122 -0
- package/lib/activity-stream.js +0 -2
- package/lib/auto-accept-certified.js +145 -2
- package/lib/autoland.js +0 -3
- package/lib/ax-auto-lane.js +94 -10
- package/lib/bench/context.js +0 -2
- package/lib/bench/report.js +0 -1
- package/lib/bench/runner.js +0 -11
- package/lib/brief-ledger.js +0 -6
- package/lib/business-simulate.js +382 -0
- package/lib/chat-log-scan.js +0 -5
- package/lib/checkout-sync.js +52 -0
- package/lib/claude-boot-block.js +48 -0
- package/lib/cloud-mission.js +0 -6
- package/lib/codex-flight.js +0 -1
- package/lib/conductor-artifacts.js +25 -0
- package/lib/context-gatherer.js +0 -1
- package/lib/deck-compose.js +0 -1
- package/lib/deck-history.js +0 -1
- package/lib/deck-layout.js +0 -5
- package/lib/deck-review.js +0 -7
- package/lib/deck-schema.js +0 -1
- package/lib/default-verifier.js +0 -2
- package/lib/engine-registry.js +16 -8
- package/lib/experiments/daily.js +0 -6
- package/lib/fleet.js +210 -27
- package/lib/functional-owner.js +0 -3
- package/lib/inspect-fields.js +0 -1
- package/lib/known-commands.js +2 -2
- package/lib/learnings.js +0 -2
- package/lib/loop-doctor.js +11 -17
- package/lib/manifest.js +0 -2
- package/lib/member-alive.js +0 -1
- package/lib/member-switches.js +0 -5
- package/lib/mission-artifact.js +0 -1
- package/lib/mission-protected-lane.js +293 -0
- package/lib/mission-room.js +0 -2
- package/lib/mission-runtime-loop.js +0 -1
- package/lib/next-moves.js +0 -18
- package/lib/official-cli-integration.js +0 -4
- package/lib/orb-context.js +6 -3
- package/lib/orb-scorecard.js +0 -1
- package/lib/outbound-send-gate.js +0 -6
- package/lib/permission-grants.js +0 -6
- package/lib/policy-lessons.js +0 -2
- package/lib/pulse.js +68 -15
- package/lib/review-integrity.js +0 -2
- package/lib/router-brain.js +0 -2
- package/lib/runner-command.js +0 -4
- package/lib/runtime-bootstrap.js +0 -1
- package/lib/scorecard.js +0 -1
- package/lib/self-drive.js +1 -1
- package/lib/state-detection.js +0 -6
- package/lib/task-db.js +0 -4
- package/lib/team-presence.js +0 -1
- package/lib/voice-gate.js +63 -2
- package/lib/wiki.js +152 -9
- package/lib/wish-audit.js +20 -6
- package/lib/wish-delegate.js +26 -20
- package/lib/wish-stats.js +0 -5
- package/lib/wish-store.js +0 -1
- package/lib/workspace-safety.js +0 -2
- package/lib/zip.js +0 -1
- package/package.json +1 -1
package/commands/codex-goal.js
CHANGED
package/commands/computer.js
CHANGED
|
@@ -1405,7 +1405,7 @@ function computerLocalLegacy(extraArgs = []) {
|
|
|
1405
1405
|
}
|
|
1406
1406
|
}
|
|
1407
1407
|
|
|
1408
|
-
function computerLocal(extraArgs = []) {
|
|
1408
|
+
function computerLocal(extraArgs = [], options = {}) {
|
|
1409
1409
|
printLocalWordmark();
|
|
1410
1410
|
printModeBanner('LOCAL', process.cwd(), [
|
|
1411
1411
|
'Claude Code + Atris workspace context.',
|
|
@@ -1417,7 +1417,7 @@ function computerLocal(extraArgs = []) {
|
|
|
1417
1417
|
const originalArgv = process.argv;
|
|
1418
1418
|
process.argv = [originalArgv[0], originalArgv[1], originalArgv[2], 'claude', ...extraArgs];
|
|
1419
1419
|
try {
|
|
1420
|
-
consoleCommand();
|
|
1420
|
+
consoleCommand(options);
|
|
1421
1421
|
} finally {
|
|
1422
1422
|
process.argv = originalArgv;
|
|
1423
1423
|
}
|
|
@@ -4390,11 +4390,12 @@ async function runComputer(argv = process.argv.slice(3), deps = {}) {
|
|
|
4390
4390
|
module.exports = {
|
|
4391
4391
|
runComputer,
|
|
4392
4392
|
computerSetup,
|
|
4393
|
-
|
|
4393
|
+
// `atris pack run` starts a packet as a free local computer, so it needs the
|
|
4394
|
+
// local starter and the business-binding check as seams, not copies.
|
|
4395
|
+
computerLocal,
|
|
4396
|
+
readBusinessBinding,
|
|
4394
4397
|
formatEngineSeats,
|
|
4395
4398
|
buildComputerCard,
|
|
4396
|
-
renderComputerCard,
|
|
4397
|
-
renderComputerCardMarkdown,
|
|
4398
4399
|
extractAttachedWorkspaceMismatch,
|
|
4399
4400
|
contextForAttachedWorkspaceMismatch,
|
|
4400
4401
|
printRecruitingLocalSyncOutcome,
|
package/commands/console.js
CHANGED
|
@@ -7,10 +7,40 @@ const { resolveClaudeRunnerBin } = require('../lib/runner-command');
|
|
|
7
7
|
|
|
8
8
|
// ── Context Gathering ──────────────────────────────────────────────
|
|
9
9
|
|
|
10
|
+
// Atris content lives in two shapes on disk, and the console has to read both.
|
|
11
|
+
//
|
|
12
|
+
// workspace: <dir>/atris/{skills,team,TODO.md,...}
|
|
13
|
+
// The normal repo shape. A project keeps its Atris brain in a subfolder so
|
|
14
|
+
// it does not collide with the project's own files. This is the common case.
|
|
15
|
+
//
|
|
16
|
+
// packet: <dir>/{pack.json,skills,team,docs,...}
|
|
17
|
+
// A packet built in root mode (pack.js exports with prefix '' when the
|
|
18
|
+
// source dir has pack.json at its root — this is how the shipped packs are
|
|
19
|
+
// built and what the web /packs/<slug>/download route serves). `pack
|
|
20
|
+
// install` preserves that flat layout, so the packet folder IS the atris
|
|
21
|
+
// root; there is no nested atris/ to find.
|
|
22
|
+
//
|
|
23
|
+
// Nested wins when both exist: never change what an existing workspace loads.
|
|
24
|
+
// Returns the directory that actually holds the atris content, or null.
|
|
25
|
+
function resolveAtrisRoot(workspaceDir) {
|
|
26
|
+
const nested = path.join(workspaceDir, 'atris');
|
|
27
|
+
try {
|
|
28
|
+
if (fs.statSync(nested).isDirectory()) return nested;
|
|
29
|
+
} catch {}
|
|
30
|
+
const manifest = path.join(workspaceDir, 'pack.json');
|
|
31
|
+
try {
|
|
32
|
+
if (!fs.statSync(manifest).isFile()) return null;
|
|
33
|
+
JSON.parse(fs.readFileSync(manifest, 'utf8'));
|
|
34
|
+
return workspaceDir;
|
|
35
|
+
} catch {}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
10
39
|
function gatherAtrisContext(workspaceDir) {
|
|
11
|
-
const atrisDir =
|
|
40
|
+
const atrisDir = resolveAtrisRoot(workspaceDir);
|
|
12
41
|
const ctx = {
|
|
13
|
-
hasAtris:
|
|
42
|
+
hasAtris: Boolean(atrisDir),
|
|
43
|
+
atrisRoot: atrisDir,
|
|
14
44
|
skills: [],
|
|
15
45
|
teamMembers: [],
|
|
16
46
|
backlogCount: 0,
|
|
@@ -119,7 +149,8 @@ function buildSystemPrompt(ctx) {
|
|
|
119
149
|
if (ctx.teamMembers.length > 0) {
|
|
120
150
|
lines.push('## Team Members');
|
|
121
151
|
lines.push(`Available: ${ctx.teamMembers.join(', ')}`);
|
|
122
|
-
|
|
152
|
+
const teamRoot = ctx.atrisRoot ? path.join(ctx.atrisRoot, 'team') : 'atris/team';
|
|
153
|
+
lines.push(`Each has a MEMBER.md in ${teamRoot}/<name>/ defining their role.`);
|
|
123
154
|
lines.push('');
|
|
124
155
|
}
|
|
125
156
|
|
|
@@ -135,7 +166,8 @@ function buildSystemPrompt(ctx) {
|
|
|
135
166
|
|
|
136
167
|
if (ctx.backlogCount > 0) {
|
|
137
168
|
lines.push(`## Current Work`);
|
|
138
|
-
|
|
169
|
+
const todoPath = ctx.atrisRoot ? path.join(ctx.atrisRoot, 'TODO.md') : 'atris/TODO.md';
|
|
170
|
+
lines.push(`${ctx.backlogCount} task${ctx.backlogCount > 1 ? 's' : ''} in backlog. Check ${todoPath} for details.`);
|
|
139
171
|
lines.push('');
|
|
140
172
|
}
|
|
141
173
|
|
|
@@ -280,10 +312,11 @@ function checkAuth(backend) {
|
|
|
280
312
|
|
|
281
313
|
// ── Launch ──────────────────────────────────────────────────────────
|
|
282
314
|
|
|
283
|
-
function launchClaude(systemPrompt, extraArgs) {
|
|
315
|
+
function launchClaude(systemPrompt, extraArgs, options = {}) {
|
|
316
|
+
const skipPermissions = options.skipPermissions !== false;
|
|
284
317
|
const runnerBin = resolveClaudeRunnerBin();
|
|
285
318
|
const args = [
|
|
286
|
-
'--dangerously-skip-permissions',
|
|
319
|
+
...(skipPermissions ? ['--dangerously-skip-permissions'] : []),
|
|
287
320
|
'--append-system-prompt', systemPrompt,
|
|
288
321
|
...extraArgs,
|
|
289
322
|
];
|
|
@@ -342,7 +375,11 @@ function launchCodex(systemPrompt, extraArgs) {
|
|
|
342
375
|
|
|
343
376
|
// ── Main ────────────────────────────────────────────────────────────
|
|
344
377
|
|
|
345
|
-
|
|
378
|
+
// skipPermissions defaults to true: `atris console` runs on a workspace you
|
|
379
|
+
// wrote. Callers that launch an agent inside content someone else authored
|
|
380
|
+
// (`atris pack run`) pass false so permission prompts stay on.
|
|
381
|
+
function consoleCommand(options = {}) {
|
|
382
|
+
const skipPermissions = options.skipPermissions !== false;
|
|
346
383
|
const args = process.argv.slice(3);
|
|
347
384
|
let requested = args[0];
|
|
348
385
|
|
|
@@ -385,7 +422,7 @@ function consoleCommand() {
|
|
|
385
422
|
|
|
386
423
|
// Launch
|
|
387
424
|
if (backend === 'claude') {
|
|
388
|
-
launchClaude(systemPrompt, extraArgs);
|
|
425
|
+
launchClaude(systemPrompt, extraArgs, { skipPermissions });
|
|
389
426
|
} else {
|
|
390
427
|
launchCodex(systemPrompt, extraArgs);
|
|
391
428
|
}
|
|
@@ -399,4 +436,4 @@ function consoleCommand() {
|
|
|
399
436
|
}
|
|
400
437
|
}
|
|
401
438
|
|
|
402
|
-
module.exports = { consoleCommand, gatherAtrisContext, buildSystemPrompt };
|
|
439
|
+
module.exports = { consoleCommand, gatherAtrisContext, buildSystemPrompt, resolveAtrisRoot, launchClaude };
|
package/commands/deck.js
CHANGED
|
@@ -141,6 +141,23 @@ function readSpec(specPath) {
|
|
|
141
141
|
return JSON.parse(fs.readFileSync(specPath, 'utf8'));
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
// Pluralize a count + noun ("1 error", "2 errors").
|
|
145
|
+
function countLabel(n, word) {
|
|
146
|
+
return `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// True when the args carry an explicit help flag.
|
|
150
|
+
function wantsHelp(argv) {
|
|
151
|
+
return argv.includes('--help') || argv.includes('-h');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// One-line lint tally with correct singular/plural nouns.
|
|
155
|
+
function lintSummary(findings) {
|
|
156
|
+
const errs = findings.filter((f) => f.severity === 'error').length;
|
|
157
|
+
const warns = findings.filter((f) => f.severity === 'warn').length;
|
|
158
|
+
return `${countLabel(errs, 'error')}, ${countLabel(warns, 'warning')}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
144
161
|
function printLint(findings) {
|
|
145
162
|
if (!findings.length) {
|
|
146
163
|
console.log('\n ✓ spec lint clean\n');
|
|
@@ -369,15 +386,13 @@ async function run(argv) {
|
|
|
369
386
|
const findings = checkSpec(spec, lintSpec);
|
|
370
387
|
if (out) {
|
|
371
388
|
fs.writeFileSync(out, `${JSON.stringify(spec, null, 2)}\n`);
|
|
372
|
-
console.error(`\n ✓ composed ${spec.slides.length}
|
|
389
|
+
console.error(`\n ✓ composed ${countLabel(spec.slides.length, 'slide')} (${spec.theme}) -> ${out}`);
|
|
373
390
|
printLint(findings);
|
|
374
391
|
console.error(` next: atris deck build ${out} --review\n`);
|
|
375
392
|
} else {
|
|
376
393
|
// stdout stays clean JSON for piping; lint goes to stderr
|
|
377
394
|
console.log(JSON.stringify(spec, null, 2));
|
|
378
|
-
|
|
379
|
-
const warns = findings.filter((f) => f.severity === 'warn').length;
|
|
380
|
-
console.error(` composed ${spec.slides.length} slides (${spec.theme}) · lint: ${errs} errors, ${warns} warnings`);
|
|
395
|
+
console.error(` composed ${countLabel(spec.slides.length, 'slide')} (${spec.theme}) · lint: ${lintSummary(findings)}`);
|
|
381
396
|
}
|
|
382
397
|
return hasErrors(findings) ? 1 : 0;
|
|
383
398
|
}
|
|
@@ -385,8 +400,11 @@ async function run(argv) {
|
|
|
385
400
|
if (sub === 'lint') {
|
|
386
401
|
const specPath = argv.slice(1).find((a) => !a.startsWith('-'));
|
|
387
402
|
if (!specPath) {
|
|
388
|
-
|
|
389
|
-
|
|
403
|
+
// Explicit -h/--help is a help request (stdout, exit 0); a genuinely
|
|
404
|
+
// missing spec path is a usage error (stderr, exit 2).
|
|
405
|
+
const help = wantsHelp(argv);
|
|
406
|
+
console[help ? 'log' : 'error'](' usage: atris deck lint <spec.json>');
|
|
407
|
+
return help ? 0 : 2;
|
|
390
408
|
}
|
|
391
409
|
let spec;
|
|
392
410
|
try { spec = readSpec(specPath); }
|
|
@@ -488,8 +506,9 @@ async function run(argv) {
|
|
|
488
506
|
}
|
|
489
507
|
const specPath = specPaths[0];
|
|
490
508
|
if (!specPath) {
|
|
491
|
-
|
|
492
|
-
|
|
509
|
+
const help = wantsHelp(argv);
|
|
510
|
+
console[help ? 'log' : 'error'](' usage: atris deck build <spec.json> [<spec2.json> ...] [--title T] [--theme x] [--review]');
|
|
511
|
+
return help ? 0 : 2;
|
|
493
512
|
}
|
|
494
513
|
let spec;
|
|
495
514
|
try { spec = readSpec(specPath); }
|
|
@@ -576,6 +595,8 @@ module.exports = {
|
|
|
576
595
|
lintSpec,
|
|
577
596
|
validateSpec,
|
|
578
597
|
checkSpec,
|
|
598
|
+
countLabel,
|
|
599
|
+
lintSummary,
|
|
579
600
|
planBatch,
|
|
580
601
|
publishDeck,
|
|
581
602
|
videoToDeck,
|
package/commands/dream.js
CHANGED
|
@@ -353,13 +353,6 @@ async function dreamCommand(args = [], root = process.cwd(), options = {}) {
|
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
module.exports = {
|
|
356
|
-
DREAM_TIMEOUT_MS,
|
|
357
356
|
dreamsPath,
|
|
358
|
-
collectDreamMaterial,
|
|
359
|
-
buildDreamPrompt,
|
|
360
|
-
parseDreamCards,
|
|
361
|
-
dreamModels,
|
|
362
|
-
defaultDreamRunner,
|
|
363
|
-
runDream,
|
|
364
357
|
dreamCommand,
|
|
365
358
|
};
|
package/commands/drill.js
CHANGED
package/commands/engine.js
CHANGED
|
@@ -33,6 +33,7 @@ const {
|
|
|
33
33
|
engineRegistryView,
|
|
34
34
|
readEngineRegistry,
|
|
35
35
|
resolveEngineForRole,
|
|
36
|
+
resolveEngineForRoleRanked,
|
|
36
37
|
setEngineHealth,
|
|
37
38
|
} = require('../lib/engine-registry');
|
|
38
39
|
const { FLEET_CAPABLE, runDispatchFlight } = require('../lib/fleet');
|
|
@@ -932,22 +933,26 @@ function runResolveCommand(args, root) {
|
|
|
932
933
|
else console.error(message);
|
|
933
934
|
return 2;
|
|
934
935
|
}
|
|
935
|
-
let
|
|
936
|
+
let picked;
|
|
936
937
|
try {
|
|
937
|
-
|
|
938
|
+
picked = resolveEngineForRoleRanked(role, root);
|
|
938
939
|
} catch (err) {
|
|
939
940
|
if (json) console.log(JSON.stringify({ ok: false, error: err.message }, null, 2));
|
|
940
941
|
else console.error(err.message);
|
|
941
942
|
return 2;
|
|
942
943
|
}
|
|
944
|
+
const engine = picked.engine;
|
|
943
945
|
if (!engine) {
|
|
944
946
|
const message = `No ready installed engine can fill role "${role}".`;
|
|
945
|
-
if (json) console.log(JSON.stringify({ ok: false, role, error: message }, null, 2));
|
|
947
|
+
if (json) console.log(JSON.stringify({ ok: false, role, error: message, won_reason: picked.reason }, null, 2));
|
|
946
948
|
else console.error(message);
|
|
947
949
|
return 1;
|
|
948
950
|
}
|
|
949
|
-
if (json) console.log(JSON.stringify(engine, null, 2));
|
|
950
|
-
else
|
|
951
|
+
if (json) console.log(JSON.stringify({ ...engine, won_reason: picked.reason }, null, 2));
|
|
952
|
+
else {
|
|
953
|
+
console.error(picked.reason);
|
|
954
|
+
console.log(engine.id);
|
|
955
|
+
}
|
|
951
956
|
return 0;
|
|
952
957
|
}
|
|
953
958
|
|
|
@@ -1294,36 +1299,23 @@ module.exports = {
|
|
|
1294
1299
|
resolveDefaultEngine,
|
|
1295
1300
|
canonicalEngineName,
|
|
1296
1301
|
readSavedEngine,
|
|
1297
|
-
setEngine,
|
|
1298
|
-
resetEngine,
|
|
1299
1302
|
roster,
|
|
1300
|
-
registryPayload,
|
|
1301
1303
|
engineRegistryFile,
|
|
1302
1304
|
readEngineRegistry,
|
|
1303
1305
|
resolveEngineForRole,
|
|
1304
1306
|
setEngineHealth,
|
|
1305
|
-
probeEngine,
|
|
1306
|
-
runEngineTest,
|
|
1307
1307
|
parseDispatchArgs,
|
|
1308
1308
|
runDispatchCommand,
|
|
1309
1309
|
ENGINE_LOGIN_MANIFESTS,
|
|
1310
|
-
MAX_ENGINE_LOGIN_FILE_BYTES,
|
|
1311
1310
|
expandHomePath,
|
|
1312
1311
|
normalizeLoginProvider,
|
|
1313
1312
|
normalizeEngineLoginSeat,
|
|
1314
1313
|
validEngineLoginSeat,
|
|
1315
1314
|
engineLoginSeatError,
|
|
1316
|
-
detectedEmailFromContent,
|
|
1317
|
-
readEngineLoginFiles,
|
|
1318
1315
|
parseEngineLoginArgs,
|
|
1319
|
-
parseEngineSeedArgs,
|
|
1320
|
-
redactBackendResponse,
|
|
1321
|
-
buildEngineLoginPayload,
|
|
1322
|
-
listEngineLogins,
|
|
1323
1316
|
listEngineSeats,
|
|
1324
1317
|
readyCheckEngineLogin,
|
|
1325
1318
|
formatEngineSeats,
|
|
1326
|
-
runEngineSeatsCommand,
|
|
1327
1319
|
runEngineDeviceLoginCommand,
|
|
1328
1320
|
runEngineLoginCommand,
|
|
1329
1321
|
runEngineSeedCommand,
|
package/commands/experiments.js
CHANGED
package/commands/fleet-report.js
CHANGED
|
@@ -79,6 +79,16 @@ async function writeReportToComputer(token, businessId, workspaceId, report) {
|
|
|
79
79
|
return remotePath;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// A missing scoreboard field must never reach the owner as "$NaN" or
|
|
83
|
+
// "$undefined"; coerce every number to 0 before it renders.
|
|
84
|
+
function formatScoreboardPnl(board) {
|
|
85
|
+
if (!board) return '';
|
|
86
|
+
const mrr = Number(board.revenue_mrr_usd) || 0;
|
|
87
|
+
const cost = (Number(board.compute_cost_usd) || 0) + (Number(board.ec2_cost_usd) || 0);
|
|
88
|
+
const profit = Number(board.profit_daily_usd) || 0;
|
|
89
|
+
return ` | mrr $${mrr} cost $${cost.toFixed(2)}/d profit $${profit}/d`;
|
|
90
|
+
}
|
|
91
|
+
|
|
82
92
|
async function deliverToBusiness(token, biz, { wake, dryRun }) {
|
|
83
93
|
const label = biz.slug || biz.name || biz.id;
|
|
84
94
|
const status = await computerStatus(token, biz.id);
|
|
@@ -97,9 +107,7 @@ async function deliverToBusiness(token, biz, { wake, dryRun }) {
|
|
|
97
107
|
|
|
98
108
|
const report = await fetchDailyReport(token, biz.id);
|
|
99
109
|
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
|
-
: '';
|
|
110
|
+
const pnl = formatScoreboardPnl(board);
|
|
103
111
|
if (dryRun) {
|
|
104
112
|
console.log(` ${label}: dry-run, report ${String(report.markdown || '').length} chars${pnl}`);
|
|
105
113
|
return { business: label, delivered: false, reason: 'dry-run', scoreboard: board };
|
|
@@ -203,4 +211,4 @@ async function fleetReport() {
|
|
|
203
211
|
process.exitCode = delivered > 0 || dryRun || results.every((r) => r.reason === 'asleep') ? 0 : 1;
|
|
204
212
|
}
|
|
205
213
|
|
|
206
|
-
module.exports = { fleetReport };
|
|
214
|
+
module.exports = { fleetReport, formatScoreboardPnl };
|
package/commands/game.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_WIDTH = 80;
|
|
8
|
+
const MIN_WIDTH = 20;
|
|
9
|
+
const MAX_WIDTH = 120;
|
|
10
|
+
const STORYLINE_SLUG_PATTERN = /^[a-z0-9-]+$/;
|
|
11
|
+
|
|
12
|
+
function isValidStorylineSlug(slug) {
|
|
13
|
+
return typeof slug === 'string' && STORYLINE_SLUG_PATTERN.test(slug);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseGameArgs(args = []) {
|
|
17
|
+
let storyline = null;
|
|
18
|
+
|
|
19
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
20
|
+
const arg = args[index];
|
|
21
|
+
if (arg === '--storyline') {
|
|
22
|
+
const value = args[index + 1];
|
|
23
|
+
if (!value || value.startsWith('--')) {
|
|
24
|
+
return { error: 'missing storyline slug' };
|
|
25
|
+
}
|
|
26
|
+
if (storyline) return { error: 'choose one storyline' };
|
|
27
|
+
storyline = value;
|
|
28
|
+
index += 1;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (arg.startsWith('--')) return { error: `unknown game option: ${arg}` };
|
|
32
|
+
if (storyline) return { error: 'choose one storyline' };
|
|
33
|
+
storyline = arg;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
storyline,
|
|
38
|
+
publisherArgs: storyline ? ['--json', '--storyline', storyline] : ['--json'],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function availableStorylines(workspaceRoot) {
|
|
43
|
+
const directory = path.join(workspaceRoot, 'atris', 'storylines');
|
|
44
|
+
try {
|
|
45
|
+
return fs.readdirSync(directory, { withFileTypes: true })
|
|
46
|
+
.filter(entry => entry.isFile() && entry.name.endsWith('.md'))
|
|
47
|
+
.map(entry => path.basename(entry.name, '.md'))
|
|
48
|
+
.filter(slug => slug !== 'resume-log')
|
|
49
|
+
.sort();
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function publisherSupportsStoryline(script) {
|
|
56
|
+
try {
|
|
57
|
+
return fs.readFileSync(script, 'utf8').includes('--storyline');
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function clean(value, fallback = '') {
|
|
64
|
+
const text = String(value == null ? '' : value)
|
|
65
|
+
.replace(/[—–]/g, '-')
|
|
66
|
+
.replace(/\s+/g, ' ')
|
|
67
|
+
.trim();
|
|
68
|
+
return text || fallback;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function clip(value, width) {
|
|
72
|
+
const chars = Array.from(clean(value));
|
|
73
|
+
if (chars.length <= width) return chars.join('');
|
|
74
|
+
if (width <= 3) return chars.slice(0, width).join('');
|
|
75
|
+
return `${chars.slice(0, width - 3).join('').trimEnd()}...`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function pad(value, width) {
|
|
79
|
+
const text = clip(value, width);
|
|
80
|
+
return `${text}${' '.repeat(Math.max(0, width - Array.from(text).length))}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function dashboardWidth(opts) {
|
|
84
|
+
const requested = Number(opts && opts.width);
|
|
85
|
+
if (!Number.isFinite(requested)) return DEFAULT_WIDTH;
|
|
86
|
+
return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.trunc(requested)));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function styles(enabled) {
|
|
90
|
+
const wrap = code => value => (
|
|
91
|
+
enabled ? `\x1b[${code}m${value}\x1b[0m` : String(value)
|
|
92
|
+
);
|
|
93
|
+
return {
|
|
94
|
+
bold: wrap('1'),
|
|
95
|
+
dim: wrap('2'),
|
|
96
|
+
green: wrap('32'),
|
|
97
|
+
yellow: wrap('33'),
|
|
98
|
+
cyan: wrap('36'),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function chapterLine(chapter, width, style) {
|
|
103
|
+
const state = clean(chapter && chapter.state, 'locked').toLowerCase();
|
|
104
|
+
const current = state === 'current';
|
|
105
|
+
const done = state === 'complete' || state === 'done';
|
|
106
|
+
const marker = current ? '[NOW ]' : done ? '[DONE]' : '[LOCK]';
|
|
107
|
+
const number = chapter && chapter.n != null ? `${chapter.n}.` : '';
|
|
108
|
+
const location = current ? ' YOU ARE HERE' : '';
|
|
109
|
+
const line = clip(`${marker} ${number} ${clean(chapter && chapter.title, 'Untitled chapter')}${location}`, width);
|
|
110
|
+
if (current) return style.yellow(style.bold(line));
|
|
111
|
+
if (done) return style.green(line);
|
|
112
|
+
return style.dim(line);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function missionBox(mission, width, style) {
|
|
116
|
+
const innerWidth = width - 2;
|
|
117
|
+
const contentWidth = width - 4;
|
|
118
|
+
const heading = ' CURRENT MISSION ';
|
|
119
|
+
const top = heading.length <= innerWidth
|
|
120
|
+
? `╔${heading}${'═'.repeat(innerWidth - heading.length)}╗`
|
|
121
|
+
: `╔${'═'.repeat(innerWidth)}╗`;
|
|
122
|
+
const row = value => `║ ${pad(value, contentWidth)} ║`;
|
|
123
|
+
const bottom = `╚${'═'.repeat(innerWidth)}╝`;
|
|
124
|
+
const title = clean(mission && mission.title, 'No current mission');
|
|
125
|
+
const status = clean(mission && mission.status, 'unavailable').toUpperCase();
|
|
126
|
+
return [
|
|
127
|
+
style.cyan(style.bold(top)),
|
|
128
|
+
style.bold(row(title)),
|
|
129
|
+
row(`STATUS: ${status}`),
|
|
130
|
+
style.cyan(style.bold(bottom)),
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Render an atris.game_state.v1 payload without reading process or filesystem state.
|
|
136
|
+
* Callers opt into ANSI with opts.color and provide their terminal width via opts.width.
|
|
137
|
+
*/
|
|
138
|
+
function renderGameDashboard(payload, opts = {}) {
|
|
139
|
+
const state = payload && typeof payload === 'object' ? payload : {};
|
|
140
|
+
const storyline = state.storyline && typeof state.storyline === 'object'
|
|
141
|
+
? state.storyline
|
|
142
|
+
: null;
|
|
143
|
+
const missions = state.missions && typeof state.missions === 'object'
|
|
144
|
+
? state.missions
|
|
145
|
+
: {};
|
|
146
|
+
const active = Array.isArray(missions.active) ? missions.active.slice(0, 3) : [];
|
|
147
|
+
const recentComplete = Array.isArray(missions.recent_complete)
|
|
148
|
+
? missions.recent_complete.length
|
|
149
|
+
: Number(missions.recent_complete) || 0;
|
|
150
|
+
const width = dashboardWidth(opts);
|
|
151
|
+
const style = styles(opts.color === true);
|
|
152
|
+
const title = clean(storyline && storyline.name, 'No storyline loaded');
|
|
153
|
+
const destination = clean(storyline && storyline.destination, 'No destination loaded');
|
|
154
|
+
const chapters = Array.isArray(storyline && storyline.chapters)
|
|
155
|
+
? storyline.chapters.slice(0, 7)
|
|
156
|
+
: [];
|
|
157
|
+
const streak = storyline && storyline.streak && typeof storyline.streak === 'object'
|
|
158
|
+
? storyline.streak
|
|
159
|
+
: {};
|
|
160
|
+
const streakCurrent = Number.isFinite(Number(streak.current)) ? Number(streak.current) : 0;
|
|
161
|
+
const streakTarget = Number.isFinite(Number(streak.target)) ? Number(streak.target) : 0;
|
|
162
|
+
const streakLabel = clean(streak.label, 'mission streak');
|
|
163
|
+
const lines = [];
|
|
164
|
+
|
|
165
|
+
lines.push(style.cyan(style.bold(clip(title, width))));
|
|
166
|
+
lines.push(style.dim(clip(`DESTINATION: ${destination}`, width)));
|
|
167
|
+
lines.push('');
|
|
168
|
+
lines.push(style.bold('CHAPTER RAIL'));
|
|
169
|
+
if (chapters.length) {
|
|
170
|
+
for (const chapter of chapters) lines.push(chapterLine(chapter, width, style));
|
|
171
|
+
} else {
|
|
172
|
+
lines.push(style.dim(clip('[LOCK] No chapters loaded', width)));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
lines.push('');
|
|
176
|
+
lines.push(...missionBox(storyline && storyline.mission, width, style));
|
|
177
|
+
lines.push('');
|
|
178
|
+
lines.push(style.yellow(style.bold(clip(`STREAK: ${streakCurrent} / ${streakTarget} | ${streakLabel}`, width))));
|
|
179
|
+
lines.push('');
|
|
180
|
+
lines.push(style.bold('LIVE MISSIONS'));
|
|
181
|
+
if (active.length) {
|
|
182
|
+
for (const mission of active) {
|
|
183
|
+
const id = mission && mission.id != null ? `#${mission.id}` : '#?';
|
|
184
|
+
lines.push(style.bold(clip(`${id} ${clean(mission && mission.title, 'Untitled mission')}`, width)));
|
|
185
|
+
lines.push(style.dim(clip(` owner: ${clean(mission && mission.owner, 'unassigned')}`, width)));
|
|
186
|
+
lines.push(clip(` next: ${clean(mission && mission.next, 'No next action recorded')}`, width));
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
lines.push(style.dim(clip('No active missions.', width)));
|
|
190
|
+
}
|
|
191
|
+
lines.push(clip(`RECENT COMPLETES: ${recentComplete}`, width));
|
|
192
|
+
lines.push('');
|
|
193
|
+
lines.push(style.cyan(style.bold(clip(`NEXT MOVE: ${clean(active[0] && active[0].next, 'No active mission')}`, width))));
|
|
194
|
+
|
|
195
|
+
return lines.join('\n');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function loadGameState(workspaceRoot = process.cwd(), publisherArgs = ['--json']) {
|
|
199
|
+
const script = path.join(workspaceRoot, 'scripts', 'game_state_sync.py');
|
|
200
|
+
const python = path.join(workspaceRoot, 'venv', 'bin', 'python');
|
|
201
|
+
if (!fs.existsSync(script) || !fs.existsSync(python)) return null;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const stdout = execFileSync(python, [script, ...publisherArgs], {
|
|
205
|
+
cwd: workspaceRoot,
|
|
206
|
+
encoding: 'utf8',
|
|
207
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
208
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
209
|
+
});
|
|
210
|
+
const payload = JSON.parse(stdout.trim());
|
|
211
|
+
return payload && typeof payload === 'object' ? payload : null;
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function gameCommand(args = [], opts = {}) {
|
|
218
|
+
const parsed = parseGameArgs(args);
|
|
219
|
+
if (parsed.error) {
|
|
220
|
+
console.error(parsed.error);
|
|
221
|
+
return 1;
|
|
222
|
+
}
|
|
223
|
+
if (parsed.storyline && !isValidStorylineSlug(parsed.storyline)) {
|
|
224
|
+
console.error(`invalid storyline slug: ${parsed.storyline}`);
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const workspaceRoot = process.cwd();
|
|
229
|
+
if (parsed.storyline) {
|
|
230
|
+
const storylines = availableStorylines(workspaceRoot);
|
|
231
|
+
if (!storylines.includes(parsed.storyline)) {
|
|
232
|
+
console.error(`storyline not found: ${parsed.storyline}. available: ${storylines.join(', ') || 'none'}`);
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const script = path.join(workspaceRoot, 'scripts', 'game_state_sync.py');
|
|
238
|
+
const publisherArgs = parsed.storyline && publisherSupportsStoryline(script)
|
|
239
|
+
? parsed.publisherArgs
|
|
240
|
+
: ['--json'];
|
|
241
|
+
const payload = loadGameState(workspaceRoot, publisherArgs);
|
|
242
|
+
if (!payload) {
|
|
243
|
+
if (!opts.silentMissing) {
|
|
244
|
+
console.log('no game state in this workspace (needs atris/storylines/)');
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(renderGameDashboard(payload, {
|
|
250
|
+
color: Boolean(process.stdout.isTTY) && !process.env.NO_COLOR,
|
|
251
|
+
width: process.stdout.columns || DEFAULT_WIDTH,
|
|
252
|
+
}));
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = {
|
|
257
|
+
renderGameDashboard,
|
|
258
|
+
isValidStorylineSlug,
|
|
259
|
+
parseGameArgs,
|
|
260
|
+
gameCommand,
|
|
261
|
+
};
|
package/commands/gm.js
CHANGED
|
@@ -658,6 +658,16 @@ async function gmCommand(...args) {
|
|
|
658
658
|
return memberCommand('wake', member, ...passthrough);
|
|
659
659
|
}
|
|
660
660
|
|
|
661
|
+
if (!hasFlag(args, '--json') && !hasFlag(args, '--watch')) {
|
|
662
|
+
try {
|
|
663
|
+
const { gameCommand } = require('./game');
|
|
664
|
+
const dashboardShown = await gameCommand([], { silentMissing: true });
|
|
665
|
+
if (dashboardShown) console.log('');
|
|
666
|
+
} catch {
|
|
667
|
+
// The dashboard is an optional opening screen and must never block GM mode.
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
661
671
|
const state = gmState(args);
|
|
662
672
|
if (hasFlag(args, '--json')) {
|
|
663
673
|
console.log(JSON.stringify(state, null, 2));
|
|
@@ -671,6 +681,4 @@ async function gmCommand(...args) {
|
|
|
671
681
|
|
|
672
682
|
module.exports = {
|
|
673
683
|
gmCommand,
|
|
674
|
-
gmState,
|
|
675
|
-
gmWatchSnapshot,
|
|
676
684
|
};
|
package/commands/goal.js
CHANGED