atris 3.69.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 +40 -1
- 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 +433 -99
- package/commands/mission.js +332 -41
- package/commands/report.js +42 -8
- package/commands/run.js +9 -2
- package/commands/scout.js +9 -1
- package/commands/task.js +132 -16
- package/commands/team.js +212 -4
- package/commands/unknowns.js +15 -1
- package/commands/verify.js +46 -0
- package/commands/worktree.js +148 -18
- 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 +35 -4
- package/lib/engine-registry.js +2 -0
- package/lib/engine-resource.js +518 -0
- package/lib/engine-validate.js +189 -9
- 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 +479 -10
- package/lib/member-board.js +86 -10
- 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 +433 -0
- package/lib/member-scaffold.js +1 -2
- package/lib/member-self-next-job.js +97 -0
- package/lib/member-switches.js +7 -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 +63 -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
|
@@ -51,8 +51,7 @@ function ensureMemberLog(memberDir, { name, role, description, source = 'cli' }
|
|
|
51
51
|
description ? `- mission: ${description}` : '',
|
|
52
52
|
`- source: ${source}`,
|
|
53
53
|
'- status: ready_for_room',
|
|
54
|
-
|
|
55
|
-
].filter(Boolean).join('\n');
|
|
54
|
+
].filter(Boolean).join('\n') + '\n';
|
|
56
55
|
fs.writeFileSync(logPath, content, 'utf8');
|
|
57
56
|
return logPath;
|
|
58
57
|
}
|
|
@@ -567,7 +566,16 @@ function memberLatestMission(name, missionMap = memberRunMissionMap()) {
|
|
|
567
566
|
|
|
568
567
|
function memberLiveMission(name, missionMap = memberRunMissionMap()) {
|
|
569
568
|
return memberOwnedMissions(name, missionMap)
|
|
570
|
-
.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;
|
|
571
579
|
}
|
|
572
580
|
|
|
573
581
|
function missionShortName(mission, max = 36) {
|
|
@@ -915,6 +923,20 @@ function memberRunBudgetSeconds(args = []) {
|
|
|
915
923
|
return null;
|
|
916
924
|
}
|
|
917
925
|
|
|
926
|
+
// A job with no time given gets 15 minutes, or the roster's limit for this
|
|
927
|
+
// member's engine plus ten minutes for the review, whichever is longer.
|
|
928
|
+
function memberRunDefaultWallSeconds(name, cwd = process.cwd()) {
|
|
929
|
+
let cap = 0;
|
|
930
|
+
try {
|
|
931
|
+
const picked = require('../lib/member-engine').memberRosterEngine(name, cwd);
|
|
932
|
+
cap = Number(picked && picked.engine && picked.engine.roster_max_seconds) || 0;
|
|
933
|
+
} catch {}
|
|
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);
|
|
938
|
+
}
|
|
939
|
+
|
|
918
940
|
function pushFlagValue(out, args, name) {
|
|
919
941
|
const value = readFlag(args, name, '');
|
|
920
942
|
if (value) out.push(name, String(value));
|
|
@@ -1040,7 +1062,7 @@ function memberRunCli(args, cwd, { live = false, missionId = '' } = {}) {
|
|
|
1040
1062
|
: null;
|
|
1041
1063
|
const receipt = receiptName ? readJson(path.join(runsDir, receiptName), null) : null;
|
|
1042
1064
|
return { status: result.status, payload: receipt?.result?.kind === 'mission_run_summary'
|
|
1043
|
-
? { ran_ticks: receipt.result.ran_ticks } : null, stderr: '', errorReported: result.status !== 0 };
|
|
1065
|
+
? { ran_ticks: receipt.result.ran_ticks, ticks: receipt.result.ticks } : null, stderr: '', errorReported: result.status !== 0 };
|
|
1044
1066
|
}
|
|
1045
1067
|
let payload = null;
|
|
1046
1068
|
try {
|
|
@@ -1235,7 +1257,16 @@ function memberRun(name, ...args) {
|
|
|
1235
1257
|
return memberRunWith(name, args);
|
|
1236
1258
|
}
|
|
1237
1259
|
|
|
1238
|
-
|
|
1260
|
+
// One plain line for why the reviewing engine refused the work.
|
|
1261
|
+
function reviewRefusalLine(refusal) {
|
|
1262
|
+
if (refusal.timed_out) return 'the reviewer ran out of time';
|
|
1263
|
+
const text = String(refusal.output || refusal.reason || '');
|
|
1264
|
+
const verdict = text.match(/VERDICT:\s*FAIL\s*[;:]\s*(.+)/i);
|
|
1265
|
+
const line = verdict ? verdict[1] : (text.split(/\r?\n/).find((l) => l.trim()) || refusal.reason || 'no reason given');
|
|
1266
|
+
return line.trim().slice(0, 200);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function memberRunWith(name, args, { quiet = false, closeTaskIds = [] } = {}) {
|
|
1239
1270
|
if (!name || name === '--help' || name === '-h' || hasFlag(args, '--help') || hasFlag(args, '-h')) {
|
|
1240
1271
|
console.log('usage: atris member run <name> ["mission text"] [--verify "<cmd>"] [--minutes N|--hours N] [--json]');
|
|
1241
1272
|
console.log('pick for me: atris member run "fix the flaky login test" (the best member takes it and runs it)');
|
|
@@ -1314,6 +1345,7 @@ function memberRunWith(name, args, { quiet = false } = {}) {
|
|
|
1314
1345
|
console.log(`watch: tail -f ${progressDir}/${String(name).replace(/[^A-Za-z0-9._-]/g, '_')}.log`);
|
|
1315
1346
|
console.log('board: atris member board --watch');
|
|
1316
1347
|
console.log('stop: atris member stop <name>');
|
|
1348
|
+
console.log(`where work goes: ${require('../lib/member-autoland').landingDestination(mission.worktree?.path || process.cwd())}`);
|
|
1317
1349
|
}
|
|
1318
1350
|
|
|
1319
1351
|
const cwd = mission.worktree?.path || process.cwd();
|
|
@@ -1338,7 +1370,7 @@ function memberRunWith(name, args, { quiet = false } = {}) {
|
|
|
1338
1370
|
if (!readFlag(runArgs, '--max-ticks', '')) {
|
|
1339
1371
|
runArgs.push('--max-ticks', budgetSeconds ? String(Math.max(4, Math.ceil(budgetSeconds / 300))) : '1');
|
|
1340
1372
|
}
|
|
1341
|
-
if (!readFlag(runArgs, '--max-wall', '')) runArgs.push('--max-wall', String(budgetSeconds ||
|
|
1373
|
+
if (!readFlag(runArgs, '--max-wall', '')) runArgs.push('--max-wall', String(budgetSeconds || memberRunDefaultWallSeconds(name, cwd)));
|
|
1342
1374
|
const caller = callerRuntime(args);
|
|
1343
1375
|
const runnerSwap = memberRunRunnerSwap(mission, args, caller);
|
|
1344
1376
|
if (runnerSwap) runArgs.push('--engine', runnerSwap.to);
|
|
@@ -1351,7 +1383,12 @@ function memberRunWith(name, args, { quiet = false } = {}) {
|
|
|
1351
1383
|
const waitingOnNativeGoal = caller === 'codex' && payload.requires_native_goal_start === true
|
|
1352
1384
|
&& memberRunUsesCallerSession(finalMission.runner);
|
|
1353
1385
|
const noWorkStepExpected = hasFlag(runArgs, '--detach') || hasFlag(runArgs, '--dry-run');
|
|
1354
|
-
|
|
1386
|
+
// The work ran but its check failed: the run failed, even though a work
|
|
1387
|
+
// step happened. Scripts and the caller read this from the exit code.
|
|
1388
|
+
const lastRanTick = [...(Array.isArray(payload.ticks) ? payload.ticks : [])].reverse().find((tick) => tick && tick.ran === true);
|
|
1389
|
+
const checkFailed = started && Boolean(String(finalMission.verifier || '').trim())
|
|
1390
|
+
&& lastRanTick?.verifier_passed === false && finalMission.status !== 'complete';
|
|
1391
|
+
const ok = ((run.status === 0 && payload.ok !== false && (started || noWorkStepExpected)) || waitingOnNativeGoal) && !checkFailed;
|
|
1355
1392
|
const reviewAsk = memberRunReviewAsk(name, payload, finalMission, cwd);
|
|
1356
1393
|
const result = { ...payload, ok, started, state: finalMission.status, mission_id: mission.id,
|
|
1357
1394
|
mission: finalMission, node_modules_linked: nodeModulesLinked, node_modules_source: nodeModulesSource,
|
|
@@ -1382,10 +1419,16 @@ function memberRunWith(name, args, { quiet = false } = {}) {
|
|
|
1382
1419
|
const summary = ticks.map((tick) => tick?.summary || tick?.claude?.summary).filter(Boolean).pop() || '';
|
|
1383
1420
|
const workerEngine = ticks.map((tick) => tick?.engine_id).filter(Boolean).pop()
|
|
1384
1421
|
|| runnerSwap?.to || readFlag(args, '--runner', '') || '';
|
|
1385
|
-
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 });
|
|
1386
1423
|
if (!asJson && !quiet) console.log(result.autoland.line);
|
|
1387
1424
|
}
|
|
1388
|
-
if (
|
|
1425
|
+
if (checkFailed) {
|
|
1426
|
+
result.check_failed = true;
|
|
1427
|
+
const refusal = lastRanTick?.referee_result?.passed === false ? lastRanTick.referee_result : null;
|
|
1428
|
+
result.error = refusal
|
|
1429
|
+
? `turned down in review: ${reviewRefusalLine(refusal)}`
|
|
1430
|
+
: `the work ran but its check failed: ${finalMission.verifier || 'completion check'}`;
|
|
1431
|
+
} else if (!ok) {
|
|
1389
1432
|
result.error = result.ask || payload.detail || payload.error || payload.reason || payload.next_action
|
|
1390
1433
|
|| run.stderr || 'mission did not run a work step';
|
|
1391
1434
|
}
|
|
@@ -3313,6 +3356,19 @@ function dateFromLogPath(relativePath) {
|
|
|
3313
3356
|
return match ? match[1] : null;
|
|
3314
3357
|
}
|
|
3315
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
|
+
|
|
3316
3372
|
function compareLogEvidenceRecentFirst(a, b) {
|
|
3317
3373
|
const dateA = a?.date || '';
|
|
3318
3374
|
const dateB = b?.date || '';
|
|
@@ -3377,7 +3433,11 @@ function isAutoImproverGeneratedLogLine(line, relativePath = '') {
|
|
|
3377
3433
|
if (/^[-*]?\s*Next tick will verify failed, halting\.?$/i.test(text)) return true;
|
|
3378
3434
|
if (/^##\s+\d{1,2}:\d{2}\s+·\s+Task failed$/i.test(text)) return true;
|
|
3379
3435
|
if (/^[-*]?\s*(status|action|state|verifier)\s*:\s*(failed|blocked)\s*$/i.test(text)) return true;
|
|
3380
|
-
|
|
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;
|
|
3381
3441
|
if (String(relativePath || '').startsWith('atris/team/auto-improver/logs/') && /^-\s*summary:\s*/i.test(text)) return true;
|
|
3382
3442
|
if (/\bauto[- ]improver\b/i.test(text) && /\b(dogfood|receipt|prevented|pain_)\b/i.test(text)) return true;
|
|
3383
3443
|
if (/\bauto_improver\b/i.test(text)) return true;
|
|
@@ -3395,7 +3455,9 @@ function normalizeFailurePattern(line) {
|
|
|
3395
3455
|
.trim());
|
|
3396
3456
|
}
|
|
3397
3457
|
|
|
3398
|
-
|
|
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()) {
|
|
3399
3461
|
const text = String(line || '');
|
|
3400
3462
|
if (!text || !passLessonText) return false;
|
|
3401
3463
|
if (/spawnSync\s+\/bin\/sh\s+ETIMEDOUT/i.test(text)) {
|
|
@@ -3411,23 +3473,59 @@ function failureCoveredByPassLesson(line, passLessonText = '') {
|
|
|
3411
3473
|
const bare = pattern.replace(/^#+\s*[#:.]*\s*·\s*/, '').trim();
|
|
3412
3474
|
for (const candidate of [pattern, bare]) {
|
|
3413
3475
|
if (candidate && candidate.length >= 12
|
|
3414
|
-
&&
|
|
3476
|
+
&& passLessonLower.includes(candidate.toLowerCase())) {
|
|
3415
3477
|
return true;
|
|
3416
3478
|
}
|
|
3417
3479
|
}
|
|
3418
3480
|
return false;
|
|
3419
3481
|
}
|
|
3420
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
|
+
|
|
3421
3523
|
function collectAutoImproverLogSignals(root) {
|
|
3422
|
-
const roots = [
|
|
3423
|
-
path.join(root, 'atris', 'logs'),
|
|
3424
|
-
path.join(root, 'atris', 'team'),
|
|
3425
|
-
path.join(root, 'atris', 'wiki'),
|
|
3426
|
-
].filter((candidate) => fs.existsSync(candidate));
|
|
3427
3524
|
const passLessonText = safeReadText(path.join(root, 'atris', 'lessons.md'), 500000)
|
|
3428
3525
|
.split(/\r?\n/)
|
|
3429
3526
|
.filter((line) => /\s+pass\s*[-\u2014:,]/.test(line))
|
|
3430
3527
|
.join('\n');
|
|
3528
|
+
const passLessonLower = passLessonText.toLowerCase();
|
|
3431
3529
|
const failureRegex = /\b(error|failed|failure|blocked|timeout|regression|crash|missing proof|naraka|suffering)\b/i;
|
|
3432
3530
|
const unclearRegex = /\b(tbd|unclear|unknown|needs user|needs owner|needs proof|no next|blocked)\b/i;
|
|
3433
3531
|
const counts = new Map();
|
|
@@ -3435,66 +3533,67 @@ function collectAutoImproverLogSignals(root) {
|
|
|
3435
3533
|
let filesScanned = 0;
|
|
3436
3534
|
let linesScanned = 0;
|
|
3437
3535
|
let unclearNextActions = 0;
|
|
3438
|
-
for (const
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
line: sourceLine,
|
|
3482
|
-
text: compactSentence(line, 180),
|
|
3483
|
-
});
|
|
3484
|
-
}
|
|
3485
|
-
if (!failureRegex.test(line)) continue;
|
|
3486
|
-
const pattern = normalizeFailurePattern(line);
|
|
3487
|
-
if (!pattern) continue;
|
|
3488
|
-
const existing = counts.get(pattern) || { pattern, count: 0, evidence: [] };
|
|
3489
|
-
existing.count += 1;
|
|
3490
|
-
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({
|
|
3491
3579
|
path: relative,
|
|
3492
3580
|
date: logDate,
|
|
3493
3581
|
line: sourceLine,
|
|
3494
3582
|
text: compactSentence(line, 180),
|
|
3495
3583
|
});
|
|
3496
|
-
counts.set(pattern, existing);
|
|
3497
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);
|
|
3498
3597
|
}
|
|
3499
3598
|
}
|
|
3500
3599
|
const repeated = [...counts.values()]
|
|
@@ -4256,7 +4355,10 @@ function parseFrontmatter(content) {
|
|
|
4256
4355
|
} else if (val.startsWith('[') && val.endsWith(']')) {
|
|
4257
4356
|
result[currentKey] = val.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
|
|
4258
4357
|
} else {
|
|
4259
|
-
|
|
4358
|
+
// Unwrap only a matching outer pair: `verify: node -e "process.exit(0)"`
|
|
4359
|
+
// must keep its closing quote or the check is a shell syntax error.
|
|
4360
|
+
const quoted = val.match(/^(["'])([\s\S]*)\1$/);
|
|
4361
|
+
result[currentKey] = quoted ? quoted[2] : val;
|
|
4260
4362
|
}
|
|
4261
4363
|
}
|
|
4262
4364
|
}
|
|
@@ -8975,6 +9077,26 @@ async function memberLoopWakeTick(name, run, index, tickStartedAt) {
|
|
|
8975
9077
|
const MEMBER_LOOP_IDLE_BREAK_THRESHOLD = 2;
|
|
8976
9078
|
const MEMBER_LOOP_IDENTICAL_ASK_BREAK_THRESHOLD = 2;
|
|
8977
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
|
+
|
|
8978
9100
|
// Loop phase: control. Hold the lease, honor the stop file, pace ticks by interval, and
|
|
8979
9101
|
// break early when the member is idling instead of faking activity.
|
|
8980
9102
|
async function memberLoopRunTicks(name, paths, lease, run) {
|
|
@@ -8985,6 +9107,13 @@ async function memberLoopRunTicks(name, paths, lease, run) {
|
|
|
8985
9107
|
state.stopped = true;
|
|
8986
9108
|
break;
|
|
8987
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
|
+
}
|
|
8988
9117
|
refreshMemberLoopLease(paths, lease.lease, plan.ttlMs);
|
|
8989
9118
|
const tickStartedAt = stampIso();
|
|
8990
9119
|
try {
|
|
@@ -9038,7 +9167,7 @@ function memberLoopRecordResult(name, paths, lease, run) {
|
|
|
9038
9167
|
const { options, plan, state } = run;
|
|
9039
9168
|
const { execute, aliveMode, asJson } = options;
|
|
9040
9169
|
const { hourly, forever, runId, ticks, intervalMs, durationMs, startedAt } = plan;
|
|
9041
|
-
const { tickResults, decisions, stopped, failed, earlyExit, tickLogPath } = state;
|
|
9170
|
+
const { tickResults, decisions, stopped, failed, earlyExit, tickLogPath, upgrade } = state;
|
|
9042
9171
|
const finishedAt = stampIso();
|
|
9043
9172
|
const plannedOnly = aliveMode && tickResults.length > 0 && tickResults.every((tick) => tick.decision === 'planned');
|
|
9044
9173
|
const summary = {
|
|
@@ -9047,7 +9176,7 @@ function memberLoopRecordResult(name, paths, lease, run) {
|
|
|
9047
9176
|
schema: aliveMode ? 'atris.member_alive.v1' : 'atris.member_loop.v1',
|
|
9048
9177
|
member: name,
|
|
9049
9178
|
alive: aliveMode,
|
|
9050
|
-
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',
|
|
9051
9180
|
mode: execute ? 'execute' : 'dry_run',
|
|
9052
9181
|
cadence: hourly ? 'hourly' : 'custom',
|
|
9053
9182
|
forever,
|
|
@@ -9063,6 +9192,8 @@ function memberLoopRecordResult(name, paths, lease, run) {
|
|
|
9063
9192
|
duration_ms_actual: Date.parse(finishedAt) - Date.parse(startedAt),
|
|
9064
9193
|
decisions,
|
|
9065
9194
|
early_exit: earlyExit,
|
|
9195
|
+
upgrade: upgrade || null,
|
|
9196
|
+
restarted_from_version: process.env.ATRIS_MEMBER_LOOP_RESTARTED_FROM || null,
|
|
9066
9197
|
stop: earlyExit?.stop || null,
|
|
9067
9198
|
blocked_on_human: earlyExit?.blocked_on_human === true,
|
|
9068
9199
|
needs_user: earlyExit?.needs_user === true,
|
|
@@ -9087,6 +9218,7 @@ function memberLoopRecordResult(name, paths, lease, run) {
|
|
|
9087
9218
|
`Status: ${payload.status}`,
|
|
9088
9219
|
`Cadence: ${payload.cadence}${payload.forever ? ' forever' : ''}`,
|
|
9089
9220
|
`Ticks: ${payload.ticks}/${payload.ticks_requested}`,
|
|
9221
|
+
...(upgrade ? [`Upgrade: atris ${upgrade.from} -> ${upgrade.to}, restarting on the new version`] : []),
|
|
9090
9222
|
`Decisions: ${Object.entries(decisions).map(([key, count]) => `${key} x${count}`).join(', ') || 'none'}`,
|
|
9091
9223
|
`Stop: ${payload.stop_command}`,
|
|
9092
9224
|
`Receipt: ${path.relative(process.cwd(), receiptPath)}`,
|
|
@@ -9152,11 +9284,14 @@ async function memberLoop(name, ...args) {
|
|
|
9152
9284
|
lastAskKey: null,
|
|
9153
9285
|
consecutiveIdenticalAsk: 0,
|
|
9154
9286
|
tickLogPath,
|
|
9287
|
+
startVersion: memberLoopInstalledVersion(),
|
|
9288
|
+
upgrade: null,
|
|
9155
9289
|
};
|
|
9156
9290
|
|
|
9157
9291
|
const run = { options, plan, state };
|
|
9158
9292
|
await memberLoopRunTicks(name, paths, lease, run);
|
|
9159
9293
|
memberLoopRecordResult(name, paths, lease, run);
|
|
9294
|
+
if (state.upgrade) memberLoopRestartOnNewVersion(state.upgrade);
|
|
9160
9295
|
}
|
|
9161
9296
|
|
|
9162
9297
|
async function memberTick(name, ...args) {
|
|
@@ -9823,10 +9958,26 @@ const EXPERIMENT_STATUSES = new Set(['experimental', 'one-off']);
|
|
|
9823
9958
|
|
|
9824
9959
|
function ideaRuleFor(slug, record, text) {
|
|
9825
9960
|
const own = IDEA_ARCHETYPE_RULES.find((rule) => rule.slugs.includes(slug));
|
|
9826
|
-
if (own && own.pattern.test(text)) return own;
|
|
9961
|
+
if (own && own.pattern.test(text)) return { rule: own, own: true };
|
|
9827
9962
|
const { autoJobForMember } = require('../lib/member-engine');
|
|
9828
9963
|
const archetype = ARCHETYPE_FOR_JOB[autoJobForMember(slug, record.role)];
|
|
9829
|
-
|
|
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;
|
|
9830
9981
|
}
|
|
9831
9982
|
|
|
9832
9983
|
// 0 = normal, 1 = experiment or one-off, 2 = parked, human, or odd-typed.
|
|
@@ -9838,10 +9989,12 @@ function dispatchTier(record) {
|
|
|
9838
9989
|
return 0;
|
|
9839
9990
|
}
|
|
9840
9991
|
|
|
9841
|
-
// 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.
|
|
9842
9994
|
function outranksDispatch(candidate, best) {
|
|
9843
9995
|
if (!best) return true;
|
|
9844
9996
|
if (candidate.tier !== best.tier) return candidate.tier < best.tier;
|
|
9997
|
+
if (candidate.fit !== best.fit) return candidate.fit > best.fit;
|
|
9845
9998
|
if (candidate.score !== best.score) return candidate.score > best.score;
|
|
9846
9999
|
return candidate.member.localeCompare(best.member) < 0;
|
|
9847
10000
|
}
|
|
@@ -9875,6 +10028,7 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
|
|
|
9875
10028
|
.toLowerCase()
|
|
9876
10029
|
.split(/[^a-z0-9]+/)
|
|
9877
10030
|
.filter((w) => w.length >= 4);
|
|
10031
|
+
const jobRule = ideaJobRule(text);
|
|
9878
10032
|
|
|
9879
10033
|
let best = null;
|
|
9880
10034
|
for (const record of members) {
|
|
@@ -9886,10 +10040,11 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
|
|
|
9886
10040
|
let score = 0;
|
|
9887
10041
|
let matchedReason = '';
|
|
9888
10042
|
|
|
9889
|
-
const
|
|
9890
|
-
|
|
10043
|
+
const memberRule = ideaRuleFor(slug, record, text);
|
|
10044
|
+
const fit = ideaJobFit(named, memberRule, jobRule);
|
|
10045
|
+
if (fit > 0 && memberRule.rule) {
|
|
9891
10046
|
score += 30;
|
|
9892
|
-
matchedReason = rule.reason;
|
|
10047
|
+
matchedReason = memberRule.rule.reason;
|
|
9893
10048
|
}
|
|
9894
10049
|
|
|
9895
10050
|
const missionFile = path.join(teamDir, slug, 'MISSION.md');
|
|
@@ -9920,6 +10075,7 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
|
|
|
9920
10075
|
reason: matchedReason,
|
|
9921
10076
|
score,
|
|
9922
10077
|
tier,
|
|
10078
|
+
fit,
|
|
9923
10079
|
};
|
|
9924
10080
|
if (outranksDispatch(candidate, best)) best = candidate;
|
|
9925
10081
|
}
|
|
@@ -10412,6 +10568,29 @@ function isKnownMemberSlug(token, root = process.cwd()) {
|
|
|
10412
10568
|
|| fs.existsSync(path.join(root, 'atris', 'team', `${clean}.md`));
|
|
10413
10569
|
}
|
|
10414
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
|
+
|
|
10415
10594
|
// The job a member does (build, review or search), read from the same team
|
|
10416
10595
|
// roster that picks its engine.
|
|
10417
10596
|
function memberGoJob(owner, root) {
|
|
@@ -10435,10 +10614,10 @@ function memberGoTask(owner, root, { claim, requireCheck = false }) {
|
|
|
10435
10614
|
const job = memberGoJob(owner, root);
|
|
10436
10615
|
const workspaceRoot = taskDb.workspaceRoot(root);
|
|
10437
10616
|
if (claim) {
|
|
10438
|
-
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 });
|
|
10439
10618
|
return got ? { ...got, job } : null;
|
|
10440
10619
|
}
|
|
10441
|
-
const task = picker.pickTaskForMember(taskDb, db, { workspaceRoot, job, requireCheck });
|
|
10620
|
+
const task = picker.pickTaskForMember(taskDb, db, { workspaceRoot, member: owner, job, requireCheck, recoverClaimed: true });
|
|
10442
10621
|
return task ? { task, objective: picker.taskObjective(task), job } : null;
|
|
10443
10622
|
} catch (error) {
|
|
10444
10623
|
console.error(`could not take a task from the list: ${String(error && error.message || error).split('\n')[0]}`);
|
|
@@ -10460,15 +10639,59 @@ function memberGoFinishTask(owner, task, result, thrown = null) {
|
|
|
10460
10639
|
if (!thrown && result && result.started && ['complete', 'ready'].includes(mission.status) && checked) {
|
|
10461
10640
|
const proof = [`[verified] ${owner} ran mission ${result.mission_id} and its check passed.`,
|
|
10462
10641
|
receipt ? `Receipt: ${receipt}` : ''].filter(Boolean).join('\n');
|
|
10463
|
-
|
|
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 });
|
|
10464
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 };
|
|
10465
10648
|
}
|
|
10649
|
+
const ticks = Array.isArray(result && result.ticks) ? result.ticks : [];
|
|
10650
|
+
const lastReason = String((ticks[ticks.length - 1] || {}).reason || (ticks[ticks.length - 1] || {}).error || '');
|
|
10651
|
+
const outOfTime = /timeout|wall-exceeded/.test(lastReason);
|
|
10466
10652
|
const why = thrown ? thrown.message
|
|
10653
|
+
: outOfTime ? 'ran out of time before the work finished'
|
|
10467
10654
|
: (result && result.error) || (result && result.started ? 'the check did not pass' : 'the run did not start');
|
|
10468
10655
|
const reason = String(why || 'the run did not finish').split(/\r?\n/)[0].trim().slice(0, 200);
|
|
10469
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
|
+
}
|
|
10667
|
+
// Third failure in a row: hold the task for a person instead of letting
|
|
10668
|
+
// members pick it up again forever.
|
|
10669
|
+
if (!held && memberGoFailStreak(taskDb, db, task.id) >= MEMBER_GO_STRIKES) {
|
|
10670
|
+
try {
|
|
10671
|
+
held = taskDb.tagTask(db, { id: task.id, actor: owner, add: ['needs-human'] }).tagged === true;
|
|
10672
|
+
if (held) taskDb.noteTask(db, { id: task.id, actor: owner,
|
|
10673
|
+
content: `Held for a person: failed ${MEMBER_GO_STRIKES} times in a row. Last reason: ${reason}` });
|
|
10674
|
+
} catch {}
|
|
10675
|
+
}
|
|
10470
10676
|
const released = taskDb.releaseTask(db, { id: task.id, actor: owner });
|
|
10471
|
-
|
|
10677
|
+
const outcome = released.released ? (held ? 'needs_human' : 'released') : 'left_claimed';
|
|
10678
|
+
return { id: task.id, title: task.title, outcome, reason, ...(held && forReview ? { held_for: 'review' } : {}) };
|
|
10679
|
+
}
|
|
10680
|
+
|
|
10681
|
+
const MEMBER_GO_STRIKES = 3;
|
|
10682
|
+
|
|
10683
|
+
// Failed runs on a task since its last pass, review, reopen, or a person
|
|
10684
|
+
// clearing its hold.
|
|
10685
|
+
function memberGoFailStreak(taskDb, db, taskId) {
|
|
10686
|
+
let streak = 0;
|
|
10687
|
+
for (const event of taskDb.listTaskEvents(db, { taskId })) {
|
|
10688
|
+
const type = event.event_type;
|
|
10689
|
+
const payload = event.payload || {};
|
|
10690
|
+
if (type === 'message' && /^Released by \S+: /.test(String(payload.content || ''))) streak += 1;
|
|
10691
|
+
else if (['proof_ready', 'reviewed', 'reopened'].includes(type)) streak = 0;
|
|
10692
|
+
else if (type === 'task_tags_updated' && (payload.removed || []).includes('needs-human')) streak = 0;
|
|
10693
|
+
}
|
|
10694
|
+
return streak;
|
|
10472
10695
|
}
|
|
10473
10696
|
|
|
10474
10697
|
async function memberSteerOrDispatch(...rawArgs) {
|
|
@@ -10531,7 +10754,7 @@ async function memberSteerOrDispatchWith({ pullTask = false } = {}, rawArgs = []
|
|
|
10531
10754
|
const missionSummary = memberMissionSummary(owner, missionMap, root);
|
|
10532
10755
|
// A human or odd-typed member never takes a task: it would be claimed and
|
|
10533
10756
|
// then stranded when the run refuses the member.
|
|
10534
|
-
const pulled = pullTask && !prompt && !missionSummary.
|
|
10757
|
+
const pulled = pullTask && !prompt && !missionSummary.live_mission && !memberRefusalFor(targetMember, paths.memberFile)
|
|
10535
10758
|
? memberGoTask(owner, root, { claim: !dryRun, requireCheck: needsCheck })
|
|
10536
10759
|
: null;
|
|
10537
10760
|
if (needsCheck && !pulled) {
|
|
@@ -10546,6 +10769,11 @@ async function memberSteerOrDispatchWith({ pullTask = false } = {}, rawArgs = []
|
|
|
10546
10769
|
|| goalPlane.goal?.title
|
|
10547
10770
|
|| extractMemberNorthStar(paths.memberDir, '');
|
|
10548
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
|
+
}
|
|
10549
10777
|
if (pulled) whyPicked = `${whyPicked} Took the oldest open ${pulled.job} task from the list.`.trim();
|
|
10550
10778
|
const engineSeat = resolveMemberEngineSeat(owner, root);
|
|
10551
10779
|
const { autoQueueMemberNextPush, closeMemberHandoffsForReceiver } = require('./mission');
|
|
@@ -10640,7 +10868,8 @@ async function memberSteerOrDispatchWith({ pullTask = false } = {}, rawArgs = []
|
|
|
10640
10868
|
const runFlags = required ? [...stripKnownFlags(nextFlags, ['--verify'], []), '--verify', required] : nextFlags;
|
|
10641
10869
|
let result = null;
|
|
10642
10870
|
try {
|
|
10643
|
-
|
|
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] });
|
|
10644
10873
|
} catch (error) {
|
|
10645
10874
|
memberGoFinishTask(owner, pulled.task, null, error);
|
|
10646
10875
|
throw error;
|
|
@@ -10651,6 +10880,9 @@ async function memberSteerOrDispatchWith({ pullTask = false } = {}, rawArgs = []
|
|
|
10651
10880
|
? { task, work_step: { summary: result?.work_step?.summary || null }, error: result?.error || null }
|
|
10652
10881
|
: merged));
|
|
10653
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}`);
|
|
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}`);
|
|
10654
10886
|
else console.log(`Task "${task.title}" went back on the list: ${task.reason}`);
|
|
10655
10887
|
return merged;
|
|
10656
10888
|
}
|
|
@@ -10723,14 +10955,42 @@ function memberUpPick(root, name, taken) {
|
|
|
10723
10955
|
const { fittingTasks } = require('../lib/member-task-pick');
|
|
10724
10956
|
const db = taskDb.open();
|
|
10725
10957
|
const job = memberGoJob(name, root);
|
|
10726
|
-
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 })
|
|
10727
10959
|
.find((task) => !taken.has(task.id)) || null;
|
|
10728
10960
|
} catch {
|
|
10729
10961
|
return null;
|
|
10730
10962
|
}
|
|
10731
10963
|
}
|
|
10732
10964
|
|
|
10733
|
-
|
|
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) {
|
|
10734
10994
|
const { spawn } = require('child_process');
|
|
10735
10995
|
const logDir = path.join(root, '.atris', 'state', 'member-up');
|
|
10736
10996
|
fs.mkdirSync(logDir, { recursive: true });
|
|
@@ -10743,12 +11003,15 @@ function memberUpSpawn(root, name, onExit) {
|
|
|
10743
11003
|
cwd: root,
|
|
10744
11004
|
detached: true,
|
|
10745
11005
|
// Children keep their log handle when the foreground supervisor stops.
|
|
10746
|
-
stdio: ['ignore', out, out],
|
|
11006
|
+
stdio: ['ignore', out, out, 'ipc'],
|
|
10747
11007
|
env: process.env,
|
|
10748
11008
|
});
|
|
10749
11009
|
} finally {
|
|
10750
11010
|
fs.closeSync(out);
|
|
10751
11011
|
}
|
|
11012
|
+
child.on('message', (message) => {
|
|
11013
|
+
if (message?.type === 'member_task_claimed' && message.member === name) onClaim?.(message.task);
|
|
11014
|
+
});
|
|
10752
11015
|
child.on('error', (error) => onExit(null, { ok: false, reason: error.message }));
|
|
10753
11016
|
child.on('close', (code, signal) => {
|
|
10754
11017
|
let output = '';
|
|
@@ -10773,7 +11036,11 @@ async function memberUp(...args) {
|
|
|
10773
11036
|
if (hasFlag(args, '--help') || hasFlag(args, '-h')) {
|
|
10774
11037
|
console.log('Usage: atris member up [--max N] [--only a,b] [--dry-run]');
|
|
10775
11038
|
console.log('Keeps the team working: every minute, starts each free member that has a fitting task with a runnable check.');
|
|
10776
|
-
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.');
|
|
10777
11044
|
console.log('--only limits it to those members. Same as atris member wake 5 or atris member wake a b.');
|
|
10778
11045
|
console.log('Ctrl-C or atris member sleep --all ends it. Runs it started keep going.');
|
|
10779
11046
|
return;
|
|
@@ -10790,8 +11057,17 @@ async function memberUp(...args) {
|
|
|
10790
11057
|
const deps = {
|
|
10791
11058
|
listMembers: () => memberUpTeam(root),
|
|
10792
11059
|
pickTask: (name, taken) => memberUpPick(root, name, taken),
|
|
10793
|
-
|
|
11060
|
+
writeNextJob: (name, opts) => memberUpWriteNextJob(root, name, opts),
|
|
11061
|
+
spawn: (name, onExit, onClaim) => memberUpSpawn(root, name, onExit, onClaim),
|
|
10794
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
|
+
}),
|
|
10795
11071
|
};
|
|
10796
11072
|
if (parsed.dryRun) {
|
|
10797
11073
|
const up = createMemberUp(deps, { max, only, dryRun: true });
|
|
@@ -10892,16 +11168,56 @@ function memberSleep(target, ...args) {
|
|
|
10892
11168
|
return payload;
|
|
10893
11169
|
}
|
|
10894
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.
|
|
10895
11189
|
async function memberOneOnOne(name, ...args) {
|
|
10896
11190
|
if (!name || name === '--help' || name === '-h' || hasFlag(args, '--help') || hasFlag(args, '-h')) {
|
|
10897
|
-
console.log('Usage: atris member 1on1 <name> [--
|
|
10898
|
-
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.');
|
|
10899
11193
|
return;
|
|
10900
11194
|
}
|
|
11195
|
+
const paths = requireMemberDir(name);
|
|
11196
|
+
const next = readFlag(args, '--next', '');
|
|
11197
|
+
const rest = stripKnownFlags(args, ['--next'], []);
|
|
11198
|
+
const before = oneOnOneGoalMark(name, paths);
|
|
10901
11199
|
const { interviewCommand } = require('./interview');
|
|
10902
|
-
const code = await interviewCommand([name, ...
|
|
10903
|
-
if (code)
|
|
10904
|
-
|
|
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;
|
|
10905
11221
|
}
|
|
10906
11222
|
|
|
10907
11223
|
function collectMemberHandoffs(root = process.cwd(), cards = []) {
|
|
@@ -11239,7 +11555,7 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11239
11555
|
const HELP_AWARE_SUBCOMMANDS = new Set([
|
|
11240
11556
|
'create', 'new', 'run', 'install', 'result', 'dispatch', 'route', 'feed', 'stream',
|
|
11241
11557
|
'card', 'cards', 'go', 'work', 'up', 'steer', 'sleep', 'rest', '1on1', 'interview',
|
|
11242
|
-
'room', 'ui', 'day', 'standup', 'handoff', 'handoffs',
|
|
11558
|
+
'room', 'ui', 'day', 'standup', 'handoff', 'handoffs', 'report',
|
|
11243
11559
|
]);
|
|
11244
11560
|
const explicitHelp = subcommand === 'help' || subcommand === '--help' || subcommand === '-h'
|
|
11245
11561
|
|| (!HELP_AWARE_SUBCOMMANDS.has(subcommand) && (args[0] === '-h' || args[0] === '--help'));
|
|
@@ -11250,6 +11566,9 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11250
11566
|
case 'list':
|
|
11251
11567
|
case 'ls':
|
|
11252
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);
|
|
11253
11572
|
case 'create':
|
|
11254
11573
|
case 'new':
|
|
11255
11574
|
return memberCreate(args[0], ...args.slice(1));
|
|
@@ -11345,6 +11664,8 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11345
11664
|
return memberHeartbeat(args[0], ...args.slice(1));
|
|
11346
11665
|
case 'improvements':
|
|
11347
11666
|
return memberImprovements(args[0], ...args.slice(1));
|
|
11667
|
+
case 'report':
|
|
11668
|
+
return require('../lib/member-report').memberReportCommand(...args);
|
|
11348
11669
|
case 'history':
|
|
11349
11670
|
return memberHistory(args[0], ...args.slice(1));
|
|
11350
11671
|
case 'supervisor':
|
|
@@ -11373,6 +11694,14 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11373
11694
|
if (isKnownMemberSlug(subcommand, process.cwd()) && args.filter((a) => !String(a).startsWith('-')).length === 0) {
|
|
11374
11695
|
return memberCard(subcommand, ...args);
|
|
11375
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
|
+
}
|
|
11376
11705
|
return memberSteerOrDispatch(subcommand, ...args);
|
|
11377
11706
|
}
|
|
11378
11707
|
console.log('run "<job>" pick the right member and run it: atris member run "fix the flaky login test"');
|
|
@@ -11386,14 +11715,15 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11386
11715
|
console.log(' "<prompt>" Steer a running member mid-flight or start the best specialist');
|
|
11387
11716
|
console.log(' go [name|"..."] Wake the highest-leverage member on its next_push or run a prompt');
|
|
11388
11717
|
console.log(' steer [name] "..." Steer a member mid-run or update its immediate direction');
|
|
11389
|
-
console.log(' 1on1 <name>
|
|
11718
|
+
console.log(' 1on1 <name> Talk with a member in its own voice; ends with its next step saved');
|
|
11390
11719
|
console.log(' sleep [name|--all] Park active member loops so your team rests until woken');
|
|
11391
11720
|
console.log(' room [--json] Render the visual Team Room and desktop UI payload');
|
|
11392
11721
|
console.log(' board [--watch] One screen of every member: job, last step, working/quiet/done');
|
|
11393
11722
|
console.log(' create <name> Scaffold a new team member (MEMBER.md + dirs) [--push]');
|
|
11394
11723
|
console.log(' install <slug> [--update] Install missing files, or refresh template skills');
|
|
11395
11724
|
console.log(' chat <name> "..." Send a message to this member\'s desk in Atris Desktop');
|
|
11396
|
-
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');
|
|
11397
11727
|
console.log(' tidy [--dry-run] Put away member copies of the repo whose work already landed');
|
|
11398
11728
|
console.log(' export [--dry-run] Write each member as a subagent for Claude Code, Cursor, and Codex');
|
|
11399
11729
|
console.log(' activate <name> Symlink member skills, show context and permissions');
|
|
@@ -11406,6 +11736,7 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11406
11736
|
console.log(' wake <name> Decide tick/wait/ask/stop, then run one bounded work step on the roster engine (--plan to only decide; --keep keeps it working)');
|
|
11407
11737
|
console.log(' run "<job>" Pick the best member for the job and run it (same as dispatch "<job>" --ignite)');
|
|
11408
11738
|
console.log(' run <name> ["..."] Start a budgeted member mission, or run its active Mission Runtime');
|
|
11739
|
+
console.log(' report [--since 2h] Short summary per member: merged, finished, turned down, done but not merged yet [--json]');
|
|
11409
11740
|
console.log(' result <name> Newest run result: run record, receipt, artifacts, number [--since <iso>] [--json]');
|
|
11410
11741
|
console.log(' dispatch "<idea>" Route an idea to the best specialist member and start work');
|
|
11411
11742
|
console.log(' up [--max N] [--only a,b] Keep the team working: start free members on fitting tasks every minute (wake 5 / wake a b)');
|
|
@@ -11469,9 +11800,12 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11469
11800
|
}
|
|
11470
11801
|
|
|
11471
11802
|
module.exports = {
|
|
11803
|
+
memberRunDefaultWallSeconds,
|
|
11472
11804
|
memberRunWantsReferee,
|
|
11473
11805
|
proposalPromptForGoal,
|
|
11474
11806
|
memberGoFinishTask,
|
|
11807
|
+
memberRunReviewAsk,
|
|
11808
|
+
memberUpSpawn,
|
|
11475
11809
|
appendMemberGoalLog,
|
|
11476
11810
|
appendProjectLog,
|
|
11477
11811
|
memberCommand,
|