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/gm.js
CHANGED
|
@@ -6,6 +6,8 @@ const path = require('path');
|
|
|
6
6
|
|
|
7
7
|
const AGENTXP_LEADERBOARD_URL = 'https://api.atris.ai/api/agentxp/leaderboard';
|
|
8
8
|
const AGENTXP_GLOBAL_SYNC_RULE = 'Run atris login, then sync. Owner-provided sync tokens are guided-demo fallback only.';
|
|
9
|
+
const GM_WATCH_DEFAULT_INTERVAL_SECONDS = 10;
|
|
10
|
+
const GM_WATCH_CONTENT_LIMIT = 180;
|
|
9
11
|
|
|
10
12
|
const ROLE_PLAYERS_TO_IGNORE = new Set([
|
|
11
13
|
'game-manager',
|
|
@@ -21,17 +23,21 @@ const ROLE_PLAYERS_TO_IGNORE = new Set([
|
|
|
21
23
|
function showHelp() {
|
|
22
24
|
console.log('');
|
|
23
25
|
console.log('Usage: atris gm [--manager <id>] [--player <id>] [--workspace <path>] [--json]');
|
|
26
|
+
console.log(' atris gm --watch [--interval 10]');
|
|
24
27
|
console.log('');
|
|
25
28
|
console.log('Description:');
|
|
26
29
|
console.log(' Enter AgentXP General Manager mode for the current Atris workspace.');
|
|
27
30
|
console.log(' Shows local players, active missions, review queue, and the next command');
|
|
28
31
|
console.log(' that moves the same AgentXP game loop forward.');
|
|
32
|
+
console.log(' Watch mode keeps that first board, then appends new mission, review, and feed events.');
|
|
29
33
|
console.log('');
|
|
30
34
|
console.log('Options:');
|
|
31
35
|
console.log(' --manager <id> Manager id. Defaults to game-manager when present.');
|
|
32
36
|
console.log(' --as <id> Alias for --player.');
|
|
33
37
|
console.log(' --player <id> Preferred player when seeding a first local mission.');
|
|
34
38
|
console.log(' --workspace <p> Read missions from another Atris workspace.');
|
|
39
|
+
console.log(' --watch Append new GM events until Ctrl+C.');
|
|
40
|
+
console.log(' --interval <sec> Watch polling interval. Defaults to 10.');
|
|
35
41
|
console.log(' --no-seed Do not create a starter player mission.');
|
|
36
42
|
console.log(' --json Print machine-readable mode state.');
|
|
37
43
|
console.log(' --help, -h Show this help.');
|
|
@@ -294,17 +300,25 @@ function nextCommands({ seeded, reviewQueue, missions, players, manager }) {
|
|
|
294
300
|
return ['atris gm --player <player>'];
|
|
295
301
|
}
|
|
296
302
|
|
|
297
|
-
function
|
|
303
|
+
function readGmTaskRows(args = []) {
|
|
298
304
|
const taskDb = require('../lib/task-db');
|
|
299
305
|
const workspaceArg = flag(args, '--workspace') || flag(args, '--root') || process.cwd();
|
|
300
306
|
const workspaceRoot = taskDb.workspaceRoot(path.resolve(workspaceArg));
|
|
301
307
|
const db = taskDb.open();
|
|
302
|
-
|
|
308
|
+
const tasks = taskDb.withTaskDisplayRefs(taskDb.listTasks(db, {
|
|
303
309
|
workspaceRoot,
|
|
304
310
|
limit: 500,
|
|
305
311
|
}));
|
|
312
|
+
return { taskDb, db, workspaceRoot, tasks };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function gmState(args = [], options = {}) {
|
|
316
|
+
const { taskDb, db, workspaceRoot, tasks: initialTasks } = readGmTaskRows(args);
|
|
317
|
+
let tasks = initialTasks;
|
|
306
318
|
const detected = inferManager(workspaceRoot, args);
|
|
307
|
-
const starter =
|
|
319
|
+
const starter = options.seed === false
|
|
320
|
+
? { tasks, seeded: null }
|
|
321
|
+
: ensureStarterMission(taskDb, db, workspaceRoot, tasks, args);
|
|
308
322
|
tasks = starter.tasks;
|
|
309
323
|
|
|
310
324
|
const missions = activeAgentXpTasks(tasks).map(compactTask);
|
|
@@ -337,6 +351,247 @@ function gmState(args = []) {
|
|
|
337
351
|
};
|
|
338
352
|
}
|
|
339
353
|
|
|
354
|
+
function watchIntervalSeconds(args = []) {
|
|
355
|
+
const raw = flag(args, '--interval');
|
|
356
|
+
const parsed = parseInt(raw || String(GM_WATCH_DEFAULT_INTERVAL_SECONDS), 10);
|
|
357
|
+
return Number.isFinite(parsed) ? Math.max(1, parsed) : GM_WATCH_DEFAULT_INTERVAL_SECONDS;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function watchStamp(value) {
|
|
361
|
+
const date = value ? new Date(value) : new Date();
|
|
362
|
+
const safe = Number.isFinite(date.getTime()) ? date : new Date();
|
|
363
|
+
return safe.toTimeString().slice(0, 5);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function eventMs(value) {
|
|
367
|
+
const ms = value ? Date.parse(value) : NaN;
|
|
368
|
+
return Number.isFinite(ms) ? ms : Date.now();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function watchLine(event) {
|
|
372
|
+
const name = String(event.name || event.kind || 'event').replace(/\s+/g, ' ').trim();
|
|
373
|
+
const content = clip(event.content || '', GM_WATCH_CONTENT_LIMIT);
|
|
374
|
+
return `[${watchStamp(event.at)}] [${event.kind}] ${name}: ${content}`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function missionShortId(mission) {
|
|
378
|
+
const id = String(mission && mission.id || '');
|
|
379
|
+
if (!id) return 'mission';
|
|
380
|
+
return id.length > 20 ? `...${id.slice(-8)}` : id;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function missionFingerprint(mission) {
|
|
384
|
+
return [
|
|
385
|
+
mission && mission.status,
|
|
386
|
+
mission && mission.last_tick_at,
|
|
387
|
+
mission && mission.last_tick_index,
|
|
388
|
+
mission && mission.receipt_path,
|
|
389
|
+
].join('|');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function missionEvent(mission) {
|
|
393
|
+
let heartbeat = '';
|
|
394
|
+
try {
|
|
395
|
+
const { missionHeartbeatLines } = require('./mission');
|
|
396
|
+
heartbeat = missionHeartbeatLines(mission).map(line => line.trim()).join(', ');
|
|
397
|
+
} catch {
|
|
398
|
+
heartbeat = '';
|
|
399
|
+
}
|
|
400
|
+
const title = mission.objective || mission.slug || mission.id || 'mission';
|
|
401
|
+
return {
|
|
402
|
+
id: `mission:${mission.id}`,
|
|
403
|
+
kind: 'mission',
|
|
404
|
+
name: mission.owner || mission.slug || 'mission',
|
|
405
|
+
at: mission.last_tick_at || mission.updated_at || mission.created_at,
|
|
406
|
+
content: [
|
|
407
|
+
missionShortId(mission),
|
|
408
|
+
`[${mission.status || 'unknown'}]`,
|
|
409
|
+
heartbeat || null,
|
|
410
|
+
clip(title, 100),
|
|
411
|
+
].filter(Boolean).join(' - '),
|
|
412
|
+
fingerprint: missionFingerprint(mission),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function taskMissionFingerprint(task) {
|
|
417
|
+
return [
|
|
418
|
+
task && task.status,
|
|
419
|
+
task && task.updated_at,
|
|
420
|
+
task && task.claimed_by,
|
|
421
|
+
task && task.done_at,
|
|
422
|
+
].join('|');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function taskMissionEvent(task) {
|
|
426
|
+
const player = taskAssignee(task) || task.claimed_by || 'unassigned';
|
|
427
|
+
return {
|
|
428
|
+
id: `task:${task.id}`,
|
|
429
|
+
kind: 'mission',
|
|
430
|
+
name: player,
|
|
431
|
+
at: task.updated_at ? new Date(Number(task.updated_at)).toISOString() : null,
|
|
432
|
+
content: `${taskRef(task)} [${task.status || 'unknown'}] ${task.title || 'untitled task'}`,
|
|
433
|
+
fingerprint: taskMissionFingerprint(task),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function reviewEvent(task) {
|
|
438
|
+
const player = task.assigned_to || taskAssignee(task) || task.claimed_by || 'review';
|
|
439
|
+
return {
|
|
440
|
+
id: task.id,
|
|
441
|
+
kind: 'review',
|
|
442
|
+
name: player,
|
|
443
|
+
at: task.updated_at ? new Date(Number(task.updated_at)).toISOString() : null,
|
|
444
|
+
content: `${task.ref || taskRef(task)} ready - ${task.title || 'untitled task'}`,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function feedPostId(post) {
|
|
449
|
+
return String(post.id || post.post_id || `${post.created_at || ''}:${post.author_id || ''}:${post.content || ''}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function credentialsFileExists() {
|
|
453
|
+
return fs.existsSync(path.join(os.homedir(), '.atris', 'credentials.json'));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function fetchFeedEvents(workspaceRoot) {
|
|
457
|
+
if (!credentialsFileExists()) return [];
|
|
458
|
+
let feed;
|
|
459
|
+
let loadCredentials;
|
|
460
|
+
try {
|
|
461
|
+
feed = require('./feed');
|
|
462
|
+
loadCredentials = require('../utils/auth').loadCredentials;
|
|
463
|
+
} catch {
|
|
464
|
+
return [];
|
|
465
|
+
}
|
|
466
|
+
if (
|
|
467
|
+
typeof feed.findBusiness !== 'function'
|
|
468
|
+
|| typeof feed.fetchPosts !== 'function'
|
|
469
|
+
|| typeof feed.authorLabel !== 'function'
|
|
470
|
+
) {
|
|
471
|
+
return [];
|
|
472
|
+
}
|
|
473
|
+
const business = feed.findBusiness(workspaceRoot);
|
|
474
|
+
if (!business) return [];
|
|
475
|
+
const credentials = loadCredentials();
|
|
476
|
+
if (!credentials || !credentials.token) return [];
|
|
477
|
+
const aliases = typeof feed.loadAuthorAliases === 'function' ? feed.loadAuthorAliases(business.root) : {};
|
|
478
|
+
const posts = await feed.fetchPosts(business.businessId, credentials.token, 20);
|
|
479
|
+
return posts.map((post) => {
|
|
480
|
+
const id = feedPostId(post);
|
|
481
|
+
return {
|
|
482
|
+
id,
|
|
483
|
+
kind: 'feed',
|
|
484
|
+
name: feed.authorLabel(post, aliases, credentials.user_id, credentials.email),
|
|
485
|
+
at: post.created_at,
|
|
486
|
+
content: String(post.content || '').replace(/\s+/g, ' ').trim(),
|
|
487
|
+
};
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function collectLocalWatchState(args = []) {
|
|
492
|
+
const { workspaceRoot, tasks } = readGmTaskRows(args);
|
|
493
|
+
const taskMissions = activeAgentXpTasks(tasks);
|
|
494
|
+
const reviewQueue = taskMissions.filter(task => task.status === 'review');
|
|
495
|
+
return { workspaceRoot, taskMissions, reviewQueue };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function listDurableMissions(workspaceRoot) {
|
|
499
|
+
try {
|
|
500
|
+
const { listMissions } = require('./mission');
|
|
501
|
+
if (typeof listMissions !== 'function') return [];
|
|
502
|
+
return listMissions(workspaceRoot);
|
|
503
|
+
} catch {
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function gmWatchSnapshot(args) {
|
|
509
|
+
const local = collectLocalWatchState(args);
|
|
510
|
+
const durableMissions = listDurableMissions(local.workspaceRoot);
|
|
511
|
+
const feedEvents = await fetchFeedEvents(local.workspaceRoot);
|
|
512
|
+
return {
|
|
513
|
+
reviewEvents: local.reviewQueue.map(reviewEvent),
|
|
514
|
+
missionEvents: [
|
|
515
|
+
...local.taskMissions.map(taskMissionEvent),
|
|
516
|
+
...durableMissions.map(missionEvent),
|
|
517
|
+
],
|
|
518
|
+
feedEvents,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function markSnapshotSeen(snapshot, seen) {
|
|
523
|
+
for (const event of snapshot.missionEvents) seen.missions.set(event.id, event.fingerprint);
|
|
524
|
+
for (const event of snapshot.reviewEvents) seen.reviews.add(event.id);
|
|
525
|
+
for (const event of snapshot.feedEvents) seen.feed.add(event.id);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function newWatchEvents(snapshot, seen) {
|
|
529
|
+
const events = [];
|
|
530
|
+
for (const event of snapshot.missionEvents) {
|
|
531
|
+
const previous = seen.missions.get(event.id);
|
|
532
|
+
if (previous !== event.fingerprint) {
|
|
533
|
+
events.push(event);
|
|
534
|
+
seen.missions.set(event.id, event.fingerprint);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
for (const event of snapshot.reviewEvents) {
|
|
538
|
+
if (!seen.reviews.has(event.id)) {
|
|
539
|
+
events.push(event);
|
|
540
|
+
seen.reviews.add(event.id);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
for (const event of snapshot.feedEvents) {
|
|
544
|
+
if (!seen.feed.has(event.id)) {
|
|
545
|
+
events.push(event);
|
|
546
|
+
seen.feed.add(event.id);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return events.sort((a, b) => eventMs(a.at) - eventMs(b.at));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function gmWatch(args = []) {
|
|
553
|
+
const intervalSeconds = watchIntervalSeconds(args);
|
|
554
|
+
const seen = {
|
|
555
|
+
missions: new Map(),
|
|
556
|
+
reviews: new Set(),
|
|
557
|
+
feed: new Set(),
|
|
558
|
+
};
|
|
559
|
+
try {
|
|
560
|
+
markSnapshotSeen(await gmWatchSnapshot(args), seen);
|
|
561
|
+
} catch {
|
|
562
|
+
// Watch mode is append-only; transient read failures should not redraw or exit.
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
console.log('');
|
|
566
|
+
console.log(`Watching GM events every ${intervalSeconds}s. Ctrl+C to stop.`);
|
|
567
|
+
console.log('');
|
|
568
|
+
|
|
569
|
+
let polling = false;
|
|
570
|
+
const poll = async () => {
|
|
571
|
+
if (polling) return;
|
|
572
|
+
polling = true;
|
|
573
|
+
try {
|
|
574
|
+
const events = newWatchEvents(await gmWatchSnapshot(args, seen), seen);
|
|
575
|
+
for (const event of events) console.log(watchLine(event));
|
|
576
|
+
} catch {
|
|
577
|
+
// Match fleet watch: quiet on transient poll failures.
|
|
578
|
+
} finally {
|
|
579
|
+
polling = false;
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const interval = setInterval(poll, intervalSeconds * 1000);
|
|
584
|
+
return new Promise((resolve) => {
|
|
585
|
+
const onSigint = () => {
|
|
586
|
+
clearInterval(interval);
|
|
587
|
+
process.off('SIGINT', onSigint);
|
|
588
|
+
console.log('\nStopped watching.\n');
|
|
589
|
+
resolve();
|
|
590
|
+
};
|
|
591
|
+
process.on('SIGINT', onSigint);
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
340
595
|
function render(state) {
|
|
341
596
|
console.log('');
|
|
342
597
|
console.log('AgentXP General Manager');
|
|
@@ -374,21 +629,48 @@ function render(state) {
|
|
|
374
629
|
for (const command of state.next_commands) console.log(`- ${command}`);
|
|
375
630
|
}
|
|
376
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
|
+
|
|
377
643
|
async function gmCommand(...args) {
|
|
378
644
|
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
379
645
|
showHelp();
|
|
380
646
|
return;
|
|
381
647
|
}
|
|
382
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
|
+
|
|
383
661
|
const state = gmState(args);
|
|
384
662
|
if (hasFlag(args, '--json')) {
|
|
385
663
|
console.log(JSON.stringify(state, null, 2));
|
|
386
664
|
return;
|
|
387
665
|
}
|
|
388
666
|
render(state);
|
|
667
|
+
if (hasFlag(args, '--watch')) {
|
|
668
|
+
await gmWatch(args);
|
|
669
|
+
}
|
|
389
670
|
}
|
|
390
671
|
|
|
391
672
|
module.exports = {
|
|
392
673
|
gmCommand,
|
|
393
674
|
gmState,
|
|
675
|
+
gmWatchSnapshot,
|
|
394
676
|
};
|
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
|
+
};
|