atris 3.68.6 → 3.68.7

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,7 +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
+ console.log(' scoreboard - Today\'s github contributions vs your best day, streak, repo commits (--watch, --json)');
634
634
  console.log(' brain - Compile MAP/TODO/wiki/state into a loadable agent brain');
635
635
  console.log(' lesson - atris lesson add <slug> <pass|fail> "<text>" (mine/sweep/resolve/revert/ledger; --detector validates the falsifier at write time)');
636
636
  console.log(' taste - Record the operator\'s keep, kill, and more creative verdicts');
@@ -1300,6 +1300,13 @@ function memberRunWith(name, args, { quiet = false } = {}) {
1300
1300
  if (nodeModulesLinked) console.log(`linked node_modules from ${nodeModulesSource}`);
1301
1301
  }
1302
1302
 
1303
+ // An ignited run works for minutes; say where to watch it before it starts.
1304
+ if (ignite && !asJson && !quiet && !hasFlag(args, '--quiet')) {
1305
+ const progressDir = process.env.ATRIS_PROGRESS_DIR || '~/.atris/progress';
1306
+ console.log(`watch: tail -f ${progressDir}/${String(name).replace(/[^A-Za-z0-9._-]/g, '_')}.log`);
1307
+ console.log('board: atris member board --watch');
1308
+ }
1309
+
1303
1310
  const cwd = mission.worktree?.path || process.cwd();
1304
1311
  const verifier = readFlag(args, '--verify', '') || mission.verifier || requestedVerifier;
1305
1312
  if (verifier && verifier !== mission.verifier) {
@@ -9789,6 +9796,17 @@ const IDEA_ARCHETYPE_RULES = [
9789
9796
  },
9790
9797
  ];
9791
9798
 
