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 +1 -1
- package/commands/member.js +25 -0
- package/commands/scoreboard.js +24 -5
- package/lib/member-board.js +142 -8
- package/lib/member-progress.js +16 -4
- package/package.json +1 -1
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');
|
package/commands/member.js
CHANGED
|
@@ -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)
|
package/commands/scoreboard.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 };
|
package/lib/member-board.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 = () =>
|
|
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 };
|
package/lib/member-progress.js
CHANGED
|
@@ -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
|
-
|
|
79
|
-
|
|
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 };
|