atris 3.68.8 → 3.69.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 +32 -0
- package/atris/team/opus-overnight/MEMBER.md +1 -0
- package/commands/init.js +9 -0
- package/commands/member.js +359 -32
- package/commands/mission.js +77 -9
- package/commands/sync.js +54 -3
- package/lib/auto-accept-certified.js +1 -0
- package/lib/engine-registry.js +5 -4
- package/lib/gitignore.js +56 -0
- package/lib/member-autoland.js +350 -0
- package/lib/member-board.js +181 -23
- package/lib/member-progress.js +68 -4
- package/lib/member-task-pick.js +116 -0
- package/lib/member-tidy.js +235 -0
- package/lib/member-up.js +286 -0
- package/lib/member-verbs.js +84 -0
- package/lib/runner-command.js +29 -0
- package/lib/task-db.js +5 -1
- package/package.json +1 -1
package/commands/member.js
CHANGED
|
@@ -1017,6 +1017,12 @@ function startMemberRunMission(name, missionText, args = []) {
|
|
|
1017
1017
|
return memberRunCli(startArgs, process.cwd());
|
|
1018
1018
|
}
|
|
1019
1019
|
|
|
1020
|
+
// An ignited run asks a second engine to review the work by default, so every
|
|
1021
|
+
// run ends with a verdict. --no-referee skips it; an explicit --referee is kept.
|
|
1022
|
+
function memberRunWantsReferee(runArgs, ignite) {
|
|
1023
|
+
return Boolean(ignite) && !hasFlag(runArgs, '--referee') && !hasFlag(runArgs, '--no-referee');
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1020
1026
|
function memberRunCli(args, cwd, { live = false, missionId = '' } = {}) {
|
|
1021
1027
|
const cliPath = path.join(__dirname, '..', 'bin', 'atris.js');
|
|
1022
1028
|
const runsDir = path.join(cwd, 'atris', 'runs');
|
|
@@ -1232,6 +1238,7 @@ function memberRun(name, ...args) {
|
|
|
1232
1238
|
function memberRunWith(name, args, { quiet = false } = {}) {
|
|
1233
1239
|
if (!name || name === '--help' || name === '-h' || hasFlag(args, '--help') || hasFlag(args, '-h')) {
|
|
1234
1240
|
console.log('usage: atris member run <name> ["mission text"] [--verify "<cmd>"] [--minutes N|--hours N] [--json]');
|
|
1241
|
+
console.log('pick for me: atris member run "fix the flaky login test" (the best member takes it and runs it)');
|
|
1235
1242
|
console.log('new mission: atris member run growth "improve onboarding proof" --verify "npm test" --minutes 30 --json');
|
|
1236
1243
|
console.log('choose work: atris member run growth --verify "npm test" --industry logistics --minutes 30 --json');
|
|
1237
1244
|
console.log('subagent shell: atris member run growth "improve onboarding proof" --engine claude --ignite --json');
|
|
@@ -1240,6 +1247,7 @@ function memberRunWith(name, args, { quiet = false } = {}) {
|
|
|
1240
1247
|
console.log('truth: useful work must improve revenue, reliability, speed, security, clarity, or trust, then show plain proof.');
|
|
1241
1248
|
console.log('isolation: new missions use --worktree by default; add --shared-checkout to stay here.');
|
|
1242
1249
|
console.log('check: --verify or a verify field in the member file adds a mission check.');
|
|
1250
|
+
console.log('review: an --ignite run has a different engine review the work; add --no-referee to skip it.');
|
|
1243
1251
|
return;
|
|
1244
1252
|
}
|
|
1245
1253
|
|
|
@@ -1305,6 +1313,7 @@ function memberRunWith(name, args, { quiet = false } = {}) {
|
|
|
1305
1313
|
const progressDir = process.env.ATRIS_PROGRESS_DIR || '~/.atris/progress';
|
|
1306
1314
|
console.log(`watch: tail -f ${progressDir}/${String(name).replace(/[^A-Za-z0-9._-]/g, '_')}.log`);
|
|
1307
1315
|
console.log('board: atris member board --watch');
|
|
1316
|
+
console.log('stop: atris member stop <name>');
|
|
1308
1317
|
}
|
|
1309
1318
|
|
|
1310
1319
|
const cwd = mission.worktree?.path || process.cwd();
|
|
@@ -1324,6 +1333,7 @@ function memberRunWith(name, args, { quiet = false } = {}) {
|
|
|
1324
1333
|
['--mission', '--mission-id', '--minutes', '--hours', '--industry', '--domain', '--value', '--outcome', '--truth', '--proof', '--base'],
|
|
1325
1334
|
['--worktree', '--shared-checkout', '--no-worktree', '--choose-work', '--auto', '--ignite', '--wait', '--sync']);
|
|
1326
1335
|
if (ignite && !hasFlag(runArgs, '--complete-on-pass')) runArgs.push('--complete-on-pass');
|
|
1336
|
+
if (memberRunWantsReferee(runArgs, ignite)) runArgs.push('--referee');
|
|
1327
1337
|
const budgetSeconds = memberRunBudgetSeconds(args);
|
|
1328
1338
|
if (!readFlag(runArgs, '--max-ticks', '')) {
|
|
1329
1339
|
runArgs.push('--max-ticks', budgetSeconds ? String(Math.max(4, Math.ceil(budgetSeconds / 300))) : '1');
|
|
@@ -1365,6 +1375,16 @@ function memberRunWith(name, args, { quiet = false } = {}) {
|
|
|
1365
1375
|
result.ask = `${name} is waiting on you: ${payload.human_asks[0]}`;
|
|
1366
1376
|
result.next_command = payload.next_command || 'atris decide';
|
|
1367
1377
|
}
|
|
1378
|
+
// A finished ignited run whose check passed hands its branch to GitHub
|
|
1379
|
+
// auto-merge, only past the local gates; any held gate leaves the branch.
|
|
1380
|
+
if (ignite && ok && started && !reviewAsk && finalMission.verifier_result?.passed === true) {
|
|
1381
|
+
const ticks = Array.isArray(payload.ticks) ? payload.ticks : [];
|
|
1382
|
+
const summary = ticks.map((tick) => tick?.summary || tick?.claude?.summary).filter(Boolean).pop() || '';
|
|
1383
|
+
const workerEngine = ticks.map((tick) => tick?.engine_id).filter(Boolean).pop()
|
|
1384
|
+
|| runnerSwap?.to || readFlag(args, '--runner', '') || '';
|
|
1385
|
+
result.autoland = require('../lib/member-autoland').autolandMemberRun({ mission: finalMission, cwd, summary, workerEngine });
|
|
1386
|
+
if (!asJson && !quiet) console.log(result.autoland.line);
|
|
1387
|
+
}
|
|
1368
1388
|
if (!ok) {
|
|
1369
1389
|
result.error = result.ask || payload.detail || payload.error || payload.reason || payload.next_action
|
|
1370
1390
|
|| run.stderr || 'mission did not run a work step';
|
|
@@ -9796,11 +9816,35 @@ const IDEA_ARCHETYPE_RULES = [
|
|
|
9796
9816
|
},
|
|
9797
9817
|
];
|
|
9798
9818
|
|
|
9799
|
-
//
|
|
9800
|
-
//
|
|
9801
|
-
const
|
|
9802
|
-
|
|
9803
|
-
|
|
9819
|
+
// Keep specialized idea rules, then fall back to the same job inference used
|
|
9820
|
+
// by the engine roster. Renamed members get the same job score as built-ins.
|
|
9821
|
+
const ARCHETYPE_FOR_JOB = { build: 'executor', review: 'validator', search: 'researcher' };
|
|
9822
|
+
const EXPERIMENT_STATUSES = new Set(['experimental', 'one-off']);
|
|
9823
|
+
|
|
9824
|
+
function ideaRuleFor(slug, record, text) {
|
|
9825
|
+
const own = IDEA_ARCHETYPE_RULES.find((rule) => rule.slugs.includes(slug));
|
|
9826
|
+
if (own && own.pattern.test(text)) return own;
|
|
9827
|
+
const { autoJobForMember } = require('../lib/member-engine');
|
|
9828
|
+
const archetype = ARCHETYPE_FOR_JOB[autoJobForMember(slug, record.role)];
|
|
9829
|
+
return IDEA_ARCHETYPE_RULES.find((rule) => rule.slugs.includes(archetype)) || null;
|
|
9830
|
+
}
|
|
9831
|
+
|
|
9832
|
+
// 0 = normal, 1 = experiment or one-off, 2 = parked, human, or odd-typed.
|
|
9833
|
+
function dispatchTier(record) {
|
|
9834
|
+
const frontmatter = record.frontmatter || {};
|
|
9835
|
+
const status = String(frontmatter.status || '').trim().toLowerCase();
|
|
9836
|
+
if (status === 'parked' || memberRefusalReason(memberSlugFromRecord(record), frontmatter)) return 2;
|
|
9837
|
+
if (EXPERIMENT_STATUSES.has(status)) return 1;
|
|
9838
|
+
return 0;
|
|
9839
|
+
}
|
|
9840
|
+
|
|
9841
|
+
// Lower tier wins first, then higher score, then name order for a stable pick.
|
|
9842
|
+
function outranksDispatch(candidate, best) {
|
|
9843
|
+
if (!best) return true;
|
|
9844
|
+
if (candidate.tier !== best.tier) return candidate.tier < best.tier;
|
|
9845
|
+
if (candidate.score !== best.score) return candidate.score > best.score;
|
|
9846
|
+
return candidate.member.localeCompare(best.member) < 0;
|
|
9847
|
+
}
|
|
9804
9848
|
|
|
9805
9849
|
function ideaNamesMember(text, slug) {
|
|
9806
9850
|
const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/-/g, '[-\\s]+');
|
|
@@ -9835,15 +9879,17 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
|
|
|
9835
9879
|
let best = null;
|
|
9836
9880
|
for (const record of members) {
|
|
9837
9881
|
const slug = memberSlugFromRecord(record);
|
|
9882
|
+
const named = ideaNamesMember(text, slug);
|
|
9883
|
+
// Naming an experiment in the idea is the owner's pick, so it ranks normally.
|
|
9884
|
+
const baseTier = dispatchTier(record);
|
|
9885
|
+
const tier = named && baseTier === 1 ? 0 : baseTier;
|
|
9838
9886
|
let score = 0;
|
|
9839
9887
|
let matchedReason = '';
|
|
9840
9888
|
|
|
9841
|
-
|
|
9842
|
-
|
|
9843
|
-
|
|
9844
|
-
|
|
9845
|
-
break;
|
|
9846
|
-
}
|
|
9889
|
+
const rule = ideaRuleFor(slug, record, text);
|
|
9890
|
+
if (rule && rule.pattern.test(text)) {
|
|
9891
|
+
score += 30;
|
|
9892
|
+
matchedReason = rule.reason;
|
|
9847
9893
|
}
|
|
9848
9894
|
|
|
9849
9895
|
const missionFile = path.join(teamDir, slug, 'MISSION.md');
|
|
@@ -9856,14 +9902,10 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
|
|
|
9856
9902
|
}
|
|
9857
9903
|
score += overlapCount * 4;
|
|
9858
9904
|
if (Number(record.skillCount || 0) > 0) score += 2;
|
|
9859
|
-
if (slug === 'executor') score += 3;
|
|
9860
|
-
if (slug === 'auto-improver') score += 2;
|
|
9861
9905
|
|
|
9862
|
-
if (
|
|
9906
|
+
if (named) {
|
|
9863
9907
|
score += 40;
|
|
9864
9908
|
matchedReason = 'named in the idea';
|
|
9865
|
-
} else if (!CORE_DISPATCH_MEMBERS.has(slug)) {
|
|
9866
|
-
score = Math.floor(score / 2);
|
|
9867
9909
|
}
|
|
9868
9910
|
|
|
9869
9911
|
if (!matchedReason) {
|
|
@@ -9877,10 +9919,9 @@ function pickMemberForIdea(ideaText, root = process.cwd(), preferredMember = '')
|
|
|
9877
9919
|
role: record.role || 'Team Member',
|
|
9878
9920
|
reason: matchedReason,
|
|
9879
9921
|
score,
|
|
9922
|
+
tier,
|
|
9880
9923
|
};
|
|
9881
|
-
if (
|
|
9882
|
-
best = candidate;
|
|
9883
|
-
}
|
|
9924
|
+
if (outranksDispatch(candidate, best)) best = candidate;
|
|
9884
9925
|
}
|
|
9885
9926
|
|
|
9886
9927
|
return best;
|
|
@@ -9938,7 +9979,7 @@ function memberDispatch(...args) {
|
|
|
9938
9979
|
printJsonOrText(
|
|
9939
9980
|
payload,
|
|
9940
9981
|
[
|
|
9941
|
-
`${picked.member} (${picked.role}) will take this
|
|
9982
|
+
`${picked.member} (${picked.role}) will take this: ${picked.reason.replace(/\.$/, '')}.`,
|
|
9942
9983
|
'',
|
|
9943
9984
|
commandPreview,
|
|
9944
9985
|
],
|
|
@@ -9948,7 +9989,7 @@ function memberDispatch(...args) {
|
|
|
9948
9989
|
}
|
|
9949
9990
|
|
|
9950
9991
|
if (!asJson) {
|
|
9951
|
-
console.log(`${picked.member} (${picked.role}) is taking this
|
|
9992
|
+
console.log(`${picked.member} (${picked.role}) is taking this: ${picked.reason.replace(/\.$/, '')}.`);
|
|
9952
9993
|
}
|
|
9953
9994
|
// --ignite means do the work now, the same as the previewed member run;
|
|
9954
9995
|
// without it dispatch only opens the mission.
|
|
@@ -10371,7 +10412,70 @@ function isKnownMemberSlug(token, root = process.cwd()) {
|
|
|
10371
10412
|
|| fs.existsSync(path.join(root, 'atris', 'team', `${clean}.md`));
|
|
10372
10413
|
}
|
|
10373
10414
|
|
|
10415
|
+
// The job a member does (build, review or search), read from the same team
|
|
10416
|
+
// roster that picks its engine.
|
|
10417
|
+
function memberGoJob(owner, root) {
|
|
10418
|
+
const byRole = { navigator: 'search', executor: 'build', validator: 'review' };
|
|
10419
|
+
try {
|
|
10420
|
+
const seat = require('../lib/member-engine').resolveEngineForMember(owner, root);
|
|
10421
|
+
return byRole[seat.role] || byRole[seat.job_key] || 'build';
|
|
10422
|
+
} catch {
|
|
10423
|
+
return 'build';
|
|
10424
|
+
}
|
|
10425
|
+
}
|
|
10426
|
+
|
|
10427
|
+
// With no idea given, member go takes the next real job from the task list:
|
|
10428
|
+
// the oldest open task that fits the member's job. A dry run only looks.
|
|
10429
|
+
// requireCheck: only tasks with a runnable check count.
|
|
10430
|
+
function memberGoTask(owner, root, { claim, requireCheck = false }) {
|
|
10431
|
+
try {
|
|
10432
|
+
const taskDb = require('../lib/task-db');
|
|
10433
|
+
const picker = require('../lib/member-task-pick');
|
|
10434
|
+
const db = taskDb.open();
|
|
10435
|
+
const job = memberGoJob(owner, root);
|
|
10436
|
+
const workspaceRoot = taskDb.workspaceRoot(root);
|
|
10437
|
+
if (claim) {
|
|
10438
|
+
const got = picker.claimTaskForMember(taskDb, db, { workspaceRoot, member: owner, job, requireCheck });
|
|
10439
|
+
return got ? { ...got, job } : null;
|
|
10440
|
+
}
|
|
10441
|
+
const task = picker.pickTaskForMember(taskDb, db, { workspaceRoot, job, requireCheck });
|
|
10442
|
+
return task ? { task, objective: picker.taskObjective(task), job } : null;
|
|
10443
|
+
} catch (error) {
|
|
10444
|
+
console.error(`could not take a task from the list: ${String(error && error.message || error).split('\n')[0]}`);
|
|
10445
|
+
return null;
|
|
10446
|
+
}
|
|
10447
|
+
}
|
|
10448
|
+
|
|
10449
|
+
// A pulled task leaves the run in review when the run's check passed (the
|
|
10450
|
+
// mission is complete, or ready: checked and waiting on a person), else back
|
|
10451
|
+
// on the list with a one-line reason.
|
|
10452
|
+
function memberGoFinishTask(owner, task, result, thrown = null) {
|
|
10453
|
+
const taskDb = require('../lib/task-db');
|
|
10454
|
+
const db = taskDb.open();
|
|
10455
|
+
const mission = (result && result.mission) || {};
|
|
10456
|
+
const receipt = (result && result.work_step && result.work_step.receipt_path) || mission.receipt_path || '';
|
|
10457
|
+
const required = require('../lib/member-task-pick').taskCheck(task).command;
|
|
10458
|
+
const checked = mission.verifier_result && mission.verifier_result.passed === true
|
|
10459
|
+
&& (!required || String(mission.verifier_result.command || '').trim() === required);
|
|
10460
|
+
if (!thrown && result && result.started && ['complete', 'ready'].includes(mission.status) && checked) {
|
|
10461
|
+
const proof = [`[verified] ${owner} ran mission ${result.mission_id} and its check passed.`,
|
|
10462
|
+
receipt ? `Receipt: ${receipt}` : ''].filter(Boolean).join('\n');
|
|
10463
|
+
const ready = taskDb.readyTask(db, { id: task.id, actor: owner, proof });
|
|
10464
|
+
if (ready.ready !== false) return { id: task.id, title: task.title, outcome: 'review', receipt: receipt || null };
|
|
10465
|
+
}
|
|
10466
|
+
const why = thrown ? thrown.message
|
|
10467
|
+
: (result && result.error) || (result && result.started ? 'the check did not pass' : 'the run did not start');
|
|
10468
|
+
const reason = String(why || 'the run did not finish').split(/\r?\n/)[0].trim().slice(0, 200);
|
|
10469
|
+
try { taskDb.noteTask(db, { id: task.id, actor: owner, content: `Released by ${owner}: ${reason}` }); } catch {}
|
|
10470
|
+
const released = taskDb.releaseTask(db, { id: task.id, actor: owner });
|
|
10471
|
+
return { id: task.id, title: task.title, outcome: released.released ? 'released' : 'left_claimed', reason };
|
|
10472
|
+
}
|
|
10473
|
+
|
|
10374
10474
|
async function memberSteerOrDispatch(...rawArgs) {
|
|
10475
|
+
return memberSteerOrDispatchWith({}, rawArgs);
|
|
10476
|
+
}
|
|
10477
|
+
|
|
10478
|
+
async function memberSteerOrDispatchWith({ pullTask = false } = {}, rawArgs = []) {
|
|
10375
10479
|
if (hasFlag(rawArgs, '--help') || hasFlag(rawArgs, '-h')) {
|
|
10376
10480
|
console.log('Usage: atris member steer [member] "<direction>" [--dry-run] [--json]');
|
|
10377
10481
|
console.log(' atris member "<direction>" [--dry-run] [--json]');
|
|
@@ -10381,6 +10485,9 @@ async function memberSteerOrDispatch(...rawArgs) {
|
|
|
10381
10485
|
const root = process.cwd();
|
|
10382
10486
|
const asJson = hasFlag(rawArgs, '--json');
|
|
10383
10487
|
const dryRun = hasFlag(rawArgs, '--dry-run');
|
|
10488
|
+
// Set by `member up`: run only a claimed task with a runnable check, never
|
|
10489
|
+
// the member's own goal text.
|
|
10490
|
+
const needsCheck = pullTask && hasFlag(rawArgs, '--needs-check');
|
|
10384
10491
|
const valueFlags = new Set(['--engine', '--model', '--verify', '--referee-engine', '--minutes', '--hours', '--max-ticks', '--from']);
|
|
10385
10492
|
const positionals = [];
|
|
10386
10493
|
const flagArgs = [];
|
|
@@ -10388,6 +10495,7 @@ async function memberSteerOrDispatch(...rawArgs) {
|
|
|
10388
10495
|
const token = String(rawArgs[i] || '');
|
|
10389
10496
|
if (!token) continue;
|
|
10390
10497
|
if (token.startsWith('-')) {
|
|
10498
|
+
if (token === '--needs-check') continue;
|
|
10391
10499
|
flagArgs.push(token);
|
|
10392
10500
|
if (valueFlags.has(token) && i + 1 < rawArgs.length) {
|
|
10393
10501
|
flagArgs.push(rawArgs[i + 1]);
|
|
@@ -10419,18 +10527,31 @@ async function memberSteerOrDispatch(...rawArgs) {
|
|
|
10419
10527
|
fm = parseFrontmatter(fs.readFileSync(paths.memberFile, 'utf8')) || {};
|
|
10420
10528
|
} catch {}
|
|
10421
10529
|
const goalPlane = memberGoalPlaneStatus(owner, paths);
|
|
10530
|
+
const missionMap = memberRunMissionMap(root);
|
|
10531
|
+
const missionSummary = memberMissionSummary(owner, missionMap, root);
|
|
10532
|
+
// A human or odd-typed member never takes a task: it would be claimed and
|
|
10533
|
+
// then stranded when the run refuses the member.
|
|
10534
|
+
const pulled = pullTask && !prompt && !missionSummary.live && !memberRefusalFor(targetMember, paths.memberFile)
|
|
10535
|
+
? memberGoTask(owner, root, { claim: !dryRun, requireCheck: needsCheck })
|
|
10536
|
+
: null;
|
|
10537
|
+
if (needsCheck && !pulled) {
|
|
10538
|
+
const payload = { ok: true, action: 'member_go', mode: 'idle', member: owner, reason: 'no task with a check' };
|
|
10539
|
+
printJsonOrText(payload, [`${owner} is idle, no task with a check`], asJson);
|
|
10540
|
+
process.exitCode = require('../lib/member-up').IDLE_EXIT;
|
|
10541
|
+
return payload;
|
|
10542
|
+
}
|
|
10422
10543
|
const effectivePrompt = prompt
|
|
10544
|
+
|| pulled?.objective
|
|
10423
10545
|
|| goalPlane.goal?.next_push
|
|
10424
10546
|
|| goalPlane.goal?.title
|
|
10425
10547
|
|| extractMemberNorthStar(paths.memberDir, '');
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
const missionSummary = memberMissionSummary(owner, missionMap, root);
|
|
10548
|
+
const pulledTask = pulled ? { id: pulled.task.id, title: pulled.task.title, job: pulled.job } : null;
|
|
10549
|
+
if (pulled) whyPicked = `${whyPicked} Took the oldest open ${pulled.job} task from the list.`.trim();
|
|
10429
10550
|
const engineSeat = resolveMemberEngineSeat(owner, root);
|
|
10430
10551
|
const { autoQueueMemberNextPush, closeMemberHandoffsForReceiver } = require('./mission');
|
|
10431
10552
|
const subagentShell = buildMemberSubagentShell(owner, fm.role || 'Team Member', engineSeat.engine_id, effectivePrompt, root);
|
|
10432
10553
|
|
|
10433
|
-
if (effectivePrompt) {
|
|
10554
|
+
if (effectivePrompt && !pulled) {
|
|
10434
10555
|
try {
|
|
10435
10556
|
autoQueueMemberNextPush(owner, effectivePrompt, missionSummary.live?.objective || '', root);
|
|
10436
10557
|
} catch {}
|
|
@@ -10494,11 +10615,13 @@ async function memberSteerOrDispatch(...rawArgs) {
|
|
|
10494
10615
|
engine_reason: engineSeat.reason,
|
|
10495
10616
|
prompt: effectivePrompt,
|
|
10496
10617
|
why: whyPicked,
|
|
10618
|
+
task: pulledTask,
|
|
10497
10619
|
command,
|
|
10498
10620
|
subagent_shell: subagentShell,
|
|
10499
10621
|
};
|
|
10500
10622
|
const lines = [
|
|
10501
10623
|
`Routed to ${owner} on engine ${engineSeat.engine_id}: ${whyPicked}`,
|
|
10624
|
+
...(pulledTask ? [`Task: ${pulledTask.title}`] : []),
|
|
10502
10625
|
`Objective: ${effectivePrompt}`,
|
|
10503
10626
|
`Next: ${command}`,
|
|
10504
10627
|
];
|
|
@@ -10511,7 +10634,25 @@ async function memberSteerOrDispatch(...rawArgs) {
|
|
|
10511
10634
|
}
|
|
10512
10635
|
const hasExplicitEngine = flagArgs.includes('--engine');
|
|
10513
10636
|
const nextFlags = hasExplicitEngine ? flagArgs : ['--engine', engineSeat.engine_id, ...flagArgs];
|
|
10514
|
-
return memberRun(owner, effectivePrompt, '--ignite', ...nextFlags);
|
|
10637
|
+
if (!pulled) return memberRun(owner, effectivePrompt, '--ignite', ...nextFlags);
|
|
10638
|
+
// A pulled task runs with the check it requires, not the member's own.
|
|
10639
|
+
const required = require('../lib/member-task-pick').taskCheck(pulled.task).command;
|
|
10640
|
+
const runFlags = required ? [...stripKnownFlags(nextFlags, ['--verify'], []), '--verify', required] : nextFlags;
|
|
10641
|
+
let result = null;
|
|
10642
|
+
try {
|
|
10643
|
+
result = await memberRunWith(owner, [effectivePrompt, '--ignite', ...runFlags], { quiet: asJson });
|
|
10644
|
+
} catch (error) {
|
|
10645
|
+
memberGoFinishTask(owner, pulled.task, null, error);
|
|
10646
|
+
throw error;
|
|
10647
|
+
}
|
|
10648
|
+
const task = memberGoFinishTask(owner, pulled.task, result);
|
|
10649
|
+
const merged = { ...(result || {}), task };
|
|
10650
|
+
if (asJson) console.log(JSON.stringify(needsCheck
|
|
10651
|
+
? { task, work_step: { summary: result?.work_step?.summary || null }, error: result?.error || null }
|
|
10652
|
+
: merged));
|
|
10653
|
+
else if (task.outcome === 'review') console.log(`Task "${task.title}" is ready for review.`);
|
|
10654
|
+
else console.log(`Task "${task.title}" went back on the list: ${task.reason}`);
|
|
10655
|
+
return merged;
|
|
10515
10656
|
}
|
|
10516
10657
|
|
|
10517
10658
|
async function memberGo(...rawArgs) {
|
|
@@ -10522,7 +10663,7 @@ async function memberGo(...rawArgs) {
|
|
|
10522
10663
|
}
|
|
10523
10664
|
const nonFlags = rawArgs.filter((a) => a && !String(a).startsWith('-'));
|
|
10524
10665
|
if (nonFlags.length > 0) {
|
|
10525
|
-
return
|
|
10666
|
+
return memberSteerOrDispatchWith({ pullTask: true }, rawArgs);
|
|
10526
10667
|
}
|
|
10527
10668
|
const root = process.cwd();
|
|
10528
10669
|
const teamDir = path.join(root, 'atris', 'team');
|
|
@@ -10542,7 +10683,155 @@ async function memberGo(...rawArgs) {
|
|
|
10542
10683
|
const fallback = findAllMembers(teamDir).filter(isFleetMember)[0];
|
|
10543
10684
|
if (fallback) chosen = memberSlugFromRecord(fallback);
|
|
10544
10685
|
}
|
|
10545
|
-
return
|
|
10686
|
+
return memberSteerOrDispatchWith({ pullTask: true }, [chosen, ...rawArgs]);
|
|
10687
|
+
}
|
|
10688
|
+
|
|
10689
|
+
// Where a running `atris member up` leaves its pid, so a whole-team sleep
|
|
10690
|
+
// can end it.
|
|
10691
|
+
function memberUpPidFile(root) {
|
|
10692
|
+
return path.join(root, '.atris', 'state', 'member-up.json');
|
|
10693
|
+
}
|
|
10694
|
+
|
|
10695
|
+
// A member slept by name stays asleep for member up until someone wakes it;
|
|
10696
|
+
// a whole-team sleep only rests the team for now.
|
|
10697
|
+
function memberSleepingByChoice(latest) {
|
|
10698
|
+
return Boolean(latest && latest.status === 'paused' && latest.stop_reason === 'member-sleep'
|
|
10699
|
+
&& latest.sleep_target === 'member');
|
|
10700
|
+
}
|
|
10701
|
+
|
|
10702
|
+
function memberUpTeam(root) {
|
|
10703
|
+
const { isMemberParked } = require('../lib/member-park');
|
|
10704
|
+
const missionMap = memberRunMissionMap(root);
|
|
10705
|
+
return findAllMembers(path.join(root, 'atris', 'team')).filter(isFleetMember).map((record) => {
|
|
10706
|
+
const name = memberSlugFromRecord(record);
|
|
10707
|
+
const summary = memberMissionSummary(name, missionMap, root);
|
|
10708
|
+
return {
|
|
10709
|
+
name,
|
|
10710
|
+
parked: isMemberParked(root, name),
|
|
10711
|
+
human: Boolean(memberRefusalFor(name, path.join(root, 'atris', 'team', name, 'MEMBER.md'))),
|
|
10712
|
+
sleeping: memberSleepingByChoice(summary.latest),
|
|
10713
|
+
live: Boolean(summary.live_mission),
|
|
10714
|
+
};
|
|
10715
|
+
});
|
|
10716
|
+
}
|
|
10717
|
+
|
|
10718
|
+
// The task `member go <name>` would claim, skipping ones already handed out.
|
|
10719
|
+
// Looks only; the child claims it.
|
|
10720
|
+
function memberUpPick(root, name, taken) {
|
|
10721
|
+
try {
|
|
10722
|
+
const taskDb = require('../lib/task-db');
|
|
10723
|
+
const { fittingTasks } = require('../lib/member-task-pick');
|
|
10724
|
+
const db = taskDb.open();
|
|
10725
|
+
const job = memberGoJob(name, root);
|
|
10726
|
+
return fittingTasks(taskDb, db, { workspaceRoot: taskDb.workspaceRoot(root), job, requireCheck: true })
|
|
10727
|
+
.find((task) => !taken.has(task.id)) || null;
|
|
10728
|
+
} catch {
|
|
10729
|
+
return null;
|
|
10730
|
+
}
|
|
10731
|
+
}
|
|
10732
|
+
|
|
10733
|
+
function memberUpSpawn(root, name, onExit) {
|
|
10734
|
+
const { spawn } = require('child_process');
|
|
10735
|
+
const logDir = path.join(root, '.atris', 'state', 'member-up');
|
|
10736
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
10737
|
+
const logFile = path.join(logDir, `${name}.log`);
|
|
10738
|
+
const out = fs.openSync(logFile, 'a');
|
|
10739
|
+
const start = fs.fstatSync(out).size;
|
|
10740
|
+
let child;
|
|
10741
|
+
try {
|
|
10742
|
+
child = spawn(process.execPath, [path.join(__dirname, '..', 'bin', 'atris.js'), 'member', 'go', name, '--needs-check', '--json'], {
|
|
10743
|
+
cwd: root,
|
|
10744
|
+
detached: true,
|
|
10745
|
+
// Children keep their log handle when the foreground supervisor stops.
|
|
10746
|
+
stdio: ['ignore', out, out],
|
|
10747
|
+
env: process.env,
|
|
10748
|
+
});
|
|
10749
|
+
} finally {
|
|
10750
|
+
fs.closeSync(out);
|
|
10751
|
+
}
|
|
10752
|
+
child.on('error', (error) => onExit(null, { ok: false, reason: error.message }));
|
|
10753
|
+
child.on('close', (code, signal) => {
|
|
10754
|
+
let output = '';
|
|
10755
|
+
let fd;
|
|
10756
|
+
try {
|
|
10757
|
+
fd = fs.openSync(logFile, 'r');
|
|
10758
|
+
const size = fs.fstatSync(fd).size;
|
|
10759
|
+
const from = Math.max(start, size - 64 * 1024);
|
|
10760
|
+
const buf = Buffer.alloc(Math.max(0, size - from));
|
|
10761
|
+
const read = fs.readSync(fd, buf, 0, buf.length, from);
|
|
10762
|
+
output = buf.subarray(0, read).toString('utf8');
|
|
10763
|
+
} catch {} finally {
|
|
10764
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
10765
|
+
}
|
|
10766
|
+
const { memberUpResult } = require('../lib/member-up');
|
|
10767
|
+
onExit(code, memberUpResult(output, '', signal));
|
|
10768
|
+
});
|
|
10769
|
+
return child;
|
|
10770
|
+
}
|
|
10771
|
+
|
|
10772
|
+
async function memberUp(...args) {
|
|
10773
|
+
if (hasFlag(args, '--help') || hasFlag(args, '-h')) {
|
|
10774
|
+
console.log('Usage: atris member up [--max N] [--only a,b] [--dry-run]');
|
|
10775
|
+
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 stays idle; it never runs its own goal instead.');
|
|
10777
|
+
console.log('--only limits it to those members. Same as atris member wake 5 or atris member wake a b.');
|
|
10778
|
+
console.log('Ctrl-C or atris member sleep --all ends it. Runs it started keep going.');
|
|
10779
|
+
return;
|
|
10780
|
+
}
|
|
10781
|
+
const root = process.cwd();
|
|
10782
|
+
const { createMemberUp, runMemberUp, parseMemberUpArgs, DEFAULT_MAX } = require('../lib/member-up');
|
|
10783
|
+
const parsed = parseMemberUpArgs(args);
|
|
10784
|
+
if (parsed.error) {
|
|
10785
|
+
console.error(parsed.error);
|
|
10786
|
+
process.exitCode = 1;
|
|
10787
|
+
return;
|
|
10788
|
+
}
|
|
10789
|
+
const { max, only } = parsed;
|
|
10790
|
+
const deps = {
|
|
10791
|
+
listMembers: () => memberUpTeam(root),
|
|
10792
|
+
pickTask: (name, taken) => memberUpPick(root, name, taken),
|
|
10793
|
+
spawn: (name, onExit) => memberUpSpawn(root, name, onExit),
|
|
10794
|
+
now: () => Date.now(),
|
|
10795
|
+
};
|
|
10796
|
+
if (parsed.dryRun) {
|
|
10797
|
+
const up = createMemberUp(deps, { max, only, dryRun: true });
|
|
10798
|
+
const started = up.check();
|
|
10799
|
+
if (!started.length && up.working() >= up.max) {
|
|
10800
|
+
console.log(`nothing to start: ${up.working()} already working, the limit is ${up.max}`);
|
|
10801
|
+
} else if (!started.length) {
|
|
10802
|
+
console.log('nothing to start: no free member has a task with a runnable check');
|
|
10803
|
+
}
|
|
10804
|
+
return started;
|
|
10805
|
+
}
|
|
10806
|
+
const pidFile = memberUpPidFile(root);
|
|
10807
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true });
|
|
10808
|
+
fs.writeFileSync(pidFile, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
|
|
10809
|
+
const loop = runMemberUp(deps, { max, only });
|
|
10810
|
+
const end = () => loop.stop();
|
|
10811
|
+
process.once('SIGINT', end);
|
|
10812
|
+
process.once('SIGTERM', end);
|
|
10813
|
+
const who = only ? ` for ${only.join(', ')}` : '';
|
|
10814
|
+
console.log(`team is up${who} (at most ${max || DEFAULT_MAX} at once). Ctrl-C to stop.`);
|
|
10815
|
+
await loop.done;
|
|
10816
|
+
process.removeListener('SIGINT', end);
|
|
10817
|
+
process.removeListener('SIGTERM', end);
|
|
10818
|
+
try {
|
|
10819
|
+
if (JSON.parse(fs.readFileSync(pidFile, 'utf8')).pid === process.pid) fs.unlinkSync(pidFile);
|
|
10820
|
+
} catch {}
|
|
10821
|
+
const working = loop.up.running();
|
|
10822
|
+
console.log(working.length ? `team is down; still finishing: ${working.join(', ')}` : 'team is down');
|
|
10823
|
+
}
|
|
10824
|
+
|
|
10825
|
+
// Ends a running `atris member up`. Returns true when one was told to stop.
|
|
10826
|
+
function stopMemberUp(root) {
|
|
10827
|
+
try {
|
|
10828
|
+
const { pid } = JSON.parse(fs.readFileSync(memberUpPidFile(root), 'utf8'));
|
|
10829
|
+
if (!Number.isInteger(pid) || pid === process.pid) return false;
|
|
10830
|
+
process.kill(pid, 'SIGTERM');
|
|
10831
|
+
return true;
|
|
10832
|
+
} catch {
|
|
10833
|
+
return false;
|
|
10834
|
+
}
|
|
10546
10835
|
}
|
|
10547
10836
|
|
|
10548
10837
|
function memberSleep(target, ...args) {
|
|
@@ -10568,6 +10857,7 @@ function memberSleep(target, ...args) {
|
|
|
10568
10857
|
status: 'paused',
|
|
10569
10858
|
paused_at: new Date().toISOString(),
|
|
10570
10859
|
stop_reason: 'member-sleep',
|
|
10860
|
+
sleep_target: filterMember ? 'member' : 'all',
|
|
10571
10861
|
next_action: `sleeping; wake with: atris member go ${owner || ''}`.trim(),
|
|
10572
10862
|
updated_at: new Date().toISOString(),
|
|
10573
10863
|
};
|
|
@@ -10577,9 +10867,11 @@ function memberSleep(target, ...args) {
|
|
|
10577
10867
|
slept.push({ mission_id: m.id, member: owner || 'mission-lead', objective: m.objective });
|
|
10578
10868
|
}
|
|
10579
10869
|
|
|
10870
|
+
const supervisorStopped = !filterMember && stopMemberUp(root);
|
|
10580
10871
|
const payload = {
|
|
10581
10872
|
ok: true,
|
|
10582
10873
|
action: 'member_sleep',
|
|
10874
|
+
supervisor_stopped: supervisorStopped,
|
|
10583
10875
|
target: filterMember || 'all',
|
|
10584
10876
|
slept_count: slept.length,
|
|
10585
10877
|
slept,
|
|
@@ -10595,6 +10887,7 @@ function memberSleep(target, ...args) {
|
|
|
10595
10887
|
`All members are already resting.`,
|
|
10596
10888
|
`Wake anytime: ${payload.wake_command}`,
|
|
10597
10889
|
];
|
|
10890
|
+
if (supervisorStopped) lines.unshift('Stopped member up; it starts nobody new.');
|
|
10598
10891
|
printJsonOrText(payload, lines, asJson);
|
|
10599
10892
|
return payload;
|
|
10600
10893
|
}
|
|
@@ -10927,13 +11220,25 @@ function memberExport(...args) {
|
|
|
10927
11220
|
return result;
|
|
10928
11221
|
}
|
|
10929
11222
|
|
|
11223
|
+
function memberTidy(...args) {
|
|
11224
|
+
const { tidyMemberCopies, formatTidy } = require('../lib/member-tidy');
|
|
11225
|
+
const dirIndex = args.indexOf('--dir');
|
|
11226
|
+
const result = tidyMemberCopies({
|
|
11227
|
+
dryRun: args.includes('--dry-run'),
|
|
11228
|
+
worktreesDir: dirIndex >= 0 ? args[dirIndex + 1] : undefined,
|
|
11229
|
+
});
|
|
11230
|
+
console.log(formatTidy(result));
|
|
11231
|
+
if (result.aborted) process.exitCode = 1;
|
|
11232
|
+
return result;
|
|
11233
|
+
}
|
|
11234
|
+
|
|
10930
11235
|
async function memberCommand(subcommand, ...args) {
|
|
10931
11236
|
// Subcommands that take a member name as args[0] otherwise treat `--help` as
|
|
10932
11237
|
// a name and error with "Member '--help' not found". `create`/`new` handle
|
|
10933
11238
|
// help themselves (with subcommand-specific usage), leave those alone.
|
|
10934
11239
|
const HELP_AWARE_SUBCOMMANDS = new Set([
|
|
10935
11240
|
'create', 'new', 'run', 'install', 'result', 'dispatch', 'route', 'feed', 'stream',
|
|
10936
|
-
'card', 'cards', 'go', 'work', 'steer', 'sleep', 'rest', '1on1', 'interview',
|
|
11241
|
+
'card', 'cards', 'go', 'work', 'up', 'steer', 'sleep', 'rest', '1on1', 'interview',
|
|
10937
11242
|
'room', 'ui', 'day', 'standup', 'handoff', 'handoffs',
|
|
10938
11243
|
]);
|
|
10939
11244
|
const explicitHelp = subcommand === 'help' || subcommand === '--help' || subcommand === '-h'
|
|
@@ -10971,9 +11276,17 @@ async function memberCommand(subcommand, ...args) {
|
|
|
10971
11276
|
case 'tick':
|
|
10972
11277
|
return memberTick(args[0], ...args.slice(1));
|
|
10973
11278
|
case 'wake':
|
|
10974
|
-
return
|
|
11279
|
+
return require('../lib/member-verbs').memberWakeVerb(args, {
|
|
11280
|
+
wake: memberWake,
|
|
11281
|
+
up: memberUp,
|
|
11282
|
+
});
|
|
10975
11283
|
case 'run':
|
|
10976
|
-
return
|
|
11284
|
+
return require('../lib/member-verbs').memberRunVerb(args, {
|
|
11285
|
+
root: process.cwd(),
|
|
11286
|
+
aliasOf: memberRuntimeKind,
|
|
11287
|
+
run: memberRun,
|
|
11288
|
+
dispatch: memberDispatch,
|
|
11289
|
+
});
|
|
10977
11290
|
case 'result': {
|
|
10978
11291
|
const code = require('../lib/member-result').memberResultCommand(args);
|
|
10979
11292
|
if (code) process.exitCode = code;
|
|
@@ -10982,6 +11295,8 @@ async function memberCommand(subcommand, ...args) {
|
|
|
10982
11295
|
case 'go':
|
|
10983
11296
|
case 'work':
|
|
10984
11297
|
return memberGo(...args);
|
|
11298
|
+
case 'up':
|
|
11299
|
+
return memberUp(...args);
|
|
10985
11300
|
case 'steer':
|
|
10986
11301
|
return memberSteerOrDispatch(...args);
|
|
10987
11302
|
case 'sleep':
|
|
@@ -11006,6 +11321,8 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11006
11321
|
return memberFeed(...args);
|
|
11007
11322
|
case 'board':
|
|
11008
11323
|
return require('../lib/member-board').memberBoardCommand(...args);
|
|
11324
|
+
case 'stop':
|
|
11325
|
+
return require('../lib/member-board').memberStopCommand(args[0]);
|
|
11009
11326
|
case 'card':
|
|
11010
11327
|
case 'cards':
|
|
11011
11328
|
return memberCard(args[0], ...args.slice(1));
|
|
@@ -11042,6 +11359,8 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11042
11359
|
return memberArchive(args[0]);
|
|
11043
11360
|
case 'purge-archived':
|
|
11044
11361
|
return memberPurgeArchived(...args);
|
|
11362
|
+
case 'tidy':
|
|
11363
|
+
return memberTidy(...args);
|
|
11045
11364
|
case undefined:
|
|
11046
11365
|
case '--json':
|
|
11047
11366
|
if (fs.existsSync(path.join(process.cwd(), 'atris', 'team'))) {
|
|
@@ -11056,6 +11375,9 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11056
11375
|
}
|
|
11057
11376
|
return memberSteerOrDispatch(subcommand, ...args);
|
|
11058
11377
|
}
|
|
11378
|
+
console.log('run "<job>" pick the right member and run it: atris member run "fix the flaky login test"');
|
|
11379
|
+
console.log('wake <N|names> keep members working: atris member wake 5 or atris member wake navigator executor');
|
|
11380
|
+
console.log('sleep [name|--all] rest the team: atris member sleep --all');
|
|
11059
11381
|
console.log('');
|
|
11060
11382
|
console.log('Usage: atris member <subcommand|"<prompt>"> [name]');
|
|
11061
11383
|
console.log('');
|
|
@@ -11072,6 +11394,7 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11072
11394
|
console.log(' install <slug> [--update] Install missing files, or refresh template skills');
|
|
11073
11395
|
console.log(' chat <name> "..." Send a message to this member\'s desk in Atris Desktop');
|
|
11074
11396
|
console.log(' list [--json] Show all team members');
|
|
11397
|
+
console.log(' tidy [--dry-run] Put away member copies of the repo whose work already landed');
|
|
11075
11398
|
console.log(' export [--dry-run] Write each member as a subagent for Claude Code, Cursor, and Codex');
|
|
11076
11399
|
console.log(' activate <name> Symlink member skills, show context and permissions');
|
|
11077
11400
|
console.log(' upgrade <name> Convert flat file (name.md) to directory format');
|
|
@@ -11080,10 +11403,12 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11080
11403
|
console.log(' goal <name> "..." Create/update a member long-term goal');
|
|
11081
11404
|
console.log(' goal-from-mission <name> Create/reuse a goal from MISSION.md and now.md');
|
|
11082
11405
|
console.log(' goal-from-score <name> Create/reuse an active goal from Team score evidence');
|
|
11083
|
-
console.log(' wake <name> Decide tick/wait/ask/stop, then run one bounded work step on the roster engine (--plan to only decide)');
|
|
11406
|
+
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
|
+
console.log(' run "<job>" Pick the best member for the job and run it (same as dispatch "<job>" --ignite)');
|
|
11084
11408
|
console.log(' run <name> ["..."] Start a budgeted member mission, or run its active Mission Runtime');
|
|
11085
11409
|
console.log(' result <name> Newest run result: run record, receipt, artifacts, number [--since <iso>] [--json]');
|
|
11086
11410
|
console.log(' dispatch "<idea>" Route an idea to the best specialist member and start work');
|
|
11411
|
+
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)');
|
|
11087
11412
|
console.log(' feed [--member <n>] Show a plain-language linear feed of real work across the team');
|
|
11088
11413
|
console.log(' card [name|--all] Show a member\'s roadmap, unlocked skills, live work, and recent wins');
|
|
11089
11414
|
console.log(' loop <name|--all> Repeat wake on a bounded cadence with a no-overlap lease');
|
|
@@ -11144,7 +11469,9 @@ async function memberCommand(subcommand, ...args) {
|
|
|
11144
11469
|
}
|
|
11145
11470
|
|
|
11146
11471
|
module.exports = {
|
|
11472
|
+
memberRunWantsReferee,
|
|
11147
11473
|
proposalPromptForGoal,
|
|
11474
|
+
memberGoFinishTask,
|
|
11148
11475
|
appendMemberGoalLog,
|
|
11149
11476
|
appendProjectLog,
|
|
11150
11477
|
memberCommand,
|