9799
+ // Dispatch defaults to the core team. Other members (experiments, one-off
9800
+ // workers) win only when the idea names them or out-scores the core by 2x.
9801
+ const CORE_DISPATCH_MEMBERS = new Set([
9802
+ 'executor', 'architect', 'auto-improver', 'navigator', 'validator', 'customer-lead', 'researcher',
9803
+ ]);
9804
+
9805
+ function ideaNamesMember(text, slug) {
9806
+ const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/-/g, '[-\\s]+');
9807
+ return new RegExp(`(^|[^a-z0-9-])${escaped}($|[^a-z0-9-])`, 'i').test(text);
9808
+ }
9809
+
9792
9810
  function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '') {
9793
9811
  const teamDir = path.join(root, 'atris', 'team');
9794
9812
  const members = findAllMembers(teamDir).filter(isFleetMember);
@@ -9841,6 +9859,13 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
9841
9859
  if (slug === 'executor') score += 3;
9842
9860
  if (slug === 'auto-improver') score += 2;
9843
9861
 
9862
+ if (ideaNamesMember(text, slug)) {
9863
+ score += 40;
9864
+ matchedReason = 'named in the idea';
9865
+ } else if (!CORE_DISPATCH_MEMBERS.has(slug)) {
9866
+ score = Math.floor(score / 2);
9867
+ }
9868
+
9844
9869
  if (!matchedReason) {
9845
9870
  matchedReason = record.description
9846
9871
  ? compactSentence(record.description, 90)
@@ -48,13 +48,21 @@ function bar(count, target) {
48
48
  return `[${'#'.repeat(filled)}${'-'.repeat(BAR_WIDTH - filled)}]`;
49
49
  }
50
50
 
51
+ // Today's commits on every ref (all local and remote branches, the checkout,
52
+ // tags), so an old branch checked out still sees today's work. Stash is left
53
+ // out. Each commit counts once, keyed by hash.
51
54
  function repoCommits(cwd = process.cwd(), now = new Date()) {
52
55
  const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate());
53
56
  try {
54
- const out = execFileSync('git', ['log', `--since=${midnight.toISOString()}`, '--format=%B%x1e'], {
57
+ const out = execFileSync('git', ['log', '--exclude=refs/stash', '--all', `--since=${midnight.toISOString()}`, '--format=%H%x1f%B%x1e'], {
55
58
  cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
56
59
  });
57
- const bodies = out.split('\x1e').map((b) => b.trim()).filter(Boolean);
60
+ const byHash = new Map();
61
+ for (const rec of out.split('\x1e')) {
62
+ const [hash, body = ''] = rec.trim().split('\x1f');
63
+ if (hash) byHash.set(hash, body);
64
+ }
65
+ const bodies = [...byHash.values()];
58
66
  const members = bodies.filter((b) => b.includes(MEMBER_TRAILER)).length;
59
67
  return { total: bodies.length, you: bodies.length - members, members };
60
68
  } catch {
@@ -90,14 +98,21 @@ function cheers(prev, next) {
90
98
 
91
99
  function snapshot() {
92
100
  const s = score(fetchDays());
93
- return { s, text: render(s, repoCommits()) };
101
+ const commits = repoCommits();
102
+ return { s, commits, text: render(s, commits) };
103
+ }
104
+
105
+ // Same numbers as the board, as one object for scripts.
106
+ function toJson(s, commits, date = localDate()) {
107
+ return { date, today: s.today, best: s.best, streak: s.streak, repo: commits };
94
108
  }
95
109
 
96
110
  async function scoreboardCommand(args = []) {
97
111
  if (args.includes('--help') || args.includes('-h')) {
98
- console.log('usage: atris scoreboard [--watch]');
112
+ console.log('usage: atris scoreboard [--watch | --json]');
99
113
  console.log(' today\'s github contributions, your best day this year, your streak, and this repo\'s commits');
100
114
  console.log(' --watch refresh every 60 seconds and cheer at every 50');
115
+ console.log(' --json print the same numbers once as one json object');
101
116
  return 0;
102
117
  }
103
118
  let snap;
@@ -107,6 +122,10 @@ async function scoreboardCommand(args = []) {
107
122
  console.error('could not read your github contributions. check that gh is installed and logged in (gh auth status).');
108
123
  return 1;
109
124
  }
125
+ if (args.includes('--json')) {
126
+ console.log(JSON.stringify(toJson(snap.s, snap.commits), null, 2));
127
+ return 0;
128
+ }
110
129
  console.log(snap.text);
111
130
  if (!args.includes('--watch')) return 0;
112
131
 
@@ -129,4 +148,4 @@ async function scoreboardCommand(args = []) {
129
148
  return 0;
130
149
  }
131
150
 
132
- module.exports = { scoreboardCommand, score, render, cheers, bar, localDate };
151
+ module.exports = { scoreboardCommand, score, render, toJson, cheers, bar, localDate };
@@ -9,6 +9,7 @@ const os = require('os');
9
9
  const path = require('path');
10
10
 
11
11
  const QUIET_AFTER_MS = 2 * 60 * 1000;
12
+ const HIDE_DONE_AFTER_MS = 60 * 60 * 1000;
12
13
  const TAIL_BYTES = 256 * 1024;
13
14
  const JOB_WIDTH = 50;
14
15
  const STEP_WIDTH = 50;
@@ -53,7 +54,13 @@ function ago(ms) {
53
54
  return `${Math.floor(s / 86400)}d ago`;
54
55
  }
55
56
 
57
+ function escapeRe(s) {
58
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
59
+ }
60
+
56
61
  // The current run is everything from the latest "started:" line on.
62
+ // Steps that only write the member's own log are housekeeping, so the last
63
+ // step column skips them unless nothing else happened.
57
64
  function memberRow(name, text, now) {
58
65
  const lines = text.split('\n').map(parseLine).filter(Boolean);
59
66
  if (!lines.length) return null;
@@ -64,7 +71,10 @@ function memberRow(name, text, now) {
64
71
  const run = lines.slice(start);
65
72
  const startedLine = run[0].text.startsWith('started:') ? run[0] : null;
66
73
  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:'));
74
+ const ownLog = new RegExp(`atris/team/${escapeRe(name)}/logs\\b`);
75
+ const steps = run.filter((l) => !l.text.startsWith('started:') && !l.text.startsWith('finished:'));
76
+ const real = steps.filter((l) => !ownLog.test(l.text));
77
+ const step = real[real.length - 1] || steps[steps.length - 1];
68
78
  const last = run[run.length - 1];
69
79
  let state;
70
80
  if (finished) state = /^finished:\s*ok\b/.test(finished.text) ? 'done ok' : 'done failed';
@@ -76,10 +86,12 @@ function memberRow(name, text, now) {
76
86
  step: step ? cut(step.text, STEP_WIDTH) : '',
77
87
  ago: ago(now - last.at),
78
88
  at: last.at,
89
+ // Finished more than an hour ago: off the board unless --all.
90
+ idle: last.text.startsWith('finished:') && now - last.at >= HIDE_DONE_AFTER_MS,
79
91
  };
80
92
  }
81
93
 
82
- function readBoard({ dir = progressDir(), now = Date.now() } = {}) {
94
+ function readBoard({ dir = progressDir(), now = Date.now(), all = false } = {}) {
83
95
  let files = [];
84
96
  try { files = fs.readdirSync(dir).filter((f) => f.endsWith('.log')); } catch {}
85
97
  const rows = [];
@@ -89,11 +101,12 @@ function readBoard({ dir = progressDir(), now = Date.now() } = {}) {
89
101
  const row = memberRow(f.slice(0, -'.log'.length), text, now);
90
102
  if (row) rows.push(row);
91
103
  }
92
- return rows.sort((a, b) => b.at - a.at);
104
+ return rows.filter((r) => all || !r.idle).sort((a, b) => b.at - a.at);
93
105
  }
94
106
 
95
- function renderBoard(rows, dir = progressDir()) {
96
- if (!rows.length) return `no member progress yet in ${dir}`;
107
+ function renderBoard(rows, dir = progressDir(), hidden = 0) {
108
+ const note = hidden ? `${hidden} finished over an hour ago, hidden. --all shows them` : '';
109
+ if (!rows.length) return note ? `nobody is working right now. ${note}` : `no member progress yet in ${dir}`;
97
110
  const w = (key, min) => Math.max(min, ...rows.map((r) => r[key].length));
98
111
  const nameW = w('member', 6);
99
112
  const stateW = w('state', 5);
@@ -103,16 +116,137 @@ function renderBoard(rows, dir = progressDir()) {
103
116
  const line = (a, b, c, d, e) => `${pad(a, nameW)} ${pad(b, stateW)} ${pad(c, agoW)} ${pad(d, jobW)} ${e}`.trimEnd();
104
117
  const out = [line('member', 'state', 'ago', 'job', 'last step')];
105
118
  for (const r of rows) out.push(line(r.member, r.state, r.ago, r.job || '-', r.step || '-'));
119
+ if (note) out.push('', note);
106
120
  return out.join('\n');
107
121
  }
108
122
 
123
+ // Live stream format for `atris member board --json --follow`, one JSON
124
+ // object per line on stdout, forever:
125
+ // first line: {"kind":"snapshot","at":"<iso>","dir":"<progress dir>","members":[<same rows as --json>]}
126
+ // then one line per new progress line, in the order it was read:
127
+ // {"member":"builder","mission":"m-1","at":"<iso>","kind":"started","text":"<job>"}
128
+ // {"member":"builder","mission":"m-1","at":"<iso>","kind":"step","text":"<step>"}
129
+ // {"member":"builder","mission":"m-1","at":"<iso>","kind":"finished","text":"ok","ok":true}
130
+ // "ok" appears only on finished. Lines that are not "<iso> <mission> <text>"
131
+ // are skipped, and a line is only read once its newline has been written.
132
+ // Stops on ctrl-c, or on the next write after the reader closes the pipe;
133
+ // an app that is done should kill the process.
134
+ function progressEvent(member, raw) {
135
+ const m = /^(\S+) (\S+) (.*)$/.exec(raw);
136
+ if (!m) return null;
137
+ const at = Date.parse(m[1]);
138
+ if (Number.isNaN(at)) return null;
139
+ const event = { member, mission: m[2], at: new Date(at).toISOString(), kind: 'step', text: m[3] };
140
+ const tagged = /^(started|finished):\s*(.*)$/.exec(m[3]);
141
+ if (tagged) {
142
+ event.kind = tagged[1];
143
+ event.text = tagged[2];
144
+ if (tagged[1] === 'finished') event.ok = /^ok\b/.test(tagged[2]);
145
+ }
146
+ return event;
147
+ }
148
+
149
+ // Tails every <member>.log in dir and calls emit(event) for each new full
150
+ // line. Files present at start are read from just after their last newline,
151
+ // so a line half written before the stream started still arrives once done;
152
+ // files that appear later are read from the top. scan() is sync, so watch
153
+ // and poll can both call it without racing.
154
+ function afterLastNewline(file) {
155
+ let fd;
156
+ try { fd = fs.openSync(file, 'r'); } catch { return 0; }
157
+ try {
158
+ const buf = Buffer.alloc(64 * 1024);
159
+ for (let end = fs.fstatSync(fd).size; end > 0;) {
160
+ const start = Math.max(0, end - buf.length);
161
+ const n = fs.readSync(fd, buf, 0, end - start, start);
162
+ const i = buf.subarray(0, n).lastIndexOf(0x0a);
163
+ if (i >= 0) return start + i + 1;
164
+ end = start;
165
+ }
166
+ return 0;
167
+ } catch { return 0; } finally { fs.closeSync(fd); }
168
+ }
169
+
170
+ function followBoard({ dir = progressDir(), emit, pollMs = 1000 } = {}) {
171
+ const offsets = new Map();
172
+ const sizeOf = (file) => { try { return fs.statSync(file).size; } catch { return null; } };
173
+ const list = () => { try { return fs.readdirSync(dir).filter((f) => f.endsWith('.log')); } catch { return []; } };
174
+ for (const f of list()) offsets.set(f, afterLastNewline(path.join(dir, f)));
175
+
176
+ function readNew(f) {
177
+ const file = path.join(dir, f);
178
+ const size = sizeOf(file);
179
+ if (size === null) return;
180
+ let from = offsets.get(f) || 0;
181
+ if (size < from) from = 0; // truncated or replaced: start over
182
+ if (size === from) { offsets.set(f, from); return; }
183
+ let buf;
184
+ try {
185
+ const fd = fs.openSync(file, 'r');
186
+ try {
187
+ buf = Buffer.alloc(size - from);
188
+ buf = buf.subarray(0, fs.readSync(fd, buf, 0, buf.length, from));
189
+ } finally { fs.closeSync(fd); }
190
+ } catch { return; }
191
+ // Stop at the last newline; a half-written tail waits for the next scan.
192
+ const end = buf.lastIndexOf(0x0a);
193
+ if (end < 0) { offsets.set(f, from); return; }
194
+ offsets.set(f, from + end + 1);
195
+ const member = f.slice(0, -'.log'.length);
196
+ for (const raw of buf.subarray(0, end).toString('utf8').split('\n')) {
197
+ const event = progressEvent(member, raw.replace(/\r$/, ''));
198
+ if (event) emit(event);
199
+ }
200
+ }
201
+
202
+ let watcher = null;
203
+ let stopped = false;
204
+ function scan() {
205
+ if (stopped) return;
206
+ if (!watcher) {
207
+ try {
208
+ watcher = fs.watch(dir, () => scan());
209
+ watcher.on('error', () => { try { watcher.close(); } catch {} watcher = null; });
210
+ } catch { watcher = null; }
211
+ }
212
+ for (const f of list()) readNew(f);
213
+ }
214
+ const timer = setInterval(scan, pollMs);
215
+ scan();
216
+ return {
217
+ scan,
218
+ stop() {
219
+ stopped = true;
220
+ clearInterval(timer);
221
+ if (watcher) { try { watcher.close(); } catch {} }
222
+ },
223
+ };
224
+ }
225
+
109
226
  function memberBoardCommand(...args) {
110
227
  const asJson = args.includes('--json');
228
+ const all = args.includes('--all');
229
+ if (args.includes('--follow')) {
230
+ const dir = progressDir();
231
+ const write = (obj) => process.stdout.write(`${JSON.stringify(obj)}\n`);
232
+ write({ kind: 'snapshot', at: new Date().toISOString(), dir, members: readBoard({ dir, all }) });
233
+ const follower = followBoard({ dir, emit: write });
234
+ // Runs until ctrl-c or the reader closes the pipe.
235
+ return new Promise((resolve) => {
236
+ const done = () => { follower.stop(); resolve(); };
237
+ process.once('SIGINT', done);
238
+ process.stdout.once('error', done);
239
+ });
240
+ }
111
241
  if (asJson) {
112
- console.log(JSON.stringify({ ok: true, dir: progressDir(), members: readBoard() }, null, 2));
242
+ console.log(JSON.stringify({ ok: true, dir: progressDir(), members: readBoard({ all }) }, null, 2));
113
243
  return;
114
244
  }
115
- const draw = () => renderBoard(readBoard());
245
+ const draw = () => {
246
+ const every = readBoard({ all: true });
247
+ const rows = all ? every : every.filter((r) => !r.idle);
248
+ return renderBoard(rows, progressDir(), every.length - rows.length);
249
+ };
116
250
  if (!args.includes('--watch')) {
117
251
  console.log(draw());
118
252
  return;
@@ -126,4 +260,4 @@ function memberBoardCommand(...args) {
126
260
  });
127
261
  }
128
262
 
129
- module.exports = { readBoard, renderBoard, memberRow, ago, memberBoardCommand };
263
+ module.exports = { readBoard, renderBoard, memberRow, ago, progressEvent, followBoard, memberBoardCommand };
@@ -62,21 +62,33 @@ function hhmm(date = new Date()) {
62
62
  return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
63
63
  }
64
64
 
65
+ // One progress line stays one short line: a 'started:' objective keeps its
66
+ // first line and at most 80 chars; any other line caps at 100.
67
+ function shortenLine(line) {
68
+ const first = String(line || '').split('\n')[0];
69
+ if (first.startsWith('started: ')) {
70
+ const objective = first.slice('started: '.length);
71
+ return `started: ${objective.length > 80 ? `${objective.slice(0, 80)}...` : objective}`;
72
+ }
73
+ return first.length > 100 ? `${first.slice(0, 100)}...` : first;
74
+ }
75
+
65
76
  function appendProgress(member, line, missionId) {
66
77
  // Test runs never write into the real home folder unless they name a dir.
67
78
  if (!process.env.ATRIS_PROGRESS_DIR && process.env.NODE_TEST_CONTEXT) return;
68
79
  try {
69
80
  const dir = progressDir();
70
81
  fs.mkdirSync(dir, { recursive: true });
71
- const entry = `${new Date().toISOString()} ${missionId || '-'} ${line}\n`;
82
+ const entry = `${new Date().toISOString()} ${missionId || '-'} ${shortenLine(line)}\n`;
72
83
  fs.appendFileSync(path.join(dir, `${safeMember(member)}.log`), entry, 'utf8');
73
84
  } catch {}
74
85
  }
75
86
 
76
87
  // Print one step for the operator and keep it in the member's progress log.
77
88
  function reportProgress(member, line, missionId) {
78
- try { process.stderr.write(` ${member || 'mission'} ${hhmm()} ${line}\n`); } catch {}
79
- appendProgress(member, line, missionId);
89
+ const short = shortenLine(line);
90
+ try { process.stderr.write(` ${member || 'mission'} ${hhmm()} ${short}\n`); } catch {}
91
+ appendProgress(member, short, missionId);
80
92
  }
81
93
 
82
- module.exports = { formatToolStep, appendProgress, reportProgress };
94
+ module.exports = { formatToolStep, appendProgress, reportProgress, shortenLine };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atris",
3
- "version": "3.68.6",
3
+ "version": "3.68.7",
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": {