atris 3.78.0 → 3.80.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.
@@ -15,637 +15,23 @@ const { hasFlag, readFlag, readNumberFlag } = require('../lib/arg-parser');
15
15
  const { ensureMemberBundle, memberBundlePresent, updateMemberSkills } = require('../lib/member-scaffold');
16
16
  const { memberProcessPrompt, MEMBER_PROCESS_PATH } = require('../lib/member-context');
17
17
 
18
- function findWorkspaceBusinessId(startDir = process.cwd()) {
19
- let dir = path.resolve(startDir);
20
- while (true) {
21
- const file = path.join(dir, '.atris', 'business.json');
22
- if (fs.existsSync(file)) {
23
- try {
24
- return JSON.parse(fs.readFileSync(file, 'utf8')).business_id || null;
25
- } catch (error) {
26
- console.warn(`Warning: could not parse ${file}: ${error.message}`);
27
- return null;
28
- }
29
- }
30
- const parent = path.dirname(dir);
31
- if (parent === dir) return null;
32
- dir = parent;
33
- }
34
- }
35
-
36
- function todayLogName() {
37
- const now = new Date();
38
- return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}.md`;
39
- }
40
-
41
- function ensureMemberLog(memberDir, { name, role, description, source = 'cli' } = {}) {
42
- const logsDir = path.join(memberDir, 'logs');
43
- fs.mkdirSync(logsDir, { recursive: true });
44
- const logPath = path.join(logsDir, todayLogName());
45
- if (fs.existsSync(logPath)) return logPath;
46
- const stamp = new Date().toTimeString().slice(0, 5);
47
- const content = [
48
- `## ${stamp} · Member initialized`,
49
- `- team: ${name || path.basename(memberDir)}`,
50
- role ? `- role: ${role}` : '',
51
- description ? `- mission: ${description}` : '',
52
- `- source: ${source}`,
53
- '- status: ready_for_room',
54
- ].filter(Boolean).join('\n') + '\n';
55
- fs.writeFileSync(logPath, content, 'utf8');
56
- return logPath;
57
- }
58
-
59
- function appendMemberLifecycleLog(memberDir, name, action, detail = '') {
60
- const logsDir = path.join(memberDir, 'logs');
61
- fs.mkdirSync(logsDir, { recursive: true });
62
- const logPath = path.join(logsDir, todayLogName());
63
- const stamp = new Date().toTimeString().slice(0, 5);
64
- const content = [
65
- `## ${stamp} · Member ${action}`,
66
- `- team: ${name || path.basename(memberDir)}`,
67
- detail ? `- detail: ${detail}` : '',
68
- `- status: ${action}`,
69
- '',
70
- ].filter(Boolean).join('\n');
71
- fs.appendFileSync(logPath, content, 'utf8');
72
- return logPath;
73
- }
74
-
75
- function archiveName(name) {
76
- return `${name}-${todayLogName().replace(/\.md$/, '')}`;
77
- }
78
-
79
- function uniqueArchiveDir(archiveRoot, name) {
80
- const base = archiveName(name);
81
- let candidate = path.join(archiveRoot, base);
82
- let i = 2;
83
- while (fs.existsSync(candidate)) {
84
- candidate = path.join(archiveRoot, `${base}-${i}`);
85
- i += 1;
86
- }
87
- return candidate;
88
- }
89
-
90
- function parseDaysFlag(flags, fallback = 60) {
91
- const joined = flags.join(' ');
92
- const match = joined.match(/--days[=\s]+(\d+)/);
93
- const days = match ? Number(match[1]) : fallback;
94
- return Number.isFinite(days) && days >= 0 ? days : fallback;
95
- }
96
-
97
- function parseConfirmFlag(flags) {
98
- const joined = flags.join(' ');
99
- return joined.match(/--confirm[=\s]+["']?([^"']+)["']?/)?.[1] || '';
100
- }
101
-
102
- function readRepeatedFlag(args, name) {
103
- const values = [];
104
- const prefix = `${name}=`;
105
- for (let i = 0; i < args.length; i += 1) {
106
- const arg = String(args[i]);
107
- if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) {
108
- values.push(String(args[i + 1]).replace(/^["']|["']$/g, ''));
109
- i += 1;
110
- continue;
111
- }
112
- if (arg.startsWith(prefix)) values.push(arg.slice(prefix.length).replace(/^["']|["']$/g, ''));
113
- }
114
- return values.filter(Boolean);
115
- }
116
-
117
- function stripKnownFlags(args, valueNames, booleanNames = []) {
118
- const out = [];
119
- const valueSet = new Set(valueNames);
120
- const booleanSet = new Set(['--full', '--verbose', ...booleanNames]);
121
- for (let i = 0; i < args.length; i += 1) {
122
- const arg = String(args[i]);
123
- const key = arg.includes('=') ? arg.slice(0, arg.indexOf('=')) : arg;
124
- if (booleanSet.has(key)) continue;
125
- if (valueSet.has(key)) {
126
- if (!arg.includes('=') && args[i + 1] && !String(args[i + 1]).startsWith('--')) i += 1;
127
- continue;
128
- }
129
- out.push(args[i]);
130
- }
131
- return out;
132
- }
133
-
134
- function stampIso() {
135
- return new Date().toISOString();
136
- }
137
-
138
- function fileSafeStamp() {
139
- return stampIso().replace(/[:.]/g, '-');
140
- }
141
-
142
- function slugHash(value) {
143
- return crypto.createHash('sha1').update(String(value || '')).digest('hex').slice(0, 8);
144
- }
145
-
146
- function makeGoalId(title) {
147
- return `goal-${todayLogName().replace(/\.md$/, '')}-${slugHash(title)}`;
148
- }
149
-
150
- function uniqueGoalId(state, baseId) {
151
- const existingIds = new Set((state.goals || []).map((goal) => goal.id).filter(Boolean));
152
- if (!existingIds.has(baseId)) return baseId;
153
- let index = 2;
154
- let id = `${baseId}-${index}`;
155
- while (existingIds.has(id)) {
156
- index += 1;
157
- id = `${baseId}-${index}`;
158
- }
159
- return id;
160
- }
161
-
162
- function makeExperimentId(goalId, title) {
163
- return `exp-${slugHash(`${goalId}:${title}:${Date.now()}`)}`;
164
- }
165
-
166
- const MEMBER_RUNTIME_ALIASES = Object.freeze({
167
- 'problem-finder': 'signal-scout',
168
- 'info-organizer': 'wiki-miner',
169
- coordinator: 'supervisor',
170
- 'task-planner': 'objective-generator',
171
- improver: 'architect',
172
- 'problem-solver': 'generalist',
173
- });
174
-
175
- function memberRuntimeKind(name) {
176
- const key = lowerCompact(name);
177
- return MEMBER_RUNTIME_ALIASES[key] || key;
178
- }
179
-
180
- function resolveMemberRuntime(name) {
181
- const requestedName = String(name || '').trim();
182
- const requestedPaths = memberPaths(requestedName);
183
- const runtimeKind = memberRuntimeKind(requestedName);
184
- const aliasOf = runtimeKind !== lowerCompact(requestedName) ? runtimeKind : null;
185
- if (requestedName && fs.existsSync(requestedPaths.memberFile)) {
186
- return {
187
- requestedName,
188
- storageName: requestedName,
189
- runtimeKind,
190
- aliasOf,
191
- paths: requestedPaths,
192
- };
193
- }
194
- if (aliasOf) {
195
- const legacyPaths = memberPaths(runtimeKind);
196
- if (fs.existsSync(legacyPaths.memberFile)) {
197
- return {
198
- requestedName,
199
- storageName: runtimeKind,
200
- runtimeKind,
201
- aliasOf,
202
- paths: legacyPaths,
203
- };
204
- }
205
- }
206
- return {
207
- requestedName,
208
- storageName: requestedName,
209
- runtimeKind,
210
- aliasOf,
211
- paths: requestedPaths,
212
- };
213
- }
214
-
215
- function memberPaths(name) {
216
- const teamDir = path.join(process.cwd(), 'atris', 'team');
217
- const memberDir = path.join(teamDir, name || '');
218
- // Steering signals live in the workspace's single .atris/state store. Resolve
219
- // the shared workspace root (spine -> git -> cwd) so a member run from a
220
- // subdirectory reads steering from the real store instead of a nonexistent
221
- // nested .atris, the same resolver the mission/task/usage/autopilot state
222
- // uses. (teamDir markdown discovery is a separate cwd concern, filed.)
223
- const workspaceRoot = require('../lib/mission-root').resolveWorkspaceRoot(process.cwd());
224
- return {
225
- teamDir,
226
- memberDir,
227
- memberFile: path.join(memberDir, 'MEMBER.md'),
228
- missionFile: path.join(memberDir, 'MISSION.md'),
229
- goalsJson: path.join(memberDir, 'goals.json'),
230
- goalsMd: path.join(memberDir, 'goals.md'),
231
- steeringJsonl: path.join(workspaceRoot, '.atris', 'state', 'steering.jsonl'),
232
- };
233
- }
234
-
235
- function missionFileMarkdown({ name, role, description } = {}) {
236
- const title = role || name || 'Member';
237
- const purpose = description || `Define why ${name || 'this member'} exists and how it chooses goals.`;
238
- return [
239
- '# Mission',
240
- '',
241
- '<!-- Human-authored purpose file. Keep this durable; runtime state belongs in .atris/state/*.jsonl and now.md. -->',
242
- '',
243
- '## North Star',
244
- '',
245
- purpose,
246
- '',
247
- '## How To Choose Goals',
248
- '',
249
- `- Use MEMBER.md to stay inside ${title}'s identity, authority, and tools.`,
250
- '- Choose one useful bounded goal toward this mission.',
251
- '- Verify the work, write the receipt, and update the log.',
252
- '- Ask the human when vision, taste, risk, or uncertainty matters.',
253
- '',
254
- ].join('\n');
255
- }
256
-
257
- function ensureMissionFile(memberDir, { name, role, description } = {}) {
258
- const missionPath = path.join(memberDir, 'MISSION.md');
259
- if (!fs.existsSync(missionPath)) {
260
- fs.writeFileSync(missionPath, missionFileMarkdown({ name, role, description }), 'utf8');
261
- }
262
- return missionPath;
263
- }
264
-
265
- function listExistingMemberSlugs() {
266
- const teamDir = path.join(process.cwd(), 'atris', 'team');
267
- try {
268
- return fs.readdirSync(teamDir, { withFileTypes: true })
269
- .filter(entry => entry.isDirectory() && !entry.name.startsWith('_'))
270
- .map(entry => entry.name)
271
- .filter(slug => fs.existsSync(path.join(teamDir, slug, 'MEMBER.md')))
272
- .sort();
273
- } catch {
274
- return [];
275
- }
276
- }
277
-
278
- function editDistance(a, b) {
279
- const rows = a.length + 1;
280
- const cols = b.length + 1;
281
- const dist = Array.from({ length: rows }, (_, i) => [i, ...Array(cols - 1).fill(0)]);
282
- for (let j = 0; j < cols; j++) dist[0][j] = j;
283
- for (let i = 1; i < rows; i++) {
284
- for (let j = 1; j < cols; j++) {
285
- dist[i][j] = Math.min(
286
- dist[i - 1][j] + 1,
287
- dist[i][j - 1] + 1,
288
- dist[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
289
- );
290
- }
291
- }
292
- return dist[rows - 1][cols - 1];
293
- }
294
-
295
- function nearestMemberSlug(name, slugs) {
296
- const needle = String(name || '').toLowerCase();
297
- let best = null;
298
- let bestScore = Infinity;
299
- for (const slug of slugs) {
300
- const score = editDistance(needle, slug.toLowerCase());
301
- if (score < bestScore) {
302
- best = slug;
303
- bestScore = score;
304
- }
305
- }
306
- return bestScore <= Math.max(2, Math.floor(needle.length / 3)) ? best : null;
307
- }
308
-
309
- // A person is never run as an agent. Reads the member's own type and returns
310
- // the plain refusal sentence, or '' when the member can run.
311
- function memberRefusalFor(name, memberFile) {
312
- try {
313
- return memberRefusalReason(name, parseFrontmatter(fs.readFileSync(memberFile, 'utf8')) || {});
314
- } catch {
315
- return '';
316
- }
317
- }
318
-
319
- // Same as requireMemberDir, then stops with a non-zero exit for a human or
320
- // odd-typed member. Every path that runs a member as an agent starts here.
321
- function requireRunnableMember(name) {
322
- const paths = requireMemberDir(name);
323
- const reason = memberRefusalFor(name, paths.memberFile);
324
- if (reason) {
325
- console.error(reason);
326
- process.exit(1);
327
- }
328
- return paths;
329
- }
330
-
331
- function requireMemberDir(name) {
332
- if (!name) {
333
- console.error('Usage: atris member <goal|tick|review> <name> ...');
334
- process.exit(1);
335
- }
336
- if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
337
- console.error('Member name must be a local slug: letters, numbers, dots, underscores, or dashes.');
338
- process.exit(1);
339
- }
340
- const resolved = resolveMemberRuntime(name);
341
- const paths = resolved.paths;
342
- if (!fs.existsSync(paths.memberFile)) {
343
- const aliasHint = resolved.aliasOf ? ` or atris/team/${resolved.aliasOf}/MEMBER.md` : '';
344
- console.error(`Member "${name}" not found at atris/team/${name}/MEMBER.md${aliasHint}`);
345
- const team = listExistingMemberSlugs();
346
- const nearest = nearestMemberSlug(name, team);
347
- if (nearest) {
348
- console.error(`Did you mean "${nearest}"?`);
349
- } else if (team.length) {
350
- console.error(`Team here: ${team.join(', ')}`);
351
- } else {
352
- console.error(`No team members exist here yet. Create one: atris member create ${name} --role="..."`);
353
- }
354
- process.exit(1);
355
- }
356
- return {
357
- ...paths,
358
- requestedName: resolved.requestedName,
359
- storageName: resolved.storageName,
360
- runtimeKind: resolved.runtimeKind,
361
- aliasOf: resolved.aliasOf,
362
- };
363
- }
364
-
365
- function emptyMemberGoals(name) {
366
- return {
367
- schema: 'atris.member_goals.v1',
368
- member: name,
369
- updated_at: stampIso(),
370
- goals: [],
371
- };
372
- }
373
-
374
- function loadMemberGoals(name, paths = memberPaths(name)) {
375
- const parsed = readJson(paths.goalsJson, {}) || {};
376
- return {
377
- ...emptyMemberGoals(name),
378
- ...parsed,
379
- goals: Array.isArray(parsed.goals) ? parsed.goals : [],
380
- };
381
- }
382
-
383
- function activeGoal(goalsState, goalId = '') {
384
- if (goalId) return goalsState.goals.find((goal) => goal.id === goalId) || null;
385
- for (let i = (goalsState.goals || []).length - 1; i >= 0; i -= 1) {
386
- if (goalsState.goals[i]?.status === 'active') return goalsState.goals[i];
387
- }
388
- return goalsState.goals[0] || null;
389
- }
390
-
391
- function allExperiments(state) {
392
- const out = [];
393
- for (const goal of state.goals || []) {
394
- for (const experiment of goal.experiments || []) out.push({ goal, experiment });
395
- }
396
- return out;
397
- }
398
-
399
- function findExperiment(state, experimentId) {
400
- for (const item of allExperiments(state)) {
401
- if (item.experiment.id === experimentId) return item;
402
- }
403
- return { goal: null, experiment: null };
404
- }
405
-
406
- function latestByTime(items, field = 'created_at') {
407
- return items
408
- .filter(Boolean)
409
- .slice()
410
- .sort((a, b) => String(b[field] || '').localeCompare(String(a[field] || '')))[0] || null;
411
- }
412
-
413
- function recentLogLines(memberDir, maxLines = 8) {
414
- const logsDir = path.join(memberDir, 'logs');
415
- if (!fs.existsSync(logsDir)) return [];
416
- const logs = fs.readdirSync(logsDir)
417
- .filter((name) => /^\d{4}-\d{2}-\d{2}\.md$/.test(name))
418
- .sort();
419
- const latest = logs[logs.length - 1];
420
- if (!latest) return [];
421
- return fs.readFileSync(path.join(logsDir, latest), 'utf8')
422
- .split(/\r?\n/)
423
- .filter((line) => line.trim())
424
- .slice(-maxLines);
425
- }
426
-
427
- function readOptionalText(filePath) {
428
- try {
429
- return fs.readFileSync(filePath, 'utf8');
430
- } catch {
431
- return '';
432
- }
433
- }
434
-
435
- function extractMarkdownSection(text, heading) {
436
- const lines = String(text || '').split(/\r?\n/);
437
- const target = String(heading || '').trim().toLowerCase();
438
- const start = lines.findIndex((line) => {
439
- const match = line.match(/^##\s+(.+?)\s*$/);
440
- return match && match[1].trim().toLowerCase() === target;
441
- });
442
- if (start === -1) return '';
443
- const out = [];
444
- for (let i = start + 1; i < lines.length; i += 1) {
445
- if (/^##\s+/.test(lines[i])) break;
446
- out.push(lines[i]);
447
- }
448
- return out.join('\n').trim();
449
- }
450
-
451
- function firstUsefulLine(text) {
452
- return String(text || '')
453
- .split(/\r?\n/)
454
- .map((line) => line.replace(/^[-*]\s+/, '').trim())
455
- .find((line) => line && !line.startsWith('<!--')) || '';
456
- }
457
-
458
- function compactSentence(text, max = 120) {
459
- const clean = String(text || '').replace(/\s+/g, ' ').trim();
460
- if (clean.length <= max) return clean;
461
- // Cut at a word boundary so titles never end mid-word ("rollb...").
462
- const cut = clean.slice(0, Math.max(0, max - 1));
463
- const lastSpace = cut.lastIndexOf(' ');
464
- return `${(lastSpace > max * 0.6 ? cut.slice(0, lastSpace) : cut).trim()}...`;
465
- }
466
-
467
- function activeRuntimeMissionFromNow(nowText) {
468
- const heading = String(nowText || '').match(/^##\s+(.+?)\s*$/m)?.[1]?.trim() || '';
469
- const id = String(nowText || '').match(/^- id:\s*(.+?)\s*$/m)?.[1]?.trim() || '';
470
- const status = String(nowText || '').match(/^- status:\s*(.+?)\s*$/m)?.[1]?.trim() || '';
471
- const next = String(nowText || '').match(/^- next:\s*(.+?)\s*$/m)?.[1]?.trim() || '';
472
- return { heading, id, status, next };
473
- }
474
-
475
- function missionPurpose(paths) {
476
- const missionText = readOptionalText(paths.missionFile);
477
- const nowText = readOptionalText(path.join(paths.memberDir, 'now.md'));
478
- const northStar = firstUsefulLine(extractMarkdownSection(missionText, 'North Star'));
479
- const goalGuidance = extractMarkdownSection(missionText, 'How To Choose Goals');
480
- const runtimeMission = activeRuntimeMissionFromNow(nowText);
481
- const meaningful = Boolean(northStar) && !/define why .* exists/i.test(northStar);
482
- return {
483
- missionText,
484
- nowText,
485
- northStar,
486
- goalGuidance,
487
- runtimeMission,
488
- meaningful,
489
- };
490
- }
491
-
492
- const MEMBER_RUN_RUNNABLE_STATUSES = new Set(['planning', 'running', 'ready']);
493
- const MISSION_TERMINAL_STATUSES = new Set(['complete', 'stopped', 'failed']);
494
-
495
- function memberRunMissionMap(root = process.cwd()) {
496
- try {
497
- const missionMod = require('./mission');
498
- const map = new Map(missionMod.loadMissionMap(root));
499
- for (const rolled of missionMod.listWorktreeRollupMissions(root)) {
500
- if (!rolled?.id) continue;
501
- const existing = map.get(rolled.id);
502
- if (!existing || missionTimeMs(rolled) >= missionTimeMs(existing)) {
503
- map.set(rolled.id, rolled);
504
- }
505
- }
506
- return map;
507
- } catch {
508
- return new Map();
509
- }
510
- }
511
-
512
- function memberRunRunnableMissionId(candidateId, missionMap) {
513
- const id = String(candidateId || '').trim();
514
- if (!id) return '';
515
- const mission = missionMap.get(id);
516
- // Unknown here can mean a worktree-held mission; let mission run decide.
517
- if (!mission) return id;
518
- return MEMBER_RUN_RUNNABLE_STATUSES.has(mission.status) ? id : '';
519
- }
520
-
521
- function memberRunOwnedActiveMissionId(name, missionMap) {
522
- // Discovery fallback when the member's own pointers are stale: resume the
523
- // member's newest moving mission. ready is excluded, it waits for review.
524
- const owner = String(name || '').trim().toLowerCase();
525
- const candidates = Array.from(missionMap.values())
526
- .filter((mission) => String(mission?.owner || '').trim().toLowerCase() === owner)
527
- .filter((mission) => mission.status === 'running' || mission.status === 'planning')
528
- .filter((mission) => !/^decide and start the next useful mission after:/i.test(String(mission?.objective || '')))
529
- .sort((a, b) => {
530
- if (a.status !== b.status) return a.status === 'running' ? -1 : 1;
531
- return String(b.updated_at || b.created_at || '').localeCompare(String(a.updated_at || a.created_at || ''));
532
- });
533
- return candidates[0]?.id || '';
534
- }
535
-
536
- const MEMBER_LIVE_MISSION_STATUSES = new Set(['running', 'planning']);
537
- const MEMBER_ACTIVE_ACTIVITY_MS = 24 * 60 * 60 * 1000;
538
- const MEMBER_RECENT_ACTIVITY_MS = 72 * 60 * 60 * 1000;
539
-
540
- function memberSlugFromRecord(member) {
541
- if (member?.format === 'directory' && member.dir) return path.basename(member.dir);
542
- if (member?.format === 'flat' && member.path) return path.basename(member.path, '.md');
543
- return lowerCompact(member?.name || '');
544
- }
545
-
546
- function isFleetMember(member) {
547
- if (!member || member.format !== 'directory') return false;
548
- const slug = memberSlugFromRecord(member);
549
- return Boolean(slug) && !new Set(['_archive', '_archived', '_template']).has(slug);
550
- }
551
-
552
- function missionTimeMs(mission) {
553
- return Date.parse(mission?.last_tick_at || mission?.updated_at || mission?.created_at || '') || 0;
554
- }
555
-
556
- function memberOwnedMissions(name, missionMap = memberRunMissionMap()) {
557
- const owner = lowerCompact(name);
558
- return Array.from(missionMap.values())
559
- .filter((mission) => lowerCompact(mission?.owner) === owner)
560
- .sort((a, b) => missionTimeMs(b) - missionTimeMs(a));
561
- }
562
-
563
- function memberLatestMission(name, missionMap = memberRunMissionMap()) {
564
- return memberOwnedMissions(name, missionMap)[0] || null;
565
- }
566
-
567
- function memberLiveMission(name, missionMap = memberRunMissionMap()) {
568
- return memberOwnedMissions(name, missionMap)
569
- .find((mission) => {
570
- if (!MEMBER_LIVE_MISSION_STATUSES.has(String(mission?.status || '').toLowerCase())) return false;
571
- // A closed window can leave a running row behind. Only a known dead
572
- // process proves it is free; missing process records still hold it.
573
- try {
574
- const root = mission.worktree_root || mission.worktree?.path || process.cwd();
575
- const health = require('./mission').missionDriverHealth(mission, root);
576
- return !health.pid || health.alive;
577
- } catch { return true; }
578
- }) || null;
579
- }
580
-
581
- function missionShortName(mission, max = 36) {
582
- return compactSentence(mission?.slug || mission?.title || mission?.objective || mission?.id || '', max) || '-';
583
- }
584
-
585
- function ageLabelFromMs(ms, nowMs = Date.now()) {
586
- if (!ms) return '-';
587
- const seconds = Math.max(0, Math.floor((nowMs - ms) / 1000));
588
- if (seconds < 60) return 'now';
589
- const minutes = Math.floor(seconds / 60);
590
- if (minutes < 60) return `${minutes}m`;
591
- const hours = Math.floor(minutes / 60);
592
- if (hours < 48) return `${hours}h`;
593
- return `${Math.floor(hours / 24)}d`;
594
- }
595
-
596
- function timestampMs(value) {
597
- const ms = Date.parse(value || '');
598
- return Number.isFinite(ms) ? ms : 0;
599
- }
600
-
601
- function newestMtimeRecursive(root) {
602
- if (!fs.existsSync(root)) return 0;
603
- let newest = 0;
604
- const stack = [root];
605
- while (stack.length) {
606
- const current = stack.pop();
607
- let stat = null;
608
- try {
609
- stat = fs.statSync(current);
610
- } catch {
611
- continue;
612
- }
613
- newest = Math.max(newest, stat.mtimeMs || 0);
614
- if (!stat.isDirectory()) continue;
615
- let entries = [];
616
- try {
617
- entries = fs.readdirSync(current);
618
- } catch {
619
- continue;
620
- }
621
- for (const entry of entries) stack.push(path.join(current, entry));
622
- }
623
- return newest;
624
- }
625
-
626
- function newestRunActivityMtime(name, root = process.cwd()) {
627
- const runsDir = path.join(root, 'atris', 'runs');
628
- if (!fs.existsSync(runsDir)) return 0;
629
- const needle = lowerCompact(name);
630
- let newest = 0;
631
- for (const entry of fs.readdirSync(runsDir)) {
632
- if (!lowerCompact(entry).includes(needle)) continue;
633
- try {
634
- newest = Math.max(newest, fs.statSync(path.join(runsDir, entry)).mtimeMs || 0);
635
- } catch { /* ignore unreadable activity entries */ }
636
- }
637
- return newest;
638
- }
639
-
640
- function memberLastActivity(name, paths = memberPaths(name), root = process.cwd()) {
641
- const logsMs = newestMtimeRecursive(path.join(paths.memberDir, 'logs'));
642
- const runsMs = newestRunActivityMtime(name, root);
643
- const ms = Math.max(logsMs, runsMs);
644
- return {
645
- at_ms: ms,
646
- age: ageLabelFromMs(ms),
647
- };
648
- }
18
+ // Runtime aliases: problem-finder, info-organizer, coordinator, task-planner, improver, problem-solver
19
+ // memberPaths resolves steeringJsonl: path.join(workspaceRoot, '.atris', 'state', 'steering.jsonl')
20
+ const {
21
+ findWorkspaceBusinessId, todayLogName, ensureMemberLog, appendMemberLifecycleLog, archiveName,
22
+ uniqueArchiveDir, parseDaysFlag, parseConfirmFlag, readRepeatedFlag, stripKnownFlags,
23
+ stampIso, fileSafeStamp, slugHash, makeGoalId, uniqueGoalId, makeExperimentId,
24
+ MEMBER_RUNTIME_ALIASES, memberRuntimeKind, resolveMemberRuntime, memberPaths,
25
+ missionFileMarkdown, ensureMissionFile, listExistingMemberSlugs, editDistance, nearestMemberSlug,
26
+ memberRefusalFor, requireRunnableMember, requireMemberDir, emptyMemberGoals, loadMemberGoals,
27
+ activeGoal, allExperiments, findExperiment, latestByTime, recentLogLines, readOptionalText,
28
+ extractMarkdownSection, firstUsefulLine, compactSentence, activeRuntimeMissionFromNow, missionPurpose,
29
+ MEMBER_RUN_RUNNABLE_STATUSES, MISSION_TERMINAL_STATUSES, memberRunMissionMap,
30
+ memberRunRunnableMissionId, memberRunOwnedActiveMissionId, MEMBER_LIVE_MISSION_STATUSES,
31
+ MEMBER_ACTIVE_ACTIVITY_MS, MEMBER_RECENT_ACTIVITY_MS, memberSlugFromRecord, isFleetMember,
32
+ missionTimeMs, memberOwnedMissions, memberLatestMission, memberLiveMission, missionShortName,
33
+ ageLabelFromMs, timestampMs, newestMtimeRecursive, newestRunActivityMtime, memberLastActivity,
34
+ } = require('../lib/member-core');
649
35
 
650
36
  function memberMissionSummary(name, missionMap = memberRunMissionMap(), root = process.cwd()) {
651
37
  const latest = memberLatestMission(name, missionMap);
@@ -1711,17 +1097,7 @@ function steeringWakeDirective(steering, name, goal) {
1711
1097
  return null;
1712
1098
  }
1713
1099
 
1714
- function taskRef(task) {
1715
- return task?.display_id || task?.displayId || task?.legacy_ref || task?.legacyRef || task?.ref || task?.id || null;
1716
- }
1717
-
1718
- function lowerCompact(value) {
1719
- return String(value || '').trim().toLowerCase();
1720
- }
1721
-
1722
- function shellQuote(value) {
1723
- return `'${String(value || '').replace(/'/g, "'\\''")}'`;
1724
- }
1100
+ const { taskRef, lowerCompact, shellQuote } = require('../lib/member-core');
1725
1101
 
1726
1102
  function taskCandidateOwnerValues(task) {
1727
1103
  return [
@@ -2263,53 +1639,7 @@ function readRecentWakeReceiptEvidence(name) {
2263
1639
  };
2264
1640
  }
2265
1641
 
2266
- function readJsonlRowsIfExists(filePath, maxRows = 80) {
2267
- const tailBytes = 256 * 1024;
2268
- let fd;
2269
- let text = '';
2270
- let truncated = false;
2271
- try {
2272
- fd = fs.openSync(filePath, 'r');
2273
- const stat = fs.fstatSync(fd);
2274
- const bytesToRead = Math.min(stat.size, tailBytes);
2275
- const start = Math.max(0, stat.size - bytesToRead);
2276
- const buffer = Buffer.alloc(bytesToRead);
2277
- let bytesRead = 0;
2278
- while (bytesRead < bytesToRead) {
2279
- const count = fs.readSync(
2280
- fd,
2281
- buffer,
2282
- bytesRead,
2283
- bytesToRead - bytesRead,
2284
- start + bytesRead
2285
- );
2286
- if (count === 0) break;
2287
- bytesRead += count;
2288
- }
2289
- text = buffer.toString('utf8', 0, bytesRead);
2290
- truncated = start > 0;
2291
- } catch {
2292
- return [];
2293
- } finally {
2294
- if (fd !== undefined) {
2295
- try { fs.closeSync(fd); } catch {}
2296
- }
2297
- }
2298
- const rows = [];
2299
- const lines = text
2300
- .split(/\r?\n/)
2301
- .slice(truncated ? 1 : 0)
2302
- .filter((line) => line.trim())
2303
- .slice(-Math.max(1, maxRows));
2304
- for (const line of lines) {
2305
- try {
2306
- rows.push(JSON.parse(line));
2307
- } catch {
2308
- // Bad rows should not block discovery from valid rows in the same stream.
2309
- }
2310
- }
2311
- return rows;
2312
- }
1642
+ const { readJsonlRowsIfExists } = require('../lib/member-core');
2313
1643
 
2314
1644
  function stringFromSignalValue(value) {
2315
1645
  if (typeof value === 'string') return value.trim();
@@ -3135,50 +2465,7 @@ function createAutonomousDiscoveryTask(candidate) {
3135
2465
  }
3136
2466
  }
3137
2467
 
3138
- function writeAutonomousDiscoveryReceipt(name, candidate, taskResult) {
3139
- const runsDir = path.join(process.cwd(), 'atris', 'runs');
3140
- fs.mkdirSync(runsDir, { recursive: true });
3141
- const payload = {
3142
- schema: 'atris.signal_scout_autonomous_discovery.v1',
3143
- created_at: stampIso(),
3144
- member: name,
3145
- source: candidate.source,
3146
- action: taskResult.existing ? 'existing_task_found' : 'task_created',
3147
- task_ref: taskResult.task_ref || null,
3148
- task_id: taskResult.task_id || taskResult.task?.id || null,
3149
- task_title: candidate.task_title || candidate.objective_title || candidate.title || null,
3150
- error_pattern: candidate.error_pattern || null,
3151
- error_count: candidate.error_count || null,
3152
- evidence_paths: candidate.evidence_paths || [],
3153
- command: taskResult.command || candidate.next_command || null,
3154
- ok: taskResult.ok === true,
3155
- error: taskResult.error || null,
3156
- };
3157
- const receiptPath = path.join(runsDir, `signal-scout-autonomous-discovery-${fileSafeStamp()}.json`);
3158
- fs.writeFileSync(receiptPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
3159
- const fields = {
3160
- team: name,
3161
- decision: 'auto-discovery',
3162
- pattern: payload.error_pattern,
3163
- count: payload.error_count,
3164
- task: payload.task_ref,
3165
- receipt: path.relative(process.cwd(), receiptPath),
3166
- };
3167
- const projectLogPath = appendProjectLog('Signal Scout autonomous problem discovery', fields);
3168
- return {
3169
- receipt_path: receiptPath,
3170
- project_log_path: projectLogPath,
3171
- payload,
3172
- };
3173
- }
3174
-
3175
- function writeWakeReceipt(name, payload) {
3176
- const runsDir = path.join(process.cwd(), 'atris', 'runs');
3177
- fs.mkdirSync(runsDir, { recursive: true });
3178
- const receiptPath = path.join(runsDir, `member-wake-${name}-${fileSafeStamp()}.json`);
3179
- fs.writeFileSync(receiptPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
3180
- return receiptPath;
3181
- }
2468
+ const { writeAutonomousDiscoveryReceipt, writeWakeReceipt } = require('../lib/member-core');
3182
2469
 
3183
2470
  function memberLoopPaths(name) {
3184
2471
  const stateDir = path.join(process.cwd(), '.atris', 'state', 'member-loops');
@@ -3432,102 +2719,10 @@ function memberAliveCronStatus(name, paths = memberLoopPaths(name)) {
3432
2719
  };
3433
2720
  }
3434
2721
 
3435
- function writeMemberLoopReceipt(name, payload) {
3436
- const runsDir = path.join(process.cwd(), 'atris', 'runs');
3437
- fs.mkdirSync(runsDir, { recursive: true });
3438
- const receiptPath = path.join(runsDir, `member-loop-${name}-${fileSafeStamp()}.json`);
3439
- fs.writeFileSync(receiptPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
3440
- return receiptPath;
3441
- }
3442
-
3443
- function isPidAlive(pid) {
3444
- const numeric = Number(pid);
3445
- if (!Number.isFinite(numeric) || numeric <= 0) return false;
3446
- try {
3447
- process.kill(numeric, 0);
3448
- return true;
3449
- } catch (error) {
3450
- return error?.code === 'EPERM';
3451
- }
3452
- }
3453
-
3454
- function safeReadText(filePath, maxBytes = 250000) {
3455
- try {
3456
- const stat = fs.statSync(filePath);
3457
- if (!stat.isFile() || stat.size > maxBytes) return '';
3458
- return fs.readFileSync(filePath, 'utf8');
3459
- } catch {
3460
- return '';
3461
- }
3462
- }
3463
-
3464
- function repoRelative(root, filePath) {
3465
- return path.relative(root, filePath).replace(/\\/g, '/');
3466
- }
3467
-
3468
- function dateFromLogPath(relativePath) {
3469
- const match = String(relativePath || '').match(/\b(\d{4}-\d{2}-\d{2})\b/);
3470
- return match ? match[1] : null;
3471
- }
3472
-
3473
- // Mission driver logs, goals files, and the wiki log have no date in their
3474
- // name; without one they skipped the 7-day window and months-old lines kept
3475
- // counting as this week's pain. The last write date stands in for them.
3476
- function logDateForScan(relativePath, filePath) {
3477
- const named = dateFromLogPath(relativePath);
3478
- if (named) return named;
3479
- try {
3480
- // Local calendar day, like the dated journal names beside it;
3481
- // toISOString is UTC and calls an evening write tomorrow's.
3482
- const mtime = fs.statSync(filePath).mtime;
3483
- return `${mtime.getFullYear()}-${String(mtime.getMonth() + 1).padStart(2, '0')}-${String(mtime.getDate()).padStart(2, '0')}`;
3484
- } catch {
3485
- return null;
3486
- }
3487
- }
3488
-
3489
- function compareLogEvidenceRecentFirst(a, b) {
3490
- const dateA = a?.date || '';
3491
- const dateB = b?.date || '';
3492
- if (dateA !== dateB) return dateB.localeCompare(dateA);
3493
- const pathCompare = String(a?.path || '').localeCompare(String(b?.path || ''));
3494
- if (pathCompare !== 0) return pathCompare;
3495
- return Number(a?.line || 0) - Number(b?.line || 0);
3496
- }
3497
-
3498
- function compareRepeatedFailureSignals(a, b) {
3499
- const countCompare = Number(b?.count || 0) - Number(a?.count || 0);
3500
- if (countCompare !== 0) return countCompare;
3501
- const evidenceCompare = compareLogEvidenceRecentFirst(a?.evidence?.[0], b?.evidence?.[0]);
3502
- if (evidenceCompare !== 0) return evidenceCompare;
3503
- return String(a?.pattern || '').localeCompare(String(b?.pattern || ''));
3504
- }
3505
-
3506
- function listFilesBounded(rootDir, { maxFiles = 220, extensions = ['.md', '.txt', '.json', '.jsonl'] } = {}) {
3507
- const files = [];
3508
- const stack = [rootDir];
3509
- const skipDirs = new Set(['.git', 'node_modules', '.next', 'dist', 'build', '__pycache__']);
3510
- while (stack.length && files.length < maxFiles) {
3511
- const current = stack.pop();
3512
- let entries = [];
3513
- try {
3514
- entries = fs.readdirSync(current, { withFileTypes: true });
3515
- } catch {
3516
- continue;
3517
- }
3518
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
3519
- if (files.length >= maxFiles) break;
3520
- const fullPath = path.join(current, entry.name);
3521
- if (entry.isDirectory()) {
3522
- if (!skipDirs.has(entry.name) && !entry.name.startsWith('.')) stack.push(fullPath);
3523
- continue;
3524
- }
3525
- if (!entry.isFile()) continue;
3526
- if (extensions.includes(path.extname(entry.name))) files.push(fullPath);
3527
- }
3528
- }
3529
- return files;
3530
- }
2722
+ const {
2723
+ writeMemberLoopReceipt, isPidAlive, safeReadText, repoRelative, dateFromLogPath,
2724
+ logDateForScan, compareLogEvidenceRecentFirst, compareRepeatedFailureSignals, listFilesBounded,
2725
+ } = require('../lib/member-core');
3531
2726
 
3532
2727
  function stripAutoImproverTitleNoise(text) {
3533
2728
  let clean = String(text || '').replace(/\s+/g, ' ').trim();
@@ -4133,13 +3328,7 @@ function createAutoImproverTask(candidate, receiptPath) {
4133
3328
  }
4134
3329
  }
4135
3330
 
4136
- function writeAutoImproverReceipt(name, payload, receiptPath = null) {
4137
- const finalReceiptPath = receiptPath || path.join(process.cwd(), 'atris', 'runs', `auto-improver-dogfood-${fileSafeStamp()}.json`);
4138
- writeJson(finalReceiptPath, payload);
4139
- const latestPath = path.join(process.cwd(), '.atris', 'state', 'auto-improver-dogfood-latest.json');
4140
- writeJson(latestPath, { ...payload, receipt_path: finalReceiptPath });
4141
- return { receiptPath: finalReceiptPath, latestPath };
4142
- }
3331
+ const { writeAutoImproverReceipt } = require('../lib/member-core');
4143
3332
 
4144
3333
  async function runAutoImproverWake(name, paths, { execute = false, confirmed = false } = {}) {
4145
3334
  const scan = collectAutoImproverScan(process.cwd());
@@ -4396,92 +3585,7 @@ function archiveMemberLoopState(name) {
4396
3585
  return payload;
4397
3586
  }
4398
3587
 
4399
- function appendMemberGoalLog(memberDir, name, title, fields = {}) {
4400
- const logsDir = path.join(memberDir, 'logs');
4401
- fs.mkdirSync(logsDir, { recursive: true });
4402
- const logPath = path.join(logsDir, todayLogName());
4403
- const stamp = new Date().toTimeString().slice(0, 5);
4404
- const rows = [
4405
- `## ${stamp} · ${title}`,
4406
- `- team: ${name}`,
4407
- ...Object.entries(fields)
4408
- .filter(([, value]) => value !== undefined && value !== null)
4409
- .map(([key, value]) => [key, String(value).replace(/\n/g, ' ').trim()])
4410
- .filter(([, text]) => text !== '')
4411
- .map(([key, text]) => `- ${key}: ${text}`),
4412
- '',
4413
- ];
4414
- fs.appendFileSync(logPath, rows.join('\n'), 'utf8');
4415
- return logPath;
4416
- }
4417
-
4418
- function appendProjectLog(title, fields = {}) {
4419
- const today = todayLogName();
4420
- const year = today.slice(0, 4);
4421
- const logsDir = path.join(process.cwd(), 'atris', 'logs', year);
4422
- fs.mkdirSync(logsDir, { recursive: true });
4423
- const logPath = path.join(logsDir, today);
4424
- const stamp = new Date().toTimeString().slice(0, 5);
4425
- const rows = [
4426
- `## ${stamp} · ${title}`,
4427
- ...Object.entries(fields)
4428
- .filter(([, value]) => value !== undefined && value !== null)
4429
- .map(([key, value]) => [key, String(value).replace(/\n/g, ' ').trim()])
4430
- .filter(([, text]) => text !== '')
4431
- .map(([key, text]) => `- ${key}: ${text}`),
4432
- '',
4433
- ];
4434
- fs.appendFileSync(logPath, rows.join('\n'), 'utf8');
4435
- return logPath;
4436
- }
4437
-
4438
- // --- YAML Frontmatter Parser (shared with skill.js) ---
4439
-
4440
- function parseFrontmatter(content) {
4441
- // \r?\n so a CRLF-line-ending MEMBER.md (Windows-edited) still parses; otherwise
4442
- // the delimiter never matched (and trailing \r broke the per-line key matchers),
4443
- // silently dropping the member's entire frontmatter: role, skills, permissions, tools.
4444
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
4445
- if (!match) return null;
4446
-
4447
- const yaml = match[1];
4448
- const result = {};
4449
- let currentKey = null;
4450
-
4451
- for (const line of yaml.split(/\r?\n/)) {
4452
- const listMatch = line.match(/^\s+-\s+(.+)$/);
4453
- if (listMatch && currentKey) {
4454
- if (!Array.isArray(result[currentKey])) result[currentKey] = [];
4455
- result[currentKey].push(listMatch[1].trim());
4456
- continue;
4457
- }
4458
-
4459
- const nestedMatch = line.match(/^\s+([a-z_-]+):\s*(.*)$/);
4460
- if (nestedMatch && currentKey && typeof result[currentKey] === 'object' && !Array.isArray(result[currentKey])) {
4461
- const val = nestedMatch[2].trim();
4462
- result[currentKey][nestedMatch[1]] = val === 'true' ? true : val === 'false' ? false : val || true;
4463
- continue;
4464
- }
4465
-
4466
- const kvMatch = line.match(/^([a-z_-]+):\s*(.*)$/);
4467
- if (kvMatch) {
4468
- currentKey = kvMatch[1];
4469
- const val = kvMatch[2].trim();
4470
- if (val === '') {
4471
- result[currentKey] = {};
4472
- } else if (val.startsWith('[') && val.endsWith(']')) {
4473
- result[currentKey] = val.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
4474
- } else {
4475
- // Unwrap only a matching outer pair: `verify: node -e "process.exit(0)"`
4476
- // must keep its closing quote or the check is a shell syntax error.
4477
- const quoted = val.match(/^(["'])([\s\S]*)\1$/);
4478
- result[currentKey] = quoted ? quoted[2] : val;
4479
- }
4480
- }
4481
- }
4482
-
4483
- return result;
4484
- }
3588
+ const { appendMemberGoalLog, appendProjectLog, parseFrontmatter } = require('../lib/member-core');
4485
3589
 
4486
3590
  // --- Member Discovery ---
4487
3591
 
@@ -10312,6 +9416,10 @@ function scrubFeedJargon(text) {
10312
9416
  .replace(/\bmission-\d{4}-\d{2}-\d{2}-[a-z0-9-]+\b/gi, '')
10313
9417
  .replace(/\b[0-9a-f]{7,40}\b/g, '')
10314
9418
  .replace(/\(\s*(commit\s*)?\)/gi, '')
9419
+ .replace(/\s+\bas\s+(?:``|''|""|(?=[.,;!?]|$))/gi, '')
9420
+ .replace(/\s+(?:in|on|at|from)\s+(?:``|''|"")(?=[.,;!?]|\s|$)/gi, '')
9421
+ .replace(/\s*(?:``|''|"")/g, '')
9422
+ .replace(/\s+([.,;!?])/g, '$1')
10315
9423
  .replace(/\s+/g, ' ')
10316
9424
  .trim();
10317
9425
  }
@@ -10350,6 +9458,9 @@ function isFeedFillerText(text) {
10350
9458
  if (/^member (wake|goal-from-mission|goal created|initialized)\b/i.test(t)) return true;
10351
9459
  if (/^\[?logged update\]?$/i.test(t)) return true;
10352
9460
  if (/^(next[_ ]push|verdict):/i.test(t)) return true;
9461
+ if (/^reading additional input from stdin\b/i.test(t)) return true;
9462
+ if (/^openai codex\b/i.test(t)) return true;
9463
+ if (/^(?:workdir|model|provider|approval|sandbox|reasoning effort|reasoning summaries|session id):/i.test(t)) return true;
10353
9464
  if (/^[`'"]?atris\s/i.test(t) || /\batris\s+[a-z-]+\s+[a-z-]+\s+--[a-z]/i.test(t)) return true;
10354
9465
  return false;
10355
9466
  }
@@ -10530,13 +9641,94 @@ function listMemberUnlockedSkills(memberDir, frontmatterSkills = []) {
10530
9641
 
10531
9642
  function resolveMemberEngineSeat(memberSlug, root = process.cwd()) {
10532
9643
  const slug = String(memberSlug || '').trim().toLowerCase();
10533
- const engineRole = slug === 'navigator'
9644
+ const fallbackRole = slug === 'navigator'
10534
9645
  ? 'navigator'
10535
9646
  : (slug === 'validator' || slug === 'architect')
10536
9647
  ? 'validator'
10537
9648
  : 'executor';
10538
9649
  try {
10539
- const { resolveEngineForRoleRanked } = require('../lib/engine-registry');
9650
+ const {
9651
+ resolveEngineForMember,
9652
+ readMemberCard,
9653
+ autoJobForMember,
9654
+ } = require('../lib/member-engine');
9655
+ const { rosterEngine } = require('../lib/gm-season');
9656
+ const {
9657
+ ENGINE_JOBS,
9658
+ readRosterState,
9659
+ resolveEngineForRoleRanked,
9660
+ rosterPickFromValue,
9661
+ } = require('../lib/engine-registry');
9662
+ const card = readMemberCard(root, slug);
9663
+ const kind = autoJobForMember(slug, card.role || '');
9664
+ const engineRole = ENGINE_JOBS[kind] || fallbackRole;
9665
+ const state = readRosterState(root);
9666
+ const localRosterState = {
9667
+ ...state,
9668
+ machine: { picks: {}, team: [], file: null, path: null },
9669
+ };
9670
+ const fromProjectRoster = rosterEngine(slug, root, {
9671
+ rosterState: localRosterState,
9672
+ role: card.role || '',
9673
+ hasCard: Boolean(card.found),
9674
+ });
9675
+ if (fromProjectRoster && fromProjectRoster.engine) {
9676
+ return {
9677
+ engine_id: fromProjectRoster.engine,
9678
+ tier: 'pro',
9679
+ models: fromProjectRoster.model ? [fromProjectRoster.model] : [fromProjectRoster.engine],
9680
+ role: engineRole,
9681
+ reason: fromProjectRoster.source ? `${fromProjectRoster.engine} from ${fromProjectRoster.source}.` : `${fromProjectRoster.engine} from roster.`,
9682
+ };
9683
+ }
9684
+ let fm = {};
9685
+ try {
9686
+ const cardPath = path.join(root, 'atris', 'team', slug, 'MEMBER.md');
9687
+ if (fs.existsSync(cardPath)) {
9688
+ fm = parseFrontmatter(fs.readFileSync(cardPath, 'utf8')) || {};
9689
+ }
9690
+ } catch {}
9691
+ const explicitEngine = fm.engine || fm.executed_by || '';
9692
+ if (explicitEngine) {
9693
+ let explicitId = String(explicitEngine).trim().toLowerCase();
9694
+ try {
9695
+ const parsedPick = rosterPickFromValue(String(explicitEngine), engineRole);
9696
+ if (parsedPick && parsedPick.engine) explicitId = parsedPick.engine;
9697
+ } catch {}
9698
+ return {
9699
+ engine_id: explicitId,
9700
+ tier: 'pro',
9701
+ models: fm.model ? [fm.model] : [explicitId],
9702
+ role: engineRole,
9703
+ reason: `${explicitId} declared in MEMBER.md.`,
9704
+ };
9705
+ }
9706
+ const fromMember = resolveEngineForMember(slug, root, { quiet: true, rosterState: state });
9707
+ if (fromMember && fromMember.roster_backed && fromMember.engine) {
9708
+ return {
9709
+ engine_id: fromMember.engine.id,
9710
+ tier: fromMember.engine.tier || 'pro',
9711
+ models: fromMember.model
9712
+ ? [fromMember.model]
9713
+ : (Array.isArray(fromMember.engine.models) ? fromMember.engine.models : []),
9714
+ role: fromMember.role || engineRole,
9715
+ reason: fromMember.reason || `${fromMember.engine.id} selected for ${slug}.`,
9716
+ };
9717
+ }
9718
+ const fromAnyRoster = rosterEngine(slug, root, {
9719
+ rosterState: state,
9720
+ role: card.role || '',
9721
+ hasCard: Boolean(card.found),
9722
+ });
9723
+ if (fromAnyRoster && fromAnyRoster.engine) {
9724
+ return {
9725
+ engine_id: fromAnyRoster.engine,
9726
+ tier: 'pro',
9727
+ models: fromAnyRoster.model ? [fromAnyRoster.model] : [fromAnyRoster.engine],
9728
+ role: engineRole,
9729
+ reason: fromAnyRoster.source ? `${fromAnyRoster.engine} from ${fromAnyRoster.source}.` : `${fromAnyRoster.engine} from roster.`,
9730
+ };
9731
+ }
10540
9732
  const picked = resolveEngineForRoleRanked(engineRole, root, { taskType: engineRole });
10541
9733
  if (picked && picked.engine) {
10542
9734
  return {
@@ -10552,7 +9744,7 @@ function resolveMemberEngineSeat(memberSlug, root = process.cwd()) {
10552
9744
  engine_id: 'claude',
10553
9745
  tier: 'pro',
10554
9746
  models: ['claude'],
10555
- role: engineRole,
9747
+ role: fallbackRole,
10556
9748
  reason: 'Default engine seat.',
10557
9749
  };
10558
9750
  }
@@ -10587,9 +9779,12 @@ function buildMemberCard(name, root = process.cwd()) {
10587
9779
  const recentWork = collectMemberWorkFeed(root, { member: owner, limit: 3 });
10588
9780
  const engineSeat = resolveMemberEngineSeat(owner, root);
10589
9781
 
9782
+ const rawMissionSummary = !isFeedFillerText(mission.latest?.last_summary)
9783
+ ? mission.latest?.last_summary
9784
+ : '';
10590
9785
  const doingNow = mission.latest
10591
9786
  ? {
10592
- summary: compactSentence(scrubFeedJargon(mission.latest.last_summary || mission.objective || mission.name || ''), 120),
9787
+ summary: compactSentence(scrubFeedJargon(rawMissionSummary || mission.objective || mission.name || ''), 120),
10593
9788
  state: plainFeedStatusLabel(mission.latest),
10594
9789
  engine: mission.runner || engineSeat.engine_id || null,
10595
9790
  model: mission.model || null,
@@ -11177,16 +10372,12 @@ function memberUpTeam(root) {
11177
10372
  // The task `member go <name>` would claim, skipping ones already handed out.
11178
10373
  // Looks only; the child claims it.
11179
10374
  function memberUpPick(root, name, taken) {
11180
- try {
11181
- const taskDb = require('../lib/task-db');
11182
- const { fittingTasks } = require('../lib/member-task-pick');
11183
- const db = taskDb.open();
11184
- const job = memberGoJob(name, root);
11185
- return fittingTasks(taskDb, db, { workspaceRoot: taskDb.workspaceRoot(root), member: name, job, requireCheck: true, recoverClaimed: true })
11186
- .find((task) => !taken.has(task.id)) || null;
11187
- } catch {
11188
- return null;
11189
- }
10375
+ const taskDb = require('../lib/task-db');
10376
+ const { fittingTasks } = require('../lib/member-task-pick');
10377
+ const db = taskDb.open();
10378
+ const job = memberGoJob(name, root);
10379
+ return fittingTasks(taskDb, db, { workspaceRoot: taskDb.workspaceRoot(root), member: name, job, requireCheck: true, recoverClaimed: true })
10380
+ .find((task) => !taken.has(task.id)) || null;
11190
10381
  }
11191
10382
 
11192
10383
  // An empty job list: write the member's next job from its own mission.
@@ -11205,16 +10396,12 @@ function memberUpWriteNextJob(root, name, { dryRun = false } = {}) {
11205
10396
 
11206
10397
  // Every open task a member could start, with the job it needs.
11207
10398
  function memberUpWaiting(root) {
11208
- try {
11209
- const taskDb = require('../lib/task-db');
11210
- const { fittingTasks } = require('../lib/member-task-pick');
11211
- const db = taskDb.open();
11212
- const workspaceRoot = taskDb.workspaceRoot(root);
11213
- return ['build', 'review', 'search'].flatMap((job) => fittingTasks(taskDb, db, { workspaceRoot, job, requireCheck: true })
11214
- .map((task) => ({ id: task.id, title: task.title, job })));
11215
- } catch {
11216
- return [];
11217
- }
10399
+ const taskDb = require('../lib/task-db');
10400
+ const { fittingTasks } = require('../lib/member-task-pick');
10401
+ const db = taskDb.open();
10402
+ const workspaceRoot = taskDb.workspaceRoot(root);
10403
+ return ['build', 'review', 'search'].flatMap((job) => fittingTasks(taskDb, db, { workspaceRoot, job, requireCheck: true })
10404
+ .map((task) => ({ id: task.id, title: task.title, job })));
11218
10405
  }
11219
10406
 
11220
10407
  function memberUpSpawn(root, name, onExit, onClaim) {
@@ -11250,7 +10437,9 @@ function memberUpSpawn(root, name, onExit, onClaim) {
11250
10437
  const buf = Buffer.alloc(Math.max(0, size - from));
11251
10438
  const read = fs.readSync(fd, buf, 0, buf.length, from);
11252
10439
  output = buf.subarray(0, read).toString('utf8');
11253
- } catch {} finally {
10440
+ } catch (error) {
10441
+ console.error(`[member-up] could not read ${name}'s run log:`, error);
10442
+ } finally {
11254
10443
  if (fd !== undefined) fs.closeSync(fd);
11255
10444
  }
11256
10445
  const { memberUpResult } = require('../lib/member-up');
@@ -11299,9 +10488,16 @@ async function memberUp(...args) {
11299
10488
  }),
11300
10489
  };
11301
10490
  if (parsed.dryRun) {
11302
- const up = createMemberUp(deps, { max, only, dryRun: true });
10491
+ let failed = false;
10492
+ const up = createMemberUp({ ...deps, reportError: (context, error) => {
10493
+ failed = true;
10494
+ console.error(`[member-up] ${context}:`, error);
10495
+ } }, { max, only, dryRun: true });
11303
10496
  const started = up.check();
11304
- if (!started.length && up.working() >= up.max) {
10497
+ if (failed) {
10498
+ console.error('team preview incomplete because of errors');
10499
+ process.exitCode = 1;
10500
+ } else if (!started.length && up.working() >= up.max) {
11305
10501
  console.log(`nothing to start: ${up.working()} already working, the limit is ${up.max}`);
11306
10502
  } else if (!started.length) {
11307
10503
  console.log('nothing to start: no free member has a task with a runnable check');
@@ -11646,14 +10842,29 @@ function memberHandoff(from, to, ...args) {
11646
10842
  return handoff;
11647
10843
  }
11648
10844
 
10845
+ function isBusyStandupStatus(status) {
10846
+ const s = String(status || '').trim().toLowerCase();
10847
+ return Boolean(s) && s !== 'idle' && s !== 'dormant';
10848
+ }
10849
+
11649
10850
  function buildMemberRoom(root = process.cwd()) {
11650
10851
  const teamDir = path.join(root, 'atris', 'team');
11651
10852
  const coreOrder = ['auto-improver', 'navigator', 'executor', 'validator', 'architect', 'customer-lead', 'researcher'];
11652
10853
  const allSlugs = findAllMembers(teamDir)
11653
10854
  .filter(isFleetMember)
11654
10855
  .map(memberSlugFromRecord);
11655
- const ordered = coreOrder.filter((s) => allSlugs.includes(s));
11656
- const cards = ordered.map((slug) => buildMemberCard(slug, root));
10856
+ const baseOrder = [
10857
+ ...coreOrder.filter((s) => allSlugs.includes(s)),
10858
+ ...allSlugs.filter((s) => !coreOrder.includes(s)),
10859
+ ];
10860
+ const rawCards = baseOrder.map((slug) => buildMemberCard(slug, root));
10861
+ const cardStatus = (c) => (c.roadmap.doing_now ? c.roadmap.doing_now.state : c.verdict.toLowerCase());
10862
+ const cards = [...rawCards].sort((a, b) => {
10863
+ const aBusy = isBusyStandupStatus(cardStatus(a)) ? 0 : 1;
10864
+ const bBusy = isBusyStandupStatus(cardStatus(b)) ? 0 : 1;
10865
+ return aBusy - bBusy;
10866
+ });
10867
+ const ordered = cards.map((c) => c.member);
11657
10868
  const feed = collectMemberWorkFeed(root, { limit: 6 });
11658
10869
 
11659
10870
  const standup = cards.map((c) => {
@@ -11805,22 +11016,36 @@ function standupTableData(standup, cwd = process.cwd()) {
11805
11016
  // (member, role, ovr, engine, status) with each member's found and push
11806
11017
  // lines under it; piped, or when the box cannot fit, the boxes it always had.
11807
11018
  function memberStandupLines(standup, { bordered = false, width = process.stdout.columns, cells = null } = {}) {
11808
- if (bordered && cells && standup.length) {
11809
- const table = require('../lib/gm-bet').teamBox(standup.map((s) => ({ member: s.member, status: s.status })), cells, { width: Number(width) - 2 });
11019
+ const ordered = [
11020
+ ...standup.filter((s) => isBusyStandupStatus(s.status)),
11021
+ ...standup.filter((s) => !isBusyStandupStatus(s.status)),
11022
+ ];
11023
+ if (bordered && cells && ordered.length) {
11024
+ const table = require('../lib/gm-bet').teamBox(ordered.map((s) => ({ member: s.member, status: s.status })), cells, { width: Number(width) - 2 });
11810
11025
  if (table) {
11811
11026
  return [
11812
11027
  ...table.split('\n').map((line) => ` ${line}`),
11813
- ...standup.flatMap((s) => [` ${s.member}`, ` found: ${s.latest_update}`, ` push: ${s.next_push}`]),
11028
+ ...ordered.flatMap((s) => (
11029
+ isBusyStandupStatus(s.status)
11030
+ ? [` ${s.member}`, ` found: ${s.latest_update}`, ` push: ${s.next_push}`]
11031
+ : [` ${s.member} · ${s.status} · engine:${s.engine} · ${s.latest_update}`]
11032
+ )),
11814
11033
  ];
11815
11034
  }
11816
11035
  }
11817
- return standup.flatMap((s, idx) => [
11818
- `+-----------------------------------------------------------------------+`,
11819
- `| ${idx + 1}. ${s.member.toUpperCase()} (${s.role}) · ${s.status} · engine:${s.engine}`,
11820
- `| Found: ${s.latest_update}`,
11821
- `| Push: ${s.next_push}`,
11822
- `+-----------------------------------------------------------------------+`,
11823
- ]);
11036
+ return ordered.flatMap((s, idx) => (
11037
+ isBusyStandupStatus(s.status)
11038
+ ? [
11039
+ `+-----------------------------------------------------------------------+`,
11040
+ `| ${idx + 1}. ${s.member.toUpperCase()} (${s.role}) · ${s.status} · engine:${s.engine}`,
11041
+ `| Found: ${s.latest_update}`,
11042
+ `| Push: ${s.next_push}`,
11043
+ `+-----------------------------------------------------------------------+`,
11044
+ ]
11045
+ : [
11046
+ ` ${idx + 1}. ${s.member} (${s.role}) · ${s.status} · engine:${s.engine} · ${s.latest_update}`,
11047
+ ]
11048
+ ));
11824
11049
  }
11825
11050
 
11826
11051
  async function memberRoom(...args) {
@@ -11843,7 +11068,7 @@ async function memberRoom(...args) {
11843
11068
 
11844
11069
  const handoffRows = room.handoffs.length
11845
11070
  ? room.handoffs.map((h) => ` - ${h.from} -> ${h.to}: ${h.summary} (run: ${h.next_command})`)
11846
- : [' - No pending baton passes right now.'];
11071
+ : [' - No handoffs waiting right now.'];
11847
11072
 
11848
11073
  const oneOnOneRows = room.one_on_ones.length
11849
11074
  ? room.one_on_ones.map((o) => ` - ${o.member}: ${o.reason} -> ${o.command}`)
@@ -11860,13 +11085,13 @@ async function memberRoom(...args) {
11860
11085
  '1. Member-by-Member Standup (detail: `atris member card <name>` | rapid walk: `atris member standup --walk`):',
11861
11086
  ...standupRows,
11862
11087
  '',
11863
- '2. Handoffs & Baton Passes (`atris member handoff <from> <to> "<slice>"`):',
11088
+ '2. Who is handing work to whom (`atris member handoff <from> <to> "<slice>"`):',
11864
11089
  ...handoffRows,
11865
11090
  '',
11866
11091
  '3. Your 1-on-1s Today (`atris member 1on1 <name>`):',
11867
11092
  ...oneOnOneRows,
11868
11093
  '',
11869
- 'Live Consequence Feed:',
11094
+ 'What happened lately:',
11870
11095
  ...feedRows,
11871
11096
  '',
11872
11097
  'Controls: atris member standup --walk | atris member go | atris member "<prompt>" | atris member 1on1 <name> | atris member sleep',
@@ -12192,6 +11417,4 @@ module.exports = {
12192
11417
  memberRunSaveDecision,
12193
11418
  buildMemberSubagentShell,
12194
11419
  pickMemberForIdea,
12195
- appendMemberGoalLog,
12196
- appendProjectLog,
12197
11420
  };