atris 3.70.0 → 3.71.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.
- package/README.md +18 -0
- package/atris/skills/README.md +1 -0
- package/atris/skills/campaigns/SKILL.md +81 -0
- package/atris/skills/meta/SKILL.md +52 -8
- package/ax +226 -42
- package/bin/atris.js +26 -6
- package/commands/autoland.js +176 -7
- package/commands/autopilot.js +71 -11
- package/commands/campaign.js +759 -0
- package/commands/compile.js +19 -8
- package/commands/doctor.js +58 -0
- package/commands/dream.js +18 -3
- package/commands/engine.js +147 -5
- package/commands/improve.js +119 -9
- package/commands/init.js +18 -1
- package/commands/interview.js +15 -3
- package/commands/land.js +22 -6
- package/commands/link.js +1 -0
- package/commands/member.js +359 -94
- package/commands/mission.js +249 -36
- package/commands/report.js +42 -8
- package/commands/run.js +9 -2
- package/commands/scout.js +9 -1
- package/commands/task.js +71 -17
- package/commands/team.js +212 -4
- package/commands/unknowns.js +15 -1
- package/commands/verify.js +46 -0
- package/commands/worktree.js +108 -15
- package/commands/write.js +12 -1
- package/commands/youtube.js +17 -2
- package/lib/accept-verify-gate.js +1 -0
- package/lib/auto-accept-certified.js +1 -0
- package/lib/autoland.js +13 -4
- package/lib/campaign-cadence.js +131 -0
- package/lib/campaign-core.js +330 -0
- package/lib/campaign-holdout.js +65 -0
- package/lib/campaign-loop.js +242 -0
- package/lib/campaign-missions.js +237 -0
- package/lib/campaign-new.js +251 -0
- package/lib/campaign-protect.js +75 -0
- package/lib/campaign-reading.js +147 -0
- package/lib/campaign-schema.js +196 -0
- package/lib/campaign-store.js +380 -0
- package/lib/campaign-sync.js +302 -0
- package/lib/campaign-trace.js +300 -0
- package/lib/conductor-artifacts.js +15 -1
- package/lib/daily-log.js +11 -1
- package/lib/dispatch-scout.js +18 -0
- package/lib/engine-ask.js +33 -3
- package/lib/engine-registry.js +2 -0
- package/lib/engine-resource.js +518 -0
- package/lib/fleet.js +125 -16
- package/lib/init-commit.js +80 -0
- package/lib/intents.js +8 -0
- package/lib/known-commands.js +1 -1
- package/lib/land-green.js +58 -7
- package/lib/landing-proof.js +352 -0
- package/lib/member-autoland.js +395 -18
- package/lib/member-board.js +7 -1
- package/lib/member-map-fold.js +126 -0
- package/lib/member-map-inbox.js +80 -0
- package/lib/member-progress.js +48 -1
- package/lib/member-report.js +127 -8
- package/lib/member-self-next-job.js +97 -0
- package/lib/member-task-pick.js +41 -14
- package/lib/member-up.js +249 -16
- package/lib/roster-prep.js +25 -2
- package/lib/runner-command.js +5 -0
- package/lib/task-db.js +47 -11
- package/lib/task-explanation.js +7 -0
- package/lib/task-list-keeper.js +8 -1
- package/lib/task-receipt.js +6 -1
- package/mcp/atris-mcp/index.mjs +212 -10
- package/package.json +2 -1
- package/scripts/det/engine-reading.js +37 -0
- package/scripts/det/ytnotes +29 -5
package/commands/member.js
CHANGED
|
@@ -566,7 +566,16 @@ function memberLatestMission(name, missionMap = memberRunMissionMap()) {
|
|
|
566
566
|
|
|
567
567
|
function memberLiveMission(name, missionMap = memberRunMissionMap()) {
|
|
568
568
|
return memberOwnedMissions(name, missionMap)
|
|
569
|
-
.find((mission) =>
|
|
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;
|
|
570
579
|
}
|
|
571
580
|
|
|
572
581
|
function missionShortName(mission, max = 36) {
|
|
@@ -922,7 +931,10 @@ function memberRunDefaultWallSeconds(name, cwd = process.cwd()) {
|
|
|
922
931
|
const picked = require('../lib/member-engine').memberRosterEngine(name, cwd);
|
|
923
932
|
cap = Number(picked && picked.engine && picked.engine.roster_max_seconds) || 0;
|
|
924
933
|
} catch {}
|
|
925
|
-
|
|
934
|
+
// No roster cap means the tick runs on the default tick cap, so the run
|
|
935
|
+
// needs that much plus review time, or the wall kills it first.
|
|
936
|
+
const tickSeconds = cap || require('./mission').missionTickTimeoutMs({}, Infinity) / 1000;
|
|
937
|
+
return Math.max(900, tickSeconds + 600);
|
|
926
938
|
}
|
|
927
939
|
|
|
928
940
|
function pushFlagValue(out, args, name) {
|
|
@@ -1254,7 +1266,7 @@ function reviewRefusalLine(refusal) {
|
|
|
1254
1266
|
return line.trim().slice(0, 200);
|
|
1255
1267
|
}
|
|
1256
1268
|
|
|
1257
|
-
function memberRunWith(name, args, { quiet = false } = {}) {
|
|
1269
|
+
function memberRunWith(name, args, { quiet = false, closeTaskIds = [] } = {}) {
|
|
1258
1270
|
if (!name || name === '--help' || name === '-h' || hasFlag(args, '--help') || hasFlag(args, '-h')) {
|
|
1259
1271
|
console.log('usage: atris member run <name> ["mission text"] [--verify "<cmd>"] [--minutes N|--hours N] [--json]');
|
|
1260
1272
|
console.log('pick for me: atris member run "fix the flaky login test" (the best member takes it and runs it)');
|
|
@@ -1333,6 +1345,7 @@ function memberRunWith(name, args, { quiet = false } = {}) {
|
|
|
1333
1345
|
console.log(`watch: tail -f ${progressDir}/${String(name).replace(/[^A-Za-z0-9._-]/g, '_')}.log`);
|
|
1334
1346
|
console.log('board: atris member board --watch');
|
|
1335
1347
|
console.log('stop: atris member stop <name>');
|
|
1348
|
+
console.log(`where work goes: ${require('../lib/member-autoland').landingDestination(mission.worktree?.path || process.cwd())}`);
|
|
1336
1349
|
}
|
|
1337
1350
|
|
|
1338
1351
|
const cwd = mission.worktree?.path || process.cwd();
|
|
@@ -1406,7 +1419,7 @@ function memberRunWith(name, args, { quiet = false } = {}) {
|
|
|
1406
1419
|
const summary = ticks.map((tick) => tick?.summary || tick?.claude?.summary).filter(Boolean).pop() || '';
|
|
1407
1420
|
const workerEngine = ticks.map((tick) => tick?.engine_id).filter(Boolean).pop()
|
|
1408
1421
|
|| runnerSwap?.to || readFlag(args, '--runner', '') || '';
|
|
1409
|
-
result.autoland = require('../lib/member-autoland').autolandMemberRun({ mission: finalMission, cwd, summary, workerEngine });
|
|
1422
|
+
result.autoland = require('../lib/member-autoland').autolandMemberRun({ mission: finalMission, cwd, summary, workerEngine, closeTaskIds });
|
|
1410
1423
|
if (!asJson && !quiet) console.log(result.autoland.line);
|
|
1411
1424
|
}
|
|
1412
1425
|
if (checkFailed) {
|
|
@@ -3343,6 +3356,19 @@ function dateFromLogPath(relativePath) {
|
|
|
3343
3356
|
return match ? match[1] : null;
|
|
3344
3357
|
}
|
|
3345
3358
|
|
|
3359
|
+
// Mission driver logs, goals files, and the wiki log have no date in their
|
|
3360
|
+
// name; without one they skipped the 7-day window and months-old lines kept
|
|
3361
|
+
// counting as this week's pain. The last write date stands in for them.
|
|
3362
|
+
function logDateForScan(relativePath, filePath) {
|
|
3363
|
+
const named = dateFromLogPath(relativePath);
|
|
3364
|
+
if (named) return named;
|
|
3365
|
+
try {
|
|
3366
|
+
return fs.statSync(filePath).mtime.toISOString().slice(0, 10);
|
|
3367
|
+
} catch {
|
|
3368
|
+
return null;
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3346
3372
|
function compareLogEvidenceRecentFirst(a, b) {
|
|
3347
3373
|
const dateA = a?.date || '';
|
|
3348
3374
|
const dateB = b?.date || '';
|
|
@@ -3407,7 +3433,11 @@ function isAutoImproverGeneratedLogLine(line, relativePath = '') {
|
|
|
3407
3433
|
if (/^[-*]?\s*Next tick will verify failed, halting\.?$/i.test(text)) return true;
|
|
3408
3434
|
if (/^##\s+\d{1,2}:\d{2}\s+·\s+Task failed$/i.test(text)) return true;
|
|
3409
3435
|
if (/^[-*]?\s*(status|action|state|verifier)\s*:\s*(failed|blocked)\s*$/i.test(text)) return true;
|
|
3410
|
-
|
|
3436
|
+
// Mission lines state a goal; their words ("leftover error text", "done failed") are not failures.
|
|
3437
|
+
// Goal files say the same in JSON ("stop_rule": "Stop if ... risk is unclear").
|
|
3438
|
+
if (/^[-*]?\s*"?(title|proof|proof_target|mission|objective|acceptance|stop_rule)"?\s*:\s*/i.test(text)) return true;
|
|
3439
|
+
// Driver logs print "error": null on every clean run.
|
|
3440
|
+
if (/^"?(error|failure)s?"?\s*:\s*(null|false|""|\[\]|\{\})\s*,?$/i.test(text)) return true;
|
|
3411
3441
|
if (String(relativePath || '').startsWith('atris/team/auto-improver/logs/') && /^-\s*summary:\s*/i.test(text)) return true;
|
|
3412
3442
|
if (/\bauto[- ]improver\b/i.test(text) && /\b(dogfood|receipt|prevented|pain_)\b/i.test(text)) return true;
|
|
3413
3443
|
if (/\bauto_improver\b/i.test(text)) return true;
|
|
@@ -3425,7 +3455,9 @@ function normalizeFailurePattern(line) {
|
|
|
3425
3455
|
.trim());
|
|
3426
3456
|
}
|
|
3427
3457
|
|
|
3428
|
-
|
|
3458
|
+
// Callers scanning many lines pass passLessonLower so the lessons file is
|
|
3459
|
+
// lowercased once per scan, not once per log line.
|
|
3460
|
+
function failureCoveredByPassLesson(line, passLessonText = '', passLessonLower = passLessonText.toLowerCase()) {
|
|
3429
3461
|
const text = String(line || '');
|
|
3430
3462
|
if (!text || !passLessonText) return false;
|
|
3431
3463
|
if (/spawnSync\s+\/bin\/sh\s+ETIMEDOUT/i.test(text)) {
|
|
@@ -3441,23 +3473,59 @@ function failureCoveredByPassLesson(line, passLessonText = '') {
|
|
|
3441
3473
|
const bare = pattern.replace(/^#+\s*[#:.]*\s*·\s*/, '').trim();
|
|
3442
3474
|
for (const candidate of [pattern, bare]) {
|
|
3443
3475
|
if (candidate && candidate.length >= 12
|
|
3444
|
-
&&
|
|
3476
|
+
&& passLessonLower.includes(candidate.toLowerCase())) {
|
|
3445
3477
|
return true;
|
|
3446
3478
|
}
|
|
3447
3479
|
}
|
|
3448
3480
|
return false;
|
|
3449
3481
|
}
|
|
3450
3482
|
|
|
3483
|
+
// Runtime logs the repeat-failure scan reads, listed by where they live
|
|
3484
|
+
// instead of one capped walk of atris/team. That folder holds hundreds of
|
|
3485
|
+
// skill and mission files, and a 220-file walk ran out before it reached
|
|
3486
|
+
// most members' logs, so this week's repeats counted as zero. Listing paths
|
|
3487
|
+
// is cheap; the 7-day filter below decides what actually gets read.
|
|
3488
|
+
function listAutoImproverRuntimeLogs(root) {
|
|
3489
|
+
const listAll = (dir) => (fs.existsSync(dir) ? listFilesBounded(dir, { maxFiles: 5000 }) : []);
|
|
3490
|
+
const childDirs = (dir) => {
|
|
3491
|
+
try {
|
|
3492
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
3493
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
3494
|
+
.map((entry) => entry.name)
|
|
3495
|
+
.sort();
|
|
3496
|
+
} catch {
|
|
3497
|
+
return [];
|
|
3498
|
+
}
|
|
3499
|
+
};
|
|
3500
|
+
const files = [];
|
|
3501
|
+
const logsRoot = path.join(root, 'atris', 'logs');
|
|
3502
|
+
try {
|
|
3503
|
+
for (const entry of fs.readdirSync(logsRoot, { withFileTypes: true })) {
|
|
3504
|
+
if (entry.isFile()) files.push(path.join(logsRoot, entry.name));
|
|
3505
|
+
}
|
|
3506
|
+
} catch {}
|
|
3507
|
+
for (const name of childDirs(logsRoot)) {
|
|
3508
|
+
if (name !== 'archive') files.push(...listAll(path.join(logsRoot, name)));
|
|
3509
|
+
}
|
|
3510
|
+
const teamRoot = path.join(root, 'atris', 'team');
|
|
3511
|
+
for (const member of childDirs(teamRoot)) {
|
|
3512
|
+
for (const name of ['goals.md', 'goals.json']) {
|
|
3513
|
+
const goalsPath = path.join(teamRoot, member, name);
|
|
3514
|
+
if (fs.existsSync(goalsPath)) files.push(goalsPath);
|
|
3515
|
+
}
|
|
3516
|
+
files.push(...listAll(path.join(teamRoot, member, 'logs')));
|
|
3517
|
+
}
|
|
3518
|
+
const wikiLog = path.join(root, 'atris', 'wiki', 'log.md');
|
|
3519
|
+
if (fs.existsSync(wikiLog)) files.push(wikiLog);
|
|
3520
|
+
return files;
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3451
3523
|
function collectAutoImproverLogSignals(root) {
|
|
3452
|
-
const roots = [
|
|
3453
|
-
path.join(root, 'atris', 'logs'),
|
|
3454
|
-
path.join(root, 'atris', 'team'),
|
|
3455
|
-
path.join(root, 'atris', 'wiki'),
|
|
3456
|
-
].filter((candidate) => fs.existsSync(candidate));
|
|
3457
3524
|
const passLessonText = safeReadText(path.join(root, 'atris', 'lessons.md'), 500000)
|
|
3458
3525
|
.split(/\r?\n/)
|
|
3459
3526
|
.filter((line) => /\s+pass\s*[-\u2014:,]/.test(line))
|
|
3460
3527
|
.join('\n');
|
|
3528
|
+
const passLessonLower = passLessonText.toLowerCase();
|
|
3461
3529
|
const failureRegex = /\b(error|failed|failure|blocked|timeout|regression|crash|missing proof|naraka|suffering)\b/i;
|
|
3462
3530
|
const unclearRegex = /\b(tbd|unclear|unknown|needs user|needs owner|needs proof|no next|blocked)\b/i;
|
|
3463
3531
|
const counts = new Map();
|
|
@@ -3465,66 +3533,67 @@ function collectAutoImproverLogSignals(root) {
|
|
|
3465
3533
|
let filesScanned = 0;
|
|
3466
3534
|
let linesScanned = 0;
|
|
3467
3535
|
let unclearNextActions = 0;
|
|
3468
|
-
for (const
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
line: sourceLine,
|
|
3512
|
-
text: compactSentence(line, 180),
|
|
3513
|
-
});
|
|
3514
|
-
}
|
|
3515
|
-
if (!failureRegex.test(line)) continue;
|
|
3516
|
-
const pattern = normalizeFailurePattern(line);
|
|
3517
|
-
if (!pattern) continue;
|
|
3518
|
-
const existing = counts.get(pattern) || { pattern, count: 0, evidence: [] };
|
|
3519
|
-
existing.count += 1;
|
|
3520
|
-
existing.evidence.push({
|
|
3536
|
+
for (const filePath of listAutoImproverRuntimeLogs(root)) {
|
|
3537
|
+
const relative = repoRelative(root, filePath);
|
|
3538
|
+
const logDate = logDateForScan(relative, filePath);
|
|
3539
|
+
if (relative.startsWith('atris/logs/archive/')) continue;
|
|
3540
|
+
const isRuntimeLog = relative.startsWith('atris/logs/')
|
|
3541
|
+
|| /^atris\/team\/[^/]+\/logs\//.test(relative)
|
|
3542
|
+
|| /^atris\/team\/[^/]+\/goals\.(md|json)$/.test(relative)
|
|
3543
|
+
|| relative === 'atris/wiki/log.md';
|
|
3544
|
+
if (!isRuntimeLog) continue;
|
|
3545
|
+
// Only scan logs from the last 7 days. Historical log lines keep
|
|
3546
|
+
// triggering false-positive findings long after their root cause is
|
|
3547
|
+
// fixed (e.g. "goal-from-mission blocked" from July when MISSION.md
|
|
3548
|
+
// was repaired in August). Old evidence stays on disk for audits;
|
|
3549
|
+
// it just stops feeding the pain score, so pain can actually drop.
|
|
3550
|
+
if (logDate && isRuntimeLog) {
|
|
3551
|
+
const logAgeDays = (Date.now() - new Date(logDate + 'T12:00:00').getTime()) / 864e5;
|
|
3552
|
+
if (logAgeDays > 7) continue;
|
|
3553
|
+
}
|
|
3554
|
+
const text = safeReadText(filePath);
|
|
3555
|
+
if (!text) continue;
|
|
3556
|
+
filesScanned += 1;
|
|
3557
|
+
const allLines = text.split(/\r?\n/);
|
|
3558
|
+
const lineOffset = Math.max(0, allLines.length - 500);
|
|
3559
|
+
const lines = allLines.slice(-500);
|
|
3560
|
+
linesScanned += lines.length;
|
|
3561
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
3562
|
+
const sourceLine = lineOffset + index + 1;
|
|
3563
|
+
const line = lines[index];
|
|
3564
|
+
if (isAutoImproverGeneratedLogLine(line, relative)) continue;
|
|
3565
|
+
// Declared verification receipts ("check: <command> ...") describe what
|
|
3566
|
+
// was verified, not what broke. Wiki upkeep sweeps write one per page,
|
|
3567
|
+
// so counting them as failures spawns bogus recurring-pattern tasks
|
|
3568
|
+
// (CLI-199 came from 13 such lines in atris/wiki/log.md).
|
|
3569
|
+
if (/^\s*[-*]?\s*check:\s/i.test(line)) continue;
|
|
3570
|
+
if (/\b(errors?|fail(?:ed|ures?)|blocked|timeouts?)\s*:\s*0\b/i.test(line)) continue;
|
|
3571
|
+
if (/\bblocked\s*(?:->|→|to)\s*ready\b/i.test(line)) continue;
|
|
3572
|
+
if (failureCoveredByPassLesson(line, passLessonText, passLessonLower)) continue;
|
|
3573
|
+
// A file name is not a next step ("receipt: atris/runs/...-unknown-word-...json"),
|
|
3574
|
+
// and "learned:" / "why:" lines explain the past, so neither counts as unclear.
|
|
3575
|
+
const unclearText = line.replace(/[\w.@~-]*\/[\w./@~-]*\.\w{1,5}\b/g, ' ');
|
|
3576
|
+
if (!/^\s*[-*]?\s*(learned|why)\s*:/i.test(line) && unclearRegex.test(unclearText)) {
|
|
3577
|
+
unclearNextActions += 1;
|
|
3578
|
+
unclearActions.push({
|
|
3521
3579
|
path: relative,
|
|
3522
3580
|
date: logDate,
|
|
3523
3581
|
line: sourceLine,
|
|
3524
3582
|
text: compactSentence(line, 180),
|
|
3525
3583
|
});
|
|
3526
|
-
counts.set(pattern, existing);
|
|
3527
3584
|
}
|
|
3585
|
+
if (!failureRegex.test(line)) continue;
|
|
3586
|
+
const pattern = normalizeFailurePattern(line);
|
|
3587
|
+
if (!pattern) continue;
|
|
3588
|
+
const existing = counts.get(pattern) || { pattern, count: 0, evidence: [] };
|
|
3589
|
+
existing.count += 1;
|
|
3590
|
+
existing.evidence.push({
|
|
3591
|
+
path: relative,
|
|
3592
|
+
date: logDate,
|
|
3593
|
+
line: sourceLine,
|
|
3594
|
+
text: compactSentence(line, 180),
|
|
3595
|
+
});
|
|
3596
|
+
counts.set(pattern, existing);
|
|
3528
3597
|
}
|
|
3529
3598
|
}
|
|
3530
3599
|
const repeated = [...counts.values()]
|
|
@@ -9008,6 +9077,26 @@ async function memberLoopWakeTick(name, run, index, tickStartedAt) {
|
|
|
9008
9077
|
const MEMBER_LOOP_IDLE_BREAK_THRESHOLD = 2;
|
|
9009
9078
|
const MEMBER_LOOP_IDENTICAL_ASK_BREAK_THRESHOLD = 2;
|
|
9010
9079
|
|
|
9080
|
+
// A loop kept running for days would keep old code after an atris upgrade. Read the
|
|
9081
|
+
// installed version from disk, not the cached require, so a new install is seen.
|
|
9082
|
+
function memberLoopInstalledVersion() {
|
|
9083
|
+
try {
|
|
9084
|
+
return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version || null;
|
|
9085
|
+
} catch {
|
|
9086
|
+
return null;
|
|
9087
|
+
}
|
|
9088
|
+
}
|
|
9089
|
+
|
|
9090
|
+
// Start the same loop again on the new code. The lease is already released, so the
|
|
9091
|
+
// fresh process takes it over; this one exits with the fresh run's status.
|
|
9092
|
+
function memberLoopRestartOnNewVersion(upgrade) {
|
|
9093
|
+
const child = spawnSync(process.execPath, process.argv.slice(1), {
|
|
9094
|
+
stdio: 'inherit',
|
|
9095
|
+
env: { ...process.env, ATRIS_MEMBER_LOOP_RESTARTED_FROM: upgrade.from },
|
|
9096
|
+
});
|
|
9097
|
+
process.exit(child.status == null ? 1 : child.status);
|
|
9098
|
+
}
|
|
9099
|
+
|
|
9011
9100
|
// Loop phase: control. Hold the lease, honor the stop file, pace ticks by interval, and
|
|
9012
9101
|
// break early when the member is idling instead of faking activity.
|
|
9013
9102
|
async function memberLoopRunTicks(name, paths, lease, run) {
|
|
@@ -9018,6 +9107,13 @@ async function memberLoopRunTicks(name, paths, lease, run) {
|
|
|
9018
9107
|
state.stopped = true;
|
|
9019
9108
|
break;
|
|
9020
9109
|
}
|
|
9110
|
+
if (index > 0 && plan.forever && state.startVersion) {
|
|
9111
|
+
const installed = memberLoopInstalledVersion();
|
|
9112
|
+
if (installed && installed !== state.startVersion) {
|
|
9113
|
+
state.upgrade = { from: state.startVersion, to: installed };
|
|
9114
|
+
break;
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
9021
9117
|
refreshMemberLoopLease(paths, lease.lease, plan.ttlMs);
|
|
9022
9118
|
const tickStartedAt = stampIso();
|
|
9023
9119
|
try {
|
|
@@ -9071,7 +9167,7 @@ function memberLoopRecordResult(name, paths, lease, run) {
|
|
|
9071
9167
|
const { options, plan, state } = run;
|
|
9072
9168
|
const { execute, aliveMode, asJson } = options;
|
|
9073
9169
|
const { hourly, forever, runId, ticks, intervalMs, durationMs, startedAt } = plan;
|
|
9074
|
-
const { tickResults, decisions, stopped, failed, earlyExit, tickLogPath } = state;
|
|
9170
|
+
const { tickResults, decisions, stopped, failed, earlyExit, tickLogPath, upgrade } = state;
|
|
9075
9171
|
const finishedAt = stampIso();
|
|
9076
9172
|
const plannedOnly = aliveMode && tickResults.length > 0 && tickResults.every((tick) => tick.decision === 'planned');
|
|
9077
9173
|
const summary = {
|
|
@@ -9080,7 +9176,7 @@ function memberLoopRecordResult(name, paths, lease, run) {
|
|
|
9080
9176
|
schema: aliveMode ? 'atris.member_alive.v1' : 'atris.member_loop.v1',
|
|
9081
9177
|
member: name,
|
|
9082
9178
|
alive: aliveMode,
|
|
9083
|
-
status: failed ? 'failed' : stopped ? 'stopped' : plannedOnly ? 'planned' : earlyExit ? (earlyExit.blocked_on_human ? 'blocked_on_human' : 'idle') : 'completed',
|
|
9179
|
+
status: failed ? 'failed' : stopped ? 'stopped' : upgrade ? 'restarting_on_new_version' : plannedOnly ? 'planned' : earlyExit ? (earlyExit.blocked_on_human ? 'blocked_on_human' : 'idle') : 'completed',
|
|
9084
9180
|
mode: execute ? 'execute' : 'dry_run',
|
|
9085
9181
|
cadence: hourly ? 'hourly' : 'custom',
|
|
9086
9182
|
forever,
|
|
@@ -9096,6 +9192,8 @@ function memberLoopRecordResult(name, paths, lease, run) {
|
|
|
9096
9192
|
duration_ms_actual: Date.parse(finishedAt) - Date.parse(startedAt),
|
|
9097
9193
|
decisions,
|
|
9098
9194
|
early_exit: earlyExit,
|
|
9195
|
+
upgrade: upgrade || null,
|
|
9196
|
+
restarted_from_version: process.env.ATRIS_MEMBER_LOOP_RESTARTED_FROM || null,
|
|
9099
9197
|
stop: earlyExit?.stop || null,
|
|
9100
9198
|
blocked_on_human: earlyExit?.blocked_on_human === true,
|
|
9101
9199
|
needs_user: earlyExit?.needs_user === true,
|
|
@@ -9120,6 +9218,7 @@ function memberLoopRecordResult(name, paths, lease, run) {
|
|
|
9120
9218
|
`Status: ${payload.status}`,
|
|
9121
9219
|
`Cadence: ${payload.cadence}${payload.forever ? ' forever' : ''}`,
|
|
9122
9220
|
`Ticks: ${payload.ticks}/${payload.ticks_requested}`,
|
|
9221
|
+
...(upgrade ? [`Upgrade: atris ${upgrade.from} -> ${upgrade.to}, restarting on the new version`] : []),
|
|
9123
9222
|
`Decisions: ${Object.entries(decisions).map(([key, count]) => `${key} x${count}`).join(', ') || 'none'}`,
|
|
9124
9223
|
`Stop: ${payload.stop_command}`,
|
|
9125
9224
|
`Receipt: ${path.relative(process.cwd(), receiptPath)}`,
|
|
@@ -9185,11 +9284,14 @@ async function memberLoop(name, ...args) {
|
|
|
9185
9284
|
lastAskKey: null,
|
|
9186
9285
|
consecutiveIdenticalAsk: 0,
|
|
9187
9286
|
tickLogPath,
|
|
9287
|
+
startVersion: memberLoopInstalledVersion(),
|
|
9288
|
+
upgrade: null,
|
|
9188
9289
|
};
|
|
9189
9290
|
|
|
9190
9291
|
const run = { options, plan, state };
|
|
9191
9292
|
await memberLoopRunTicks(name, paths, lease, run);
|
|
9192
9293
|
memberLoopRecordResult(name, paths, lease, run);
|
|
9294
|
+
if (state.upgrade) memberLoopRestartOnNewVersion(state.upgrade);
|
|
9193
9295
|
}
|
|
9194
9296
|
|
|
9195
9297
|
async function memberTick(name, ...args) {
|
|
@@ -9856,10 +9958,26 @@ const EXPERIMENT_STATUSES = new Set(['experimental', 'one-off']);
|
|
|
9856
9958
|
|
|
9857
9959
|
function ideaRuleFor(slug, record, text) {
|
|
9858
9960
|
const own = IDEA_ARCHETYPE_RULES.find((rule) => rule.slugs.includes(slug));
|
|
9859
|
-
if (own && own.pattern.test(text)) return own;
|
|
9961
|
+
if (own && own.pattern.test(text)) return { rule: own, own: true };
|
|
9860
9962
|
const { autoJobForMember } = require('../lib/member-engine');
|
|
9861
9963
|
const archetype = ARCHETYPE_FOR_JOB[autoJobForMember(slug, record.role)];
|
|
9862
|
-
|
|
9964
|
+
const rule = IDEA_ARCHETYPE_RULES.find((r) => r.slugs.includes(archetype)) || null;
|
|
9965
|
+
return { rule, own: Boolean(own && rule === own) };
|
|
9966
|
+
}
|
|
9967
|
+
|
|
9968
|
+
// The request's job is the first rule it matches; the list runs from
|
|
9969
|
+
// specific jobs to the general build job, so "where is the code" is a
|
|
9970
|
+
// navigator job even though "code" is also a build word.
|
|
9971
|
+
function ideaJobRule(text) {
|
|
9972
|
+
return IDEA_ARCHETYPE_RULES.find((rule) => rule.pattern.test(text)) || null;
|
|
9973
|
+
}
|
|
9974
|
+
|
|
9975
|
+
// 3 = named in the idea, 2 = the job is the member's own, 1 = the member
|
|
9976
|
+
// falls back to the job, 0 = not its job. Fit outranks shared words.
|
|
9977
|
+
function ideaJobFit(named, memberRule, jobRule) {
|
|
9978
|
+
if (named) return 3;
|
|
9979
|
+
if (!jobRule || !memberRule.rule || memberRule.rule !== jobRule) return 0;
|
|
9980
|
+
return memberRule.own ? 2 : 1;
|
|
9863
9981
|
}
|
|
9864
9982
|
|
|
9865
9983
|
// 0 = normal, 1 = experiment or one-off, 2 = parked, human, or odd-typed.
|
|
@@ -9871,10 +9989,12 @@ function dispatchTier(record) {
|
|
|
9871
9989
|
return 0;
|
|
9872
9990
|
}
|
|
9873
9991
|
|
|
9874
|
-
// Lower tier wins first, then higher score, then name order
|
|
9992
|
+
// Lower tier wins first, then job fit, then higher score, then name order
|
|
9993
|
+
// for a stable pick.
|
|
9875
9994
|
function outranksDispatch(candidate, best) {
|
|
9876
9995
|
if (!best) return true;
|
|
9877
9996
|
if (candidate.tier !== best.tier) return candidate.tier < best.tier;
|
|
9997
|
+
if (candidate.fit !== best.fit) return candidate.fit > best.fit;
|
|
9878
9998
|
if (candidate.score !== best.score) return candidate.score > best.score;
|
|
9879
9999
|
return candidate.member.localeCompare(best.member) < 0;
|
|
9880
10000
|
}
|
|
@@ -9908,6 +10028,7 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
|
|
|
9908
10028
|
.toLowerCase()
|
|
9909
10029
|
.split(/[^a-z0-9]+/)
|
|
9910
10030
|
.filter((w) => w.length >= 4);
|
|
10031
|
+
const jobRule = ideaJobRule(text);
|
|
9911
10032
|
|
|
9912
10033
|
let best = null;
|
|
9913
10034
|
for (const record of members) {
|
|
@@ -9919,10 +10040,11 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
|
|
|
9919
10040
|
let score = 0;
|
|
9920
10041
|
let matchedReason = '';
|
|
9921
10042
|
|
|
9922
|
-
const
|
|
9923
|
-
|
|
10043
|
+
const memberRule = ideaRuleFor(slug, record, text);
|
|
10044
|
+
const fit = ideaJobFit(named, memberRule, jobRule);
|
|
10045
|
+
if (fit > 0 && memberRule.rule) {
|
|
9924
10046
|
score += 30;
|
|
9925
|
-
matchedReason = rule.reason;
|
|
10047
|
+
matchedReason = memberRule.rule.reason;
|
|
9926
10048
|
}
|
|
9927
10049
|
|
|
9928
10050
|
const missionFile = path.join(teamDir, slug, 'MISSION.md');
|
|
@@ -9953,6 +10075,7 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
|
|
|
9953
10075
|
reason: matchedReason,
|
|
9954
10076
|
score,
|
|
9955
10077
|
tier,
|
|
10078
|
+
fit,
|
|
9956
10079
|
};
|
|
9957
10080
|
if (outranksDispatch(candidate, best)) best = candidate;
|
|
9958
10081
|
}
|
|
@@ -10445,6 +10568,29 @@ function isKnownMemberSlug(token, root = process.cwd()) {
|
|
|
10445
10568
|
|| fs.existsSync(path.join(root, 'atris', 'team', `${clean}.md`));
|
|
10446
10569
|
}
|
|
10447
10570
|
|
|
10571
|
+
const MEMBER_SUBCOMMANDS = [
|
|
10572
|
+
'list', 'roster', 'create', 'install', 'activate', 'upgrade', 'push', 'pull', 'goal',
|
|
10573
|
+
'chat', 'tick', 'wake', 'run', 'result', 'go', 'work', 'up', 'steer', 'sleep', 'rest',
|
|
10574
|
+
'room', 'day', 'standup', 'handoff', 'dispatch', 'feed', 'board', 'stop', 'card', 'ping',
|
|
10575
|
+
'loop', 'alive', 'review', 'block', 'status', 'live', 'heartbeat', 'improvements',
|
|
10576
|
+
'report', 'history', 'supervisor', 'export', 'archive', 'tidy', 'help',
|
|
10577
|
+
];
|
|
10578
|
+
|
|
10579
|
+
// `atris member <word>` where the word is neither a member nor a subcommand.
|
|
10580
|
+
// Points at the nearest name and the team, and exits 1 without running anyone.
|
|
10581
|
+
function memberUnknownWord(word, root = process.cwd()) {
|
|
10582
|
+
const { suggestCommand } = require('../lib/known-commands');
|
|
10583
|
+
let names = [];
|
|
10584
|
+
try { names = findAllMembers(path.join(root, 'atris', 'team')).map((m) => m.name); } catch {}
|
|
10585
|
+
const near = suggestCommand(word, [...MEMBER_SUBCOMMANDS, ...names]);
|
|
10586
|
+
console.log(`No member or command named "${word}", so nothing started.`);
|
|
10587
|
+
if (near) console.log(`Did you mean: atris member ${near}`);
|
|
10588
|
+
if (names.length) console.log(`Your team: ${names.join(', ')}`);
|
|
10589
|
+
console.log('To give a member a job: atris member run "<job in a few words>"');
|
|
10590
|
+
process.exitCode = 1;
|
|
10591
|
+
return { ok: false, reason: 'unknown_word', word, suggestion: near || null };
|
|
10592
|
+
}
|
|
10593
|
+
|
|
10448
10594
|
// The job a member does (build, review or search), read from the same team
|
|
10449
10595
|
// roster that picks its engine.
|
|
10450
10596
|
function memberGoJob(owner, root) {
|
|
@@ -10468,10 +10614,10 @@ function memberGoTask(owner, root, { claim, requireCheck = false }) {
|
|
|
10468
10614
|
const job = memberGoJob(owner, root);
|
|
10469
10615
|
const workspaceRoot = taskDb.workspaceRoot(root);
|
|
10470
10616
|
if (claim) {
|
|
10471
|
-
const got = picker.claimTaskForMember(taskDb, db, { workspaceRoot, member: owner, job, requireCheck });
|
|
10617
|
+
const got = picker.claimTaskForMember(taskDb, db, { workspaceRoot, member: owner, job, requireCheck, recoverClaimed: true });
|
|
10472
10618
|
return got ? { ...got, job } : null;
|
|
10473
10619
|
}
|
|
10474
|
-
const task = picker.pickTaskForMember(taskDb, db, { workspaceRoot, job, requireCheck });
|
|
10620
|
+
const task = picker.pickTaskForMember(taskDb, db, { workspaceRoot, member: owner, job, requireCheck, recoverClaimed: true });
|
|
10475
10621
|
return task ? { task, objective: picker.taskObjective(task), job } : null;
|
|
10476
10622
|
} catch (error) {
|
|
10477
10623
|
console.error(`could not take a task from the list: ${String(error && error.message || error).split('\n')[0]}`);
|
|
@@ -10493,8 +10639,12 @@ function memberGoFinishTask(owner, task, result, thrown = null) {
|
|
|
10493
10639
|
if (!thrown && result && result.started && ['complete', 'ready'].includes(mission.status) && checked) {
|
|
10494
10640
|
const proof = [`[verified] ${owner} ran mission ${result.mission_id} and its check passed.`,
|
|
10495
10641
|
receipt ? `Receipt: ${receipt}` : ''].filter(Boolean).join('\n');
|
|
10496
|
-
|
|
10642
|
+
// The commit comes from the member's own work copy, not the main folder.
|
|
10643
|
+
const workRoot = (result.worktree && result.worktree.path) || (mission.worktree && mission.worktree.path) || undefined;
|
|
10644
|
+
const ready = taskDb.readyTask(db, { id: task.id, actor: owner, proof, workRoot });
|
|
10497
10645
|
if (ready.ready !== false) return { id: task.id, title: task.title, outcome: 'review', receipt: receipt || null };
|
|
10646
|
+
// Its change already merged and the merge line closed the job.
|
|
10647
|
+
if (taskDb.getTask(db, task.id)?.status === 'done') return { id: task.id, title: task.title, outcome: 'done', receipt: receipt || null };
|
|
10498
10648
|
}
|
|
10499
10649
|
const ticks = Array.isArray(result && result.ticks) ? result.ticks : [];
|
|
10500
10650
|
const lastReason = String((ticks[ticks.length - 1] || {}).reason || (ticks[ticks.length - 1] || {}).error || '');
|
|
@@ -10504,10 +10654,19 @@ function memberGoFinishTask(owner, task, result, thrown = null) {
|
|
|
10504
10654
|
: (result && result.error) || (result && result.started ? 'the check did not pass' : 'the run did not start');
|
|
10505
10655
|
const reason = String(why || 'the run did not finish').split(/\r?\n/)[0].trim().slice(0, 200);
|
|
10506
10656
|
try { taskDb.noteTask(db, { id: task.id, actor: owner, content: `Released by ${owner}: ${reason}` }); } catch {}
|
|
10657
|
+
// Work staged on a protected surface waits for a person to review it; a
|
|
10658
|
+
// member picking the task again would only redo it and stop again.
|
|
10659
|
+
let held = false;
|
|
10660
|
+
const forReview = Boolean(!thrown && result && result.needs_review === true);
|
|
10661
|
+
if (forReview) {
|
|
10662
|
+
try {
|
|
10663
|
+
held = taskDb.tagTask(db, { id: task.id, actor: owner, add: ['needs-human'] }).tagged === true;
|
|
10664
|
+
if (held) taskDb.noteTask(db, { id: task.id, actor: owner, content: `Held for a person to review: ${reason}` });
|
|
10665
|
+
} catch {}
|
|
10666
|
+
}
|
|
10507
10667
|
// Third failure in a row: hold the task for a person instead of letting
|
|
10508
10668
|
// members pick it up again forever.
|
|
10509
|
-
|
|
10510
|
-
if (memberGoFailStreak(taskDb, db, task.id) >= MEMBER_GO_STRIKES) {
|
|
10669
|
+
if (!held && memberGoFailStreak(taskDb, db, task.id) >= MEMBER_GO_STRIKES) {
|
|
10511
10670
|
try {
|
|
10512
10671
|
held = taskDb.tagTask(db, { id: task.id, actor: owner, add: ['needs-human'] }).tagged === true;
|
|
10513
10672
|
if (held) taskDb.noteTask(db, { id: task.id, actor: owner,
|
|
@@ -10516,7 +10675,7 @@ function memberGoFinishTask(owner, task, result, thrown = null) {
|
|
|
10516
10675
|
}
|
|
10517
10676
|
const released = taskDb.releaseTask(db, { id: task.id, actor: owner });
|
|
10518
10677
|
const outcome = released.released ? (held ? 'needs_human' : 'released') : 'left_claimed';
|
|
10519
|
-
return { id: task.id, title: task.title, outcome, reason };
|
|
10678
|
+
return { id: task.id, title: task.title, outcome, reason, ...(held && forReview ? { held_for: 'review' } : {}) };
|
|
10520
10679
|
}
|
|
10521
10680
|
|
|
10522
10681
|
const MEMBER_GO_STRIKES = 3;
|
|
@@ -10595,7 +10754,7 @@ async function memberSteerOrDispatchWith({ pullTask = false } = {}, rawArgs = []
|
|
|
10595
10754
|
const missionSummary = memberMissionSummary(owner, missionMap, root);
|
|
10596
10755
|
// A human or odd-typed member never takes a task: it would be claimed and
|
|
10597
10756
|
// then stranded when the run refuses the member.
|
|
10598
|
-
const pulled = pullTask && !prompt && !missionSummary.
|
|
10757
|
+
const pulled = pullTask && !prompt && !missionSummary.live_mission && !memberRefusalFor(targetMember, paths.memberFile)
|
|
10599
10758
|
? memberGoTask(owner, root, { claim: !dryRun, requireCheck: needsCheck })
|
|
10600
10759
|
: null;
|
|
10601
10760
|
if (needsCheck && !pulled) {
|
|
@@ -10610,6 +10769,11 @@ async function memberSteerOrDispatchWith({ pullTask = false } = {}, rawArgs = []
|
|
|
10610
10769
|
|| goalPlane.goal?.title
|
|
10611
10770
|
|| extractMemberNorthStar(paths.memberDir, '');
|
|
10612
10771
|
const pulledTask = pulled ? { id: pulled.task.id, title: pulled.task.title, job: pulled.job } : null;
|
|
10772
|
+
// The supervisor's preview can lose a race. Only the child that claimed
|
|
10773
|
+
// the task may announce which job it actually got.
|
|
10774
|
+
if (needsCheck && pulledTask && !dryRun && process.connected && process.send) {
|
|
10775
|
+
process.send({ type: 'member_task_claimed', member: owner, task: pulledTask }, () => {});
|
|
10776
|
+
}
|
|
10613
10777
|
if (pulled) whyPicked = `${whyPicked} Took the oldest open ${pulled.job} task from the list.`.trim();
|
|
10614
10778
|
const engineSeat = resolveMemberEngineSeat(owner, root);
|
|
10615
10779
|
const { autoQueueMemberNextPush, closeMemberHandoffsForReceiver } = require('./mission');
|
|
@@ -10704,7 +10868,8 @@ async function memberSteerOrDispatchWith({ pullTask = false } = {}, rawArgs = []
|
|
|
10704
10868
|
const runFlags = required ? [...stripKnownFlags(nextFlags, ['--verify'], []), '--verify', required] : nextFlags;
|
|
10705
10869
|
let result = null;
|
|
10706
10870
|
try {
|
|
10707
|
-
|
|
10871
|
+
// The job rides to the merge line so it closes itself when its change merges.
|
|
10872
|
+
result = await memberRunWith(owner, [effectivePrompt, '--ignite', ...runFlags], { quiet: asJson, closeTaskIds: [pulled.task.id] });
|
|
10708
10873
|
} catch (error) {
|
|
10709
10874
|
memberGoFinishTask(owner, pulled.task, null, error);
|
|
10710
10875
|
throw error;
|
|
@@ -10715,6 +10880,8 @@ async function memberSteerOrDispatchWith({ pullTask = false } = {}, rawArgs = []
|
|
|
10715
10880
|
? { task, work_step: { summary: result?.work_step?.summary || null }, error: result?.error || null }
|
|
10716
10881
|
: merged));
|
|
10717
10882
|
else if (task.outcome === 'review') console.log(`Task "${task.title}" is ready for review.`);
|
|
10883
|
+
else if (task.outcome === 'done') console.log(`Task "${task.title}" merged and closed itself.`);
|
|
10884
|
+
else if (task.outcome === 'needs_human' && task.held_for === 'review') console.log(`Task "${task.title}" waits for a person to review it: ${task.reason}`);
|
|
10718
10885
|
else if (task.outcome === 'needs_human') console.log(`Task "${task.title}" failed three times in a row and now waits for a person: ${task.reason}`);
|
|
10719
10886
|
else console.log(`Task "${task.title}" went back on the list: ${task.reason}`);
|
|
10720
10887
|
return merged;
|
|
@@ -10788,14 +10955,42 @@ function memberUpPick(root, name, taken) {
|
|
|
10788
10955
|
const { fittingTasks } = require('../lib/member-task-pick');
|
|
10789
10956
|
const db = taskDb.open();
|
|
10790
10957
|
const job = memberGoJob(name, root);
|
|
10791
|
-
return fittingTasks(taskDb, db, { workspaceRoot: taskDb.workspaceRoot(root), job, requireCheck: true })
|
|
10958
|
+
return fittingTasks(taskDb, db, { workspaceRoot: taskDb.workspaceRoot(root), member: name, job, requireCheck: true, recoverClaimed: true })
|
|
10792
10959
|
.find((task) => !taken.has(task.id)) || null;
|
|
10793
10960
|
} catch {
|
|
10794
10961
|
return null;
|
|
10795
10962
|
}
|
|
10796
10963
|
}
|
|
10797
10964
|
|
|
10798
|
-
|
|
10965
|
+
// An empty job list: write the member's next job from its own mission.
|
|
10966
|
+
function memberUpWriteNextJob(root, name, { dryRun = false } = {}) {
|
|
10967
|
+
const taskDb = require('../lib/task-db');
|
|
10968
|
+
const { writeSelfNextJob } = require('../lib/member-self-next-job');
|
|
10969
|
+
const db = taskDb.open();
|
|
10970
|
+
return writeSelfNextJob(taskDb, db, {
|
|
10971
|
+
root,
|
|
10972
|
+
workspaceRoot: taskDb.workspaceRoot(root),
|
|
10973
|
+
member: name,
|
|
10974
|
+
job: memberGoJob(name, root),
|
|
10975
|
+
dryRun,
|
|
10976
|
+
});
|
|
10977
|
+
}
|
|
10978
|
+
|
|
10979
|
+
// Every open task a member could start, with the job it needs.
|
|
10980
|
+
function memberUpWaiting(root) {
|
|
10981
|
+
try {
|
|
10982
|
+
const taskDb = require('../lib/task-db');
|
|
10983
|
+
const { fittingTasks } = require('../lib/member-task-pick');
|
|
10984
|
+
const db = taskDb.open();
|
|
10985
|
+
const workspaceRoot = taskDb.workspaceRoot(root);
|
|
10986
|
+
return ['build', 'review', 'search'].flatMap((job) => fittingTasks(taskDb, db, { workspaceRoot, job, requireCheck: true })
|
|
10987
|
+
.map((task) => ({ id: task.id, title: task.title, job })));
|
|
10988
|
+
} catch {
|
|
10989
|
+
return [];
|
|
10990
|
+
}
|
|
10991
|
+
}
|
|
10992
|
+
|
|
10993
|
+
function memberUpSpawn(root, name, onExit, onClaim) {
|
|
10799
10994
|
const { spawn } = require('child_process');
|
|
10800
10995
|
const logDir = path.join(root, '.atris', 'state', 'member-up');
|
|
10801
10996
|
fs.mkdirSync(logDir, { recursive: true });
|
|
@@ -10808,12 +11003,15 @@ function memberUpSpawn(root, name, onExit) {
|
|
|
10808
11003
|
cwd: root,
|
|
10809
11004
|
detached: true,
|
|
10810
11005
|
// Children keep their log handle when the foreground supervisor stops.
|
|
10811
|
-
stdio: ['ignore', out, out],
|
|
11006
|
+
stdio: ['ignore', out, out, 'ipc'],
|
|
10812
11007
|
env: process.env,
|
|
10813
11008
|
});
|
|
10814
11009
|
} finally {
|
|
10815
11010
|
fs.closeSync(out);
|
|
10816
11011
|
}
|
|
11012
|
+
child.on('message', (message) => {
|
|
11013
|
+
if (message?.type === 'member_task_claimed' && message.member === name) onClaim?.(message.task);
|
|
11014
|
+
});
|
|
10817
11015
|
child.on('error', (error) => onExit(null, { ok: false, reason: error.message }));
|
|
10818
11016
|
child.on('close', (code, signal) => {
|
|
10819
11017
|
let output = '';
|
|
@@ -10838,7 +11036,11 @@ async function memberUp(...args) {
|
|
|
10838
11036
|
if (hasFlag(args, '--help') || hasFlag(args, '-h')) {
|
|
10839
11037
|
console.log('Usage: atris member up [--max N] [--only a,b] [--dry-run]');
|
|
10840
11038
|
console.log('Keeps the team working: every minute, starts each free member that has a fitting task with a runnable check.');
|
|
10841
|
-
console.log('A member with no such task
|
|
11039
|
+
console.log('A member with no such task writes its next job from its mission when the mission has a ## Check, then starts it.');
|
|
11040
|
+
console.log('Without a check it stays idle. At most one self-written job is open per member, and 3 a day.');
|
|
11041
|
+
console.log('The longer a member stays idle, the less often it is checked: 5, 10, 20, then every 30 minutes.');
|
|
11042
|
+
console.log('A working member with no new step for 20 minutes is called out as stuck, once until it moves again.');
|
|
11043
|
+
console.log('Every 5 minutes it also puts member changes github dropped from its merge line back in, when self-landing is on.');
|
|
10842
11044
|
console.log('--only limits it to those members. Same as atris member wake 5 or atris member wake a b.');
|
|
10843
11045
|
console.log('Ctrl-C or atris member sleep --all ends it. Runs it started keep going.');
|
|
10844
11046
|
return;
|
|
@@ -10855,8 +11057,17 @@ async function memberUp(...args) {
|
|
|
10855
11057
|
const deps = {
|
|
10856
11058
|
listMembers: () => memberUpTeam(root),
|
|
10857
11059
|
pickTask: (name, taken) => memberUpPick(root, name, taken),
|
|
10858
|
-
|
|
11060
|
+
writeNextJob: (name, opts) => memberUpWriteNextJob(root, name, opts),
|
|
11061
|
+
spawn: (name, onExit, onClaim) => memberUpSpawn(root, name, onExit, onClaim),
|
|
10859
11062
|
now: () => Date.now(),
|
|
11063
|
+
jobOf: (name) => memberGoJob(name, root),
|
|
11064
|
+
waitingTasks: () => memberUpWaiting(root),
|
|
11065
|
+
requeueDropped: () => require('../lib/member-up').memberMaintenance({
|
|
11066
|
+
cwd: root,
|
|
11067
|
+
codeRoot: path.resolve(__dirname, '..'),
|
|
11068
|
+
requeueDropped: () => require('../lib/member-autoland').requeueDroppedAutolands({ cwd: root }),
|
|
11069
|
+
waitingTasks: () => memberUpWaiting(root),
|
|
11070
|
+
}),
|
|
10860
11071
|
};
|
|
10861
11072
|
if (parsed.dryRun) {
|
|
10862
11073
|
const up = createMemberUp(deps, { max, only, dryRun: true });
|
|
@@ -10957,16 +11168,56 @@ function memberSleep(target, ...args) {
|
|
|
10957
11168
|
return payload;
|
|
10958
11169
|
}
|
|
10959
11170
|
|
|
11171
|
+
// The member's active goal plus how many times it was set, so a re-agreed
|
|
11172
|
+
// goal with the same words still counts as the talk's next step.
|
|
11173
|
+
function oneOnOneGoalMark(name, paths) {
|
|
11174
|
+
const goal = (loadMemberGoals(name, paths).goals || []).filter((g) => g && g.status === 'active').pop();
|
|
11175
|
+
return goal ? { title: goal.title, mark: `${goal.id}:${(goal.history || []).length}` } : { title: '', mark: '' };
|
|
11176
|
+
}
|
|
11177
|
+
|
|
11178
|
+
function askOneOnOneNextStep(name) {
|
|
11179
|
+
const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
|
|
11180
|
+
return new Promise((resolve) => rl.question(`What is ${name}'s next step? `, (answer) => {
|
|
11181
|
+
rl.close();
|
|
11182
|
+
resolve(String(answer || '').trim());
|
|
11183
|
+
}));
|
|
11184
|
+
}
|
|
11185
|
+
|
|
11186
|
+
// A 1-on-1 is a talk with the member in its own voice, and it ends with the
|
|
11187
|
+
// member's next step saved as its active goal. The member records the step
|
|
11188
|
+
// itself during the talk; --next or a closing question fills it in otherwise.
|
|
10960
11189
|
async function memberOneOnOne(name, ...args) {
|
|
10961
11190
|
if (!name || name === '--help' || name === '-h' || hasFlag(args, '--help') || hasFlag(args, '-h')) {
|
|
10962
|
-
console.log('Usage: atris member 1on1 <name> [--
|
|
10963
|
-
console.log('
|
|
11191
|
+
console.log('Usage: atris member 1on1 <name> [--next "..."]');
|
|
11192
|
+
console.log('Talk with a team member in its own voice. The talk ends with its next step saved as its goal.');
|
|
10964
11193
|
return;
|
|
10965
11194
|
}
|
|
11195
|
+
const paths = requireMemberDir(name);
|
|
11196
|
+
const next = readFlag(args, '--next', '');
|
|
11197
|
+
const rest = stripKnownFlags(args, ['--next'], []);
|
|
11198
|
+
const before = oneOnOneGoalMark(name, paths);
|
|
10966
11199
|
const { interviewCommand } = require('./interview');
|
|
10967
|
-
const code = await interviewCommand([name, ...
|
|
10968
|
-
if (code)
|
|
10969
|
-
|
|
11200
|
+
const code = await interviewCommand([name, ...rest], process.cwd(), { mode: 'one_on_one' });
|
|
11201
|
+
if (code) {
|
|
11202
|
+
process.exitCode = code;
|
|
11203
|
+
return code;
|
|
11204
|
+
}
|
|
11205
|
+
let after = oneOnOneGoalMark(name, paths);
|
|
11206
|
+
if (after.mark === before.mark) {
|
|
11207
|
+
const step = next || (process.stdin.isTTY && process.stdout.isTTY ? await askOneOnOneNextStep(name) : '');
|
|
11208
|
+
if (step) {
|
|
11209
|
+
memberGoal(name, step);
|
|
11210
|
+
after = oneOnOneGoalMark(name, paths);
|
|
11211
|
+
}
|
|
11212
|
+
}
|
|
11213
|
+
if (after.title && after.mark !== before.mark) {
|
|
11214
|
+
console.log(`${name}'s next step: ${after.title}`);
|
|
11215
|
+
return 0;
|
|
11216
|
+
}
|
|
11217
|
+
console.log(`The 1-on-1 ended without a next step for ${name}.`);
|
|
11218
|
+
console.log(`Set one: atris member goal ${name} "..."`);
|
|
11219
|
+
process.exitCode = 1;
|
|
11220
|
+
return 1;
|
|
10970
11221
|
}
|
|
10971
11222
|
|
|
10972
11223
|
function collectMemberHandoffs(root = process.cwd(), cards = []) {
|
|
@@ -11315,6 +11566,9 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11315
11566
|
case 'list':
|
|
11316
11567
|
case 'ls':
|
|
11317
11568
|
return memberList(...args);
|
|
11569
|
+
case 'roster':
|
|
11570
|
+
// Who is on the team, working or idle, and on which engine.
|
|
11571
|
+
return require('./team').memberRosterCommand(args);
|
|
11318
11572
|
case 'create':
|
|
11319
11573
|
case 'new':
|
|
11320
11574
|
return memberCreate(args[0], ...args.slice(1));
|
|
@@ -11440,6 +11694,14 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11440
11694
|
if (isKnownMemberSlug(subcommand, process.cwd()) && args.filter((a) => !String(a).startsWith('-')).length === 0) {
|
|
11441
11695
|
return memberCard(subcommand, ...args);
|
|
11442
11696
|
}
|
|
11697
|
+
// One word that is neither a member nor a subcommand is a typo far
|
|
11698
|
+
// more often than a job. Starting a paid run on it costs money and
|
|
11699
|
+
// does nothing useful, so name the miss and stop. Jobs keep working
|
|
11700
|
+
// as more than one word, or through `atris member run "<job>"`.
|
|
11701
|
+
const extraWords = args.filter((a) => !String(a).startsWith('-')).length;
|
|
11702
|
+
if (!/\s/.test(subcommand.trim()) && extraWords === 0) {
|
|
11703
|
+
return memberUnknownWord(subcommand);
|
|
11704
|
+
}
|
|
11443
11705
|
return memberSteerOrDispatch(subcommand, ...args);
|
|
11444
11706
|
}
|
|
11445
11707
|
console.log('run "<job>" pick the right member and run it: atris member run "fix the flaky login test"');
|
|
@@ -11453,14 +11715,15 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11453
11715
|
console.log(' "<prompt>" Steer a running member mid-flight or start the best specialist');
|
|
11454
11716
|
console.log(' go [name|"..."] Wake the highest-leverage member on its next_push or run a prompt');
|
|
11455
11717
|
console.log(' steer [name] "..." Steer a member mid-run or update its immediate direction');
|
|
11456
|
-
console.log(' 1on1 <name>
|
|
11718
|
+
console.log(' 1on1 <name> Talk with a member in its own voice; ends with its next step saved');
|
|
11457
11719
|
console.log(' sleep [name|--all] Park active member loops so your team rests until woken');
|
|
11458
11720
|
console.log(' room [--json] Render the visual Team Room and desktop UI payload');
|
|
11459
11721
|
console.log(' board [--watch] One screen of every member: job, last step, working/quiet/done');
|
|
11460
11722
|
console.log(' create <name> Scaffold a new team member (MEMBER.md + dirs) [--push]');
|
|
11461
11723
|
console.log(' install <slug> [--update] Install missing files, or refresh template skills');
|
|
11462
11724
|
console.log(' chat <name> "..." Send a message to this member\'s desk in Atris Desktop');
|
|
11463
|
-
console.log(' list [--json] Show all team members');
|
|
11725
|
+
console.log(' list [--json] Show all team members with skills and version');
|
|
11726
|
+
console.log(' roster [--all] Who is on the team: what each does, working or idle, and engine');
|
|
11464
11727
|
console.log(' tidy [--dry-run] Put away member copies of the repo whose work already landed');
|
|
11465
11728
|
console.log(' export [--dry-run] Write each member as a subagent for Claude Code, Cursor, and Codex');
|
|
11466
11729
|
console.log(' activate <name> Symlink member skills, show context and permissions');
|
|
@@ -11541,6 +11804,8 @@ module.exports = {
|
|
|
11541
11804
|
memberRunWantsReferee,
|
|
11542
11805
|
proposalPromptForGoal,
|
|
11543
11806
|
memberGoFinishTask,
|
|
11807
|
+
memberRunReviewAsk,
|
|
11808
|
+
memberUpSpawn,
|
|
11544
11809
|
appendMemberGoalLog,
|
|
11545
11810
|
appendProjectLog,
|
|
11546
11811
|
memberCommand,
|