atris 3.79.0 → 3.80.1
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/README.md +6 -0
- package/atris/atris.md +66 -167
- package/atris/skills/security-audit/SKILL.md +77 -0
- package/atris/team/brainstormer/MEMBER.md +2 -2
- package/atris.md +66 -167
- package/bin/atris.js +6 -0
- package/commands/auth.js +2 -2
- package/commands/business.js +44 -1984
- package/commands/computer.js +165 -2412
- package/commands/member.js +189 -966
- package/commands/mission.js +229 -1409
- package/commands/pack.js +112 -1476
- package/commands/security-review.js +129 -0
- package/commands/site-deploy.js +5 -1
- package/commands/task.js +137 -1359
- package/lib/business-core.js +2022 -0
- package/lib/computer-core.js +2447 -0
- package/lib/engine-ask.js +1 -0
- package/lib/handoff-wake.js +8 -6
- package/lib/known-commands.js +1 -1
- package/lib/member-autoland.js +21 -2
- package/lib/member-board.js +12 -10
- package/lib/member-core.js +1014 -0
- package/lib/member-up.js +46 -3
- package/lib/mission-core.js +1559 -0
- package/lib/pack-core.js +1588 -0
- package/lib/security-audit-classes.js +318 -0
- package/lib/security-audit.js +1549 -0
- package/lib/task-core.js +1412 -0
- package/lib/task-decision.js +66 -0
- package/lib/task-explanation.js +20 -0
- package/lib/yes-lock.js +1 -1
- package/package.json +9 -2
- package/utils/auth.js +18 -4
package/commands/member.js
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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:
|
|
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(
|
|
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
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
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
|
-
|
|
11209
|
-
|
|
11210
|
-
|
|
11211
|
-
|
|
11212
|
-
|
|
11213
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
11656
|
-
|
|
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
|
-
|
|
11809
|
-
|
|
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
|
-
...
|
|
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
|
|
11818
|
-
|
|
11819
|
-
|
|
11820
|
-
|
|
11821
|
-
|
|
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
|
|
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.
|
|
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
|
-
'
|
|
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
|
};
|