atris 3.68.5 → 3.68.6

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/bin/atris.js CHANGED
@@ -630,6 +630,7 @@ function showHelpAll() {
630
630
  console.log(' rainmaker - Relationship manager dashboard (atrisos-backend/scripts/rainmaker.py)');
631
631
  console.log(' meet - onboard a stranger in one sitting and print their /book link');
632
632
  console.log(' avail - Booking availability (/book/{username} weekly windows)');
633
+ console.log(' scoreboard - Today\'s github contributions vs your best day, streak, repo commits (--watch)');
633
634
  console.log(' brain - Compile MAP/TODO/wiki/state into a loadable agent brain');
634
635
  console.log(' lesson - atris lesson add <slug> <pass|fail> "<text>" (mine/sweep/resolve/revert/ledger; --detector validates the falsifier at write time)');
635
636
  console.log(' taste - Record the operator\'s keep, kill, and more creative verdicts');
@@ -2189,6 +2190,10 @@ if (command === 'guide') {
2189
2190
  .then((code) => process.exit(typeof code === 'number' ? code : 0))
2190
2191
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
2191
2192
  }
2193
+ } else if (command === 'scoreboard') {
2194
+ Promise.resolve(require('../commands/scoreboard').scoreboardCommand(process.argv.slice(3)))
2195
+ .then((code) => process.exit(typeof code === 'number' ? code : 0))
2196
+ .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
2192
2197
  } else if (command === 'meet') {
2193
2198
  Promise.resolve(require('../commands/meet').meetCommand(process.argv.slice(3)))
2194
2199
  .then((code) => process.exit(typeof code === 'number' ? code : 0))
@@ -9937,6 +9937,7 @@ function scrubFeedJargon(text) {
9937
9937
  return String(text || '')
9938
9938
  .replace(/\bmission-\d{4}-\d{2}-\d{2}-[a-z0-9-]+\b/gi, '')
9939
9939
  .replace(/\b[0-9a-f]{7,40}\b/g, '')
9940
+ .replace(/\(\s*(commit\s*)?\)/gi, '')
9940
9941
  .replace(/\s+/g, ' ')
9941
9942
  .trim();
9942
9943
  }
@@ -9966,68 +9967,65 @@ function buildMemberImpactChain({ summary, verified, goalWhy, nextPush, skills =
9966
9967
  };
9967
9968
  }
9968
9969
 
9970
+ // Rows that only look like work: placeholder goals, wake and blocked
9971
+ // notices, empty "logged update" rows, and raw commands.
9972
+ function isFeedFillerText(text) {
9973
+ const t = String(text || '').trim();
9974
+ if (!t) return true;
9975
+ if (/^serve the team as\b/i.test(t)) return true;
9976
+ if (/^member (wake|goal-from-mission|goal created|initialized)\b/i.test(t)) return true;
9977
+ if (/^\[?logged update\]?$/i.test(t)) return true;
9978
+ if (/^(next[_ ]push|verdict):/i.test(t)) return true;
9979
+ if (/^[`'"]?atris\s/i.test(t) || /\batris\s+[a-z-]+\s+[a-z-]+\s+--[a-z]/i.test(t)) return true;
9980
+ return false;
9981
+ }
9982
+
9983
+ // How a row was checked, in words a reader can follow without the command.
9984
+ function plainFeedCheckLabel(state, verifierCommand = '') {
9985
+ if (state === 'failed') return 'check failed, needs another pass';
9986
+ if (state !== 'passed') return 'not checked yet';
9987
+ const cmd = String(verifierCommand || '');
9988
+ if (/\b(npm|pnpm|yarn)\s+(run\s+)?test\b|--test\b|\bpytest\b|\bjest\b|\bvitest\b/i.test(cmd)) return 'checked: tests passed';
9989
+ if (/git\s+diff\s+--check/i.test(cmd)) return 'checked: the change has no whitespace errors';
9990
+ if (/\b(lint|eslint|tsc)\b/i.test(cmd)) return 'checked: lint passed';
9991
+ return 'checked: its check passed';
9992
+ }
9993
+
9994
+ function feedEntryTimeMs(file, header, fallbackMs) {
9995
+ const day = String(file).match(/^(\d{4})-(\d{2})-(\d{2})\.md$/);
9996
+ const clock = String(header).match(/^(\d{2}):(\d{2})\b/);
9997
+ if (!day) return fallbackMs;
9998
+ const ms = new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3]), clock ? Number(clock[1]) : 23, clock ? Number(clock[2]) : 59).getTime();
9999
+ return Number.isFinite(ms) ? Math.min(ms, fallbackMs || ms) : fallbackMs;
10000
+ }
10001
+
10002
+ // The feed reads member log entries that say what changed. Mission records
10003
+ // only hold the assignment, so they are used to name how the work was checked.
9969
10004
  function collectMemberWorkFeed(root = process.cwd(), options = {}) {
9970
10005
  const teamDir = path.join(root, 'atris', 'team');
9971
10006
  const members = findAllMembers(teamDir).filter(isFleetMember);
9972
- const roleByMember = new Map(members.map((m) => [memberSlugFromRecord(m), m.role || 'Team Member']));
9973
- const metaByMember = new Map();
9974
- for (const m of members) {
9975
- const slug = memberSlugFromRecord(m);
9976
- const mDir = path.join(teamDir, slug);
9977
- const skills = listMemberUnlockedSkills(mDir, m.skills);
9978
- let goalWhy = null;
9979
- let goalTitle = null;
9980
- try {
9981
- const gp = memberGoalPlaneStatus(slug, { memberDir: mDir, goalsFile: path.join(mDir, 'goals.json') });
9982
- goalWhy = gp?.goal?.why || null;
9983
- goalTitle = gp?.goal?.title || null;
9984
- } catch {}
9985
- metaByMember.set(slug, { skills, goalWhy, goalTitle });
9986
- }
9987
10007
  const filterMember = String(options.member || '').trim().toLowerCase();
9988
10008
  const limit = Number.isFinite(Number(options.limit)) && Number(options.limit) > 0 ? Number(options.limit) : 15;
9989
10009
 
9990
- const items = [];
9991
- const missionMap = memberRunMissionMap(root);
9992
- for (const mission of missionMap.values()) {
9993
- const owner = String(mission?.owner || '').trim();
9994
- if (!owner) continue;
9995
- if (filterMember && owner.toLowerCase() !== filterMember) continue;
9996
- const atIso = mission.last_tick_at || mission.updated_at || mission.created_at || null;
9997
- const atMs = timestampMs(atIso);
9998
- const summaryText = scrubFeedJargon(mission.last_summary || mission.objective || '');
9999
- if (!summaryText) continue;
10000
- const cleanSummary = compactSentence(summaryText, 140);
10001
- const verified = Boolean(mission?.verifier_result?.passed);
10002
- const meta = metaByMember.get(owner) || { skills: [], goalWhy: null, goalTitle: null };
10003
- const nextPush = scrubFeedJargon(mission.last_next_push || meta.goalTitle || '');
10004
- const impactChain = buildMemberImpactChain({
10005
- summary: cleanSummary,
10006
- verified,
10007
- goalWhy: meta.goalWhy,
10008
- nextPush,
10009
- skills: meta.skills,
10010
- });
10011
- items.push({
10012
- member: owner,
10013
- role: roleByMember.get(owner) || 'Team Member',
10014
- at: atIso,
10015
- at_ms: atMs,
10016
- age: ageLabelFromMs(atMs),
10017
- summary: cleanSummary,
10018
- status: plainFeedStatusLabel(mission),
10019
- verified,
10020
- next_push: impactChain.compounding,
10021
- impact_chain: impactChain,
10022
- });
10010
+ const verifierByObjective = new Map();
10011
+ for (const mission of memberRunMissionMap(root).values()) {
10012
+ const objective = String(mission?.objective || '').trim();
10013
+ if (objective && mission?.verifier) verifierByObjective.set(`${mission.owner}\u0000${objective}`, String(mission.verifier));
10023
10014
  }
10024
10015
 
10016
+ const items = [];
10025
10017
  for (const record of members) {
10026
10018
  const slug = memberSlugFromRecord(record);
10027
10019
  if (filterMember && slug.toLowerCase() !== filterMember) continue;
10028
10020
  const logsDir = path.join(teamDir, slug, 'logs');
10029
10021
  if (!fs.existsSync(logsDir)) continue;
10030
- const logFiles = fs.readdirSync(logsDir).filter((f) => f.endsWith('.md')).sort().slice(-2);
10022
+ const mDir = path.join(teamDir, slug);
10023
+ let goalWhy = null;
10024
+ try {
10025
+ goalWhy = memberGoalPlaneStatus(slug, { memberDir: mDir, goalsFile: path.join(mDir, 'goals.json') })?.goal?.why || null;
10026
+ } catch {}
10027
+ const skills = listMemberUnlockedSkills(mDir, record.skills);
10028
+ const logFiles = fs.readdirSync(logsDir).filter((f) => f.endsWith('.md')).sort().slice(-7);
10031
10029
  for (const file of logFiles) {
10032
10030
  const fullPath = path.join(logsDir, file);
10033
10031
  let raw = '';
@@ -10038,42 +10036,45 @@ function collectMemberWorkFeed(root = process.cwd(), options = {}) {
10038
10036
  } catch {
10039
10037
  continue;
10040
10038
  }
10041
- const sections = raw.split(/^##\s+/m).slice(1);
10042
- for (const section of sections.slice(-3)) {
10039
+ for (const section of raw.split(/^##\s+/m).slice(1)) {
10043
10040
  const lines = section.trim().split(/\r?\n/);
10044
10041
  const header = lines[0] || '';
10045
- const summaryLine = lines.find((l) => /^-\s*(summary|mission|title):/i.test(l));
10046
- const verifierLine = lines.find((l) => /^-\s*verifier:/i.test(l));
10047
- const nextPushLine = lines.find((l) => /^-\s*(next_push|next push|next):/i.test(l));
10048
- const rawSummary = summaryLine
10049
- ? summaryLine.replace(/^-\s*(summary|mission|title):\s*/i, '')
10050
- : header.replace(/^\d{2}:\d{2}\s*·\s*/, '');
10042
+ const title = header.replace(/^\d{2}:\d{2}\s*·\s*/, '');
10043
+ if (isFeedFillerText(title)) continue;
10044
+ const field = (name) => {
10045
+ const line = lines.find((l) => new RegExp(`^-\\s*${name}:`, 'i').test(l));
10046
+ return line ? line.replace(new RegExp(`^-\\s*${name}:\\s*`, 'i'), '').trim() : '';
10047
+ };
10048
+ // No summary means nothing was recorded as changed: skip the row.
10049
+ const rawSummary = field('summary');
10050
+ if (isFeedFillerText(rawSummary)) continue;
10051
10051
  const cleanSummary = compactSentence(scrubFeedJargon(rawSummary), 140);
10052
- if (!cleanSummary) continue;
10053
- const alreadyInItems = items.some((item) => item.member === slug && item.summary === cleanSummary);
10054
- if (alreadyInItems) continue;
10055
- const passed = verifierLine ? /passed/i.test(verifierLine) : false;
10056
- const meta = metaByMember.get(slug) || { skills: [], goalWhy: null, goalTitle: null };
10057
- const rawNextPush = nextPushLine
10058
- ? nextPushLine.replace(/^-\s*(next_push|next push|next):\s*/i, '')
10059
- : (meta.goalTitle || '');
10052
+ if (!cleanSummary || isFeedFillerText(cleanSummary)) continue;
10053
+ if (items.some((item) => item.member === slug && item.summary === cleanSummary)) continue;
10054
+ const verifierState = (field('verifier').match(/^(passed|failed)/i)?.[1] || '').toLowerCase();
10055
+ const passed = verifierState === 'passed';
10056
+ const verifierCommand = verifierByObjective.get(`${slug}\u0000${field('mission')}`) || '';
10057
+ const rawNextPush = field('next_push') || field('next push');
10058
+ const nextPush = isFeedFillerText(rawNextPush) ? '' : compactSentence(scrubFeedJargon(rawNextPush), 110);
10059
+ const atMs = feedEntryTimeMs(file, header, mtimeMs);
10060
10060
  const impactChain = buildMemberImpactChain({
10061
10061
  summary: cleanSummary,
10062
10062
  verified: passed,
10063
- goalWhy: meta.goalWhy,
10064
- nextPush: rawNextPush,
10065
- skills: meta.skills,
10063
+ goalWhy,
10064
+ nextPush,
10065
+ skills,
10066
10066
  });
10067
10067
  items.push({
10068
10068
  member: slug,
10069
10069
  role: record.role || 'Team Member',
10070
- at: new Date(mtimeMs).toISOString(),
10071
- at_ms: mtimeMs,
10072
- age: ageLabelFromMs(mtimeMs),
10070
+ at: new Date(atMs).toISOString(),
10071
+ at_ms: atMs,
10072
+ age: ageLabelFromMs(atMs),
10073
10073
  summary: cleanSummary,
10074
- status: passed ? 'verified and ready' : 'logged update',
10074
+ checked: plainFeedCheckLabel(verifierState, verifierCommand),
10075
+ status: passed ? 'verified and ready' : verifierState === 'failed' ? 'check failed' : 'not checked yet',
10075
10076
  verified: passed,
10076
- next_push: impactChain.compounding,
10077
+ next_push: nextPush || null,
10077
10078
  impact_chain: impactChain,
10078
10079
  });
10079
10080
  }
@@ -10085,6 +10086,14 @@ function collectMemberWorkFeed(root = process.cwd(), options = {}) {
10085
10086
  .slice(0, limit);
10086
10087
  }
10087
10088
 
10089
+ function formatMemberFeedRow(item, indent = '') {
10090
+ const tail = item.next_push ? ` · next: ${item.next_push}` : '';
10091
+ return [
10092
+ `${indent}${item.age.padEnd(4)} · ${item.member}: ${item.summary}`,
10093
+ `${indent} ${item.checked}${tail}`,
10094
+ ];
10095
+ }
10096
+
10088
10097
  function memberFeed(...args) {
10089
10098
  if (hasFlag(args, '--help') || hasFlag(args, '-h')) {
10090
10099
  console.log('Usage: atris member feed [--member <name>] [--limit N] [--json]');
@@ -10096,14 +10105,8 @@ function memberFeed(...args) {
10096
10105
  const limit = readNumberFlag(args, '--limit', 15);
10097
10106
  const feed = collectMemberWorkFeed(process.cwd(), { member: memberFilter, limit });
10098
10107
  const lines = !feed.length
10099
- ? ['No team work recorded yet. Run: atris member dispatch "<idea>"']
10100
- : [
10101
- 'Team work feed',
10102
- ...feed.flatMap((item) => [
10103
- `${item.age.padEnd(4)} · ${item.member} (${item.role}): ${item.summary} [${item.status}]`,
10104
- ` -> ${item.impact_chain.consequence} -> Next push: ${item.impact_chain.compounding}`,
10105
- ]),
10106
- ];
10108
+ ? ['no real work yet']
10109
+ : ['Team work feed', ...feed.flatMap((item) => formatMemberFeedRow(item))];
10107
10110
  printJsonOrText(
10108
10111
  {
10109
10112
  ok: true,
@@ -10828,9 +10831,10 @@ async function memberRoom(...args) {
10828
10831
  if (hasFlag(args, '--walk') || Boolean(readFlag(args, '--decisions', ''))) {
10829
10832
  return memberStandupWalk(room, args);
10830
10833
  }
10831
- const engineSummary = room.engines.length
10832
- ? room.engines.map((e) => `${e.id}(${e.tier})`).join(', ')
10833
- : 'claude, codex';
10834
+ const teamEngines = [...new Set(room.standup.map((s) => s.engine).filter((e) => e && e !== 'auto'))];
10835
+ const engineLine = teamEngines.length
10836
+ ? `Your team runs on ${teamEngines.length > 1 ? `${teamEngines.slice(0, -1).join(', ')} and ${teamEngines[teamEngines.length - 1]}` : teamEngines[0]}.`
10837
+ : 'Your team picks an engine for each job.';
10834
10838
 
10835
10839
  const standupRows = room.standup.flatMap((s, idx) => [
10836
10840
  `+-----------------------------------------------------------------------+`,
@@ -10849,16 +10853,12 @@ async function memberRoom(...args) {
10849
10853
  : [' - Zero blocking 1-on-1s today. Every member has an active roadmap and engine seat.'];
10850
10854
 
10851
10855
  const feedRows = room.feed.length
10852
- ? room.feed.slice(0, 3).flatMap((item) => [
10853
- ` ${item.age.padEnd(4)} · ${item.member}: ${item.summary} [${item.status}]`,
10854
- ` -> ${item.impact_chain.consequence} -> Next push: ${item.impact_chain.compounding}`,
10855
- ])
10856
- : [' No work recorded yet. Run: atris member go'];
10856
+ ? room.feed.slice(0, 3).flatMap((item) => formatMemberFeedRow(item, ' '))
10857
+ : [' no real work yet'];
10857
10858
 
10858
10859
  const lines = [
10859
10860
  'Your team · workday briefing',
10860
- `Engines ready: ${engineSummary}`,
10861
- `Loop: ${room.utopia.loop}`,
10861
+ engineLine,
10862
10862
  '',
10863
10863
  '1. Member-by-Member Standup (detail: `atris member card <name>` | rapid walk: `atris member standup --walk`):',
10864
10864
  ...standupRows,
@@ -10956,6 +10956,8 @@ async function memberCommand(subcommand, ...args) {
10956
10956
  case 'feed':
10957
10957
  case 'stream':
10958
10958
  return memberFeed(...args);
10959
+ case 'board':
10960
+ return require('../lib/member-board').memberBoardCommand(...args);
10959
10961
  case 'card':
10960
10962
  case 'cards':
10961
10963
  return memberCard(args[0], ...args.slice(1));
@@ -11015,6 +11017,7 @@ async function memberCommand(subcommand, ...args) {
11015
11017
  console.log(' 1on1 <name> Sit down for a 1-on-1 to review recent wins and set the roadmap');
11016
11018
  console.log(' sleep [name|--all] Park active member loops so your team rests until woken');
11017
11019
  console.log(' room [--json] Render the visual Team Room and desktop UI payload');
11020
+ console.log(' board [--watch] One screen of every member: job, last step, working/quiet/done');
11018
11021
  console.log(' create <name> Scaffold a new team member (MEMBER.md + dirs) [--push]');
11019
11022
  console.log(' install <slug> [--update] Install missing files, or refresh template skills');
11020
11023
  console.log(' chat <name> "..." Send a message to this member\'s desk in Atris Desktop');
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ // atris scoreboard: today's GitHub contributions as a small game.
4
+ // Data comes from `gh api graphql` (the contribution calendar) and `git log`
5
+ // for this repo. Node built-ins only.
6
+
7
+ const { execFileSync } = require('child_process');
8
+
9
+ const QUERY = 'query { viewer { contributionsCollection { contributionCalendar { weeks { contributionDays { date contributionCount } } } } } }';
10
+ const MEMBER_TRAILER = 'atris-builder[bot]@users.noreply.github.com';
11
+ const BAR_WIDTH = 20;
12
+ const CHEER_STEP = 50;
13
+
14
+ function localDate(d = new Date()) {
15
+ const pad = (n) => String(n).padStart(2, '0');
16
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
17
+ }
18
+
19
+ function fetchDays() {
20
+ const out = execFileSync('gh', ['api', 'graphql', '-f', `query=${QUERY}`], {
21
+ encoding: 'utf8',
22
+ stdio: ['ignore', 'pipe', 'pipe'],
23
+ });
24
+ const weeks = JSON.parse(out).data.viewer.contributionsCollection.contributionCalendar.weeks;
25
+ return weeks.flatMap((w) => w.contributionDays)
26
+ .map((d) => ({ date: d.date, count: d.contributionCount }))
27
+ .sort((a, b) => a.date.localeCompare(b.date));
28
+ }
29
+
30
+ function score(days, today = localDate()) {
31
+ const todayDay = days.find((d) => d.date === today) || { date: today, count: 0 };
32
+ const year = today.slice(0, 4);
33
+ let best = null;
34
+ for (const d of days) {
35
+ if (d.date === today || !d.date.startsWith(year)) continue;
36
+ if (!best || d.count > best.count) best = d;
37
+ }
38
+ // A streak survives a quiet morning: if today is still zero, count from yesterday.
39
+ const past = days.filter((d) => d.date <= today);
40
+ if (past.length && past[past.length - 1].date === today && todayDay.count === 0) past.pop();
41
+ let streak = 0;
42
+ for (let i = past.length - 1; i >= 0 && past[i].count > 0; i--) streak++;
43
+ return { today: todayDay.count, best, streak };
44
+ }
45
+
46
+ function bar(count, target) {
47
+ const filled = Math.max(0, Math.min(BAR_WIDTH, Math.round((count / target) * BAR_WIDTH)));
48
+ return `[${'#'.repeat(filled)}${'-'.repeat(BAR_WIDTH - filled)}]`;
49
+ }
50
+
51
+ function repoCommits(cwd = process.cwd(), now = new Date()) {
52
+ const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate());
53
+ try {
54
+ const out = execFileSync('git', ['log', `--since=${midnight.toISOString()}`, '--format=%B%x1e'], {
55
+ cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
56
+ });
57
+ const bodies = out.split('\x1e').map((b) => b.trim()).filter(Boolean);
58
+ const members = bodies.filter((b) => b.includes(MEMBER_TRAILER)).length;
59
+ return { total: bodies.length, you: bodies.length - members, members };
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ function render(s, commits) {
66
+ const lines = [`today: ${s.today} contribution${s.today === 1 ? '' : 's'}`];
67
+ if (s.best) {
68
+ const target = s.best.count + 1;
69
+ const note = s.today >= target ? 'new best day this year' : `${target - s.today} to beat your best`;
70
+ lines.push(`${bar(s.today, target)} ${note}`);
71
+ lines.push(`best day this year: ${s.best.date}, ${s.best.count}`);
72
+ } else {
73
+ lines.push('best day this year: none yet, today sets it');
74
+ }
75
+ lines.push(`streak: ${s.streak} day${s.streak === 1 ? '' : 's'}`);
76
+ lines.push(commits
77
+ ? `this repo today: ${commits.total} commit${commits.total === 1 ? '' : 's'}, you ${commits.you}, members ${commits.members}`
78
+ : 'this repo today: not a git repo');
79
+ return lines.join('\n');
80
+ }
81
+
82
+ // One cheer line per multiple of 50 crossed between two readings.
83
+ function cheers(prev, next) {
84
+ const out = [];
85
+ for (let m = (Math.floor(prev / CHEER_STEP) + 1) * CHEER_STEP; m <= next; m += CHEER_STEP) {
86
+ out.push(`nice, you passed ${m} today. keep pushing.`);
87
+ }
88
+ return out;
89
+ }
90
+
91
+ function snapshot() {
92
+ const s = score(fetchDays());
93
+ return { s, text: render(s, repoCommits()) };
94
+ }
95
+
96
+ async function scoreboardCommand(args = []) {
97
+ if (args.includes('--help') || args.includes('-h')) {
98
+ console.log('usage: atris scoreboard [--watch]');
99
+ console.log(' today\'s github contributions, your best day this year, your streak, and this repo\'s commits');
100
+ console.log(' --watch refresh every 60 seconds and cheer at every 50');
101
+ return 0;
102
+ }
103
+ let snap;
104
+ try {
105
+ snap = snapshot();
106
+ } catch {
107
+ console.error('could not read your github contributions. check that gh is installed and logged in (gh auth status).');
108
+ return 1;
109
+ }
110
+ console.log(snap.text);
111
+ if (!args.includes('--watch')) return 0;
112
+
113
+ const intervalMs = Number(process.env.ATRIS_SCOREBOARD_INTERVAL_MS) || 60000;
114
+ const maxTicks = Number(process.env.ATRIS_SCOREBOARD_TICKS) || Infinity;
115
+ let last = snap.s.today;
116
+ for (let tick = 0; tick < maxTicks; tick++) {
117
+ await new Promise((r) => setTimeout(r, intervalMs));
118
+ try {
119
+ snap = snapshot();
120
+ } catch {
121
+ console.log('refresh failed, trying again next minute');
122
+ continue;
123
+ }
124
+ console.log('');
125
+ console.log(snap.text);
126
+ for (const line of cheers(last, snap.s.today)) console.log(line);
127
+ last = snap.s.today;
128
+ }
129
+ return 0;
130
+ }
131
+
132
+ module.exports = { scoreboardCommand, score, render, cheers, bar, localDate };
@@ -3,7 +3,7 @@
3
3
  const knownCommands = ['init', 'guide', 'doctor', 'doc-health', 'install', 'workspace', 'log', 'logs', 'later', 'done', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'revisions', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
4
4
  'activate', '_activate', 'agent', 'team', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
5
5
  'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'taste', 'teach', 'plugin', 'experiments', 'bench', 'router', 'tree', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube', 'x-search',
6
- 'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'sync-checkout',
6
+ 'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'scoreboard', 'sync-checkout',
7
7
  'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'api-key', 'agent-client', 'topup', 'buy', 'transactions', 'hire', 'hires', 'mail', 'design', 'mcp', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
8
8
  'fork', 'browse', 'publish', 'pack', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'fleet-report', 'loops', 'self-improve', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines', 'link', 'playbook', 'feed', 'brief', 'social', 'host', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join', 'rsi'];
9
9
 
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ // One screen of every member with a progress log: what job it is on, its last
4
+ // step, how long ago that was, and whether it is working, quiet, or done.
5
+ // Reads the logs lib/member-progress.js writes: "<iso> <mission> <text>".
6
+
7
+ const fs = require('fs');
8
+ const os = require('os');
9
+ const path = require('path');
10
+
11
+ const QUIET_AFTER_MS = 2 * 60 * 1000;
12
+ const TAIL_BYTES = 256 * 1024;
13
+ const JOB_WIDTH = 50;
14
+ const STEP_WIDTH = 50;
15
+
16
+ function progressDir() {
17
+ return process.env.ATRIS_PROGRESS_DIR || path.join(os.homedir(), '.atris', 'progress');
18
+ }
19
+
20
+ function readTail(file) {
21
+ const fd = fs.openSync(file, 'r');
22
+ try {
23
+ const size = fs.fstatSync(fd).size;
24
+ const start = Math.max(0, size - TAIL_BYTES);
25
+ const buf = Buffer.alloc(size - start);
26
+ fs.readSync(fd, buf, 0, buf.length, start);
27
+ const text = buf.toString('utf8');
28
+ // A cut read starts mid-line; drop the partial first line.
29
+ return start > 0 ? text.slice(text.indexOf('\n') + 1) : text;
30
+ } finally {
31
+ fs.closeSync(fd);
32
+ }
33
+ }
34
+
35
+ function parseLine(raw) {
36
+ const m = /^(\S+) (\S+) (.*)$/.exec(raw);
37
+ if (!m) return null;
38
+ const at = Date.parse(m[1]);
39
+ if (Number.isNaN(at)) return null;
40
+ return { at, text: m[3] };
41
+ }
42
+
43
+ function cut(text, width) {
44
+ const s = String(text || '').replace(/\s+/g, ' ').trim();
45
+ return s.length > width ? `${s.slice(0, width - 3)}...` : s;
46
+ }
47
+
48
+ function ago(ms) {
49
+ const s = Math.max(0, Math.floor(ms / 1000));
50
+ if (s < 60) return `${s}s ago`;
51
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
52
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
53
+ return `${Math.floor(s / 86400)}d ago`;
54
+ }
55
+
56
+ // The current run is everything from the latest "started:" line on.
57
+ function memberRow(name, text, now) {
58
+ const lines = text.split('\n').map(parseLine).filter(Boolean);
59
+ if (!lines.length) return null;
60
+ let start = 0;
61
+ for (let i = lines.length - 1; i >= 0; i--) {
62
+ if (lines[i].text.startsWith('started:')) { start = i; break; }
63
+ }
64
+ const run = lines.slice(start);
65
+ const startedLine = run[0].text.startsWith('started:') ? run[0] : null;
66
+ const finished = [...run].reverse().find((l) => l.text.startsWith('finished:'));
67
+ const step = [...run].reverse().find((l) => !l.text.startsWith('started:') && !l.text.startsWith('finished:'));
68
+ const last = run[run.length - 1];
69
+ let state;
70
+ if (finished) state = /^finished:\s*ok\b/.test(finished.text) ? 'done ok' : 'done failed';
71
+ else state = now - last.at < QUIET_AFTER_MS ? 'working' : 'quiet';
72
+ return {
73
+ member: name,
74
+ state,
75
+ job: startedLine ? cut(startedLine.text.slice('started:'.length), JOB_WIDTH) : '',
76
+ step: step ? cut(step.text, STEP_WIDTH) : '',
77
+ ago: ago(now - last.at),
78
+ at: last.at,
79
+ };
80
+ }
81
+
82
+ function readBoard({ dir = progressDir(), now = Date.now() } = {}) {
83
+ let files = [];
84
+ try { files = fs.readdirSync(dir).filter((f) => f.endsWith('.log')); } catch {}
85
+ const rows = [];
86
+ for (const f of files) {
87
+ let text = '';
88
+ try { text = readTail(path.join(dir, f)); } catch { continue; }
89
+ const row = memberRow(f.slice(0, -'.log'.length), text, now);
90
+ if (row) rows.push(row);
91
+ }
92
+ return rows.sort((a, b) => b.at - a.at);
93
+ }
94
+
95
+ function renderBoard(rows, dir = progressDir()) {
96
+ if (!rows.length) return `no member progress yet in ${dir}`;
97
+ const w = (key, min) => Math.max(min, ...rows.map((r) => r[key].length));
98
+ const nameW = w('member', 6);
99
+ const stateW = w('state', 5);
100
+ const agoW = w('ago', 3);
101
+ const jobW = Math.max(3, ...rows.map((r) => (r.job || '-').length));
102
+ const pad = (s, n) => s.padEnd(n);
103
+ const line = (a, b, c, d, e) => `${pad(a, nameW)} ${pad(b, stateW)} ${pad(c, agoW)} ${pad(d, jobW)} ${e}`.trimEnd();
104
+ const out = [line('member', 'state', 'ago', 'job', 'last step')];
105
+ for (const r of rows) out.push(line(r.member, r.state, r.ago, r.job || '-', r.step || '-'));
106
+ return out.join('\n');
107
+ }
108
+
109
+ function memberBoardCommand(...args) {
110
+ const asJson = args.includes('--json');
111
+ if (asJson) {
112
+ console.log(JSON.stringify({ ok: true, dir: progressDir(), members: readBoard() }, null, 2));
113
+ return;
114
+ }
115
+ const draw = () => renderBoard(readBoard());
116
+ if (!args.includes('--watch')) {
117
+ console.log(draw());
118
+ return;
119
+ }
120
+ const redraw = () => process.stdout.write(`\x1b[2J\x1b[H${draw()}\n\nredraws every 3s, ctrl-c to stop\n`);
121
+ redraw();
122
+ // The member route exits once this settles, so only settle on ctrl-c.
123
+ return new Promise((resolve) => {
124
+ const timer = setInterval(redraw, 3000);
125
+ process.once('SIGINT', () => { clearInterval(timer); resolve(); });
126
+ });
127
+ }
128
+
129
+ module.exports = { readBoard, renderBoard, memberRow, ago, memberBoardCommand };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.68.5",
3
+ "version": "3.68.6",
4
4
  "description": "you say what you want in plain words. atris builds it, checks it, and shows you proof.",
5
5
  "main": "bin/atris.js",
6
6
  "bin": {