atris 3.74.4 → 3.74.5
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/FOR_AGENTS.md +12 -12
- package/atris/AGENTS.md +7 -7
- package/atris/CLAUDE.md +10 -10
- package/atris/policies/atris-backend.md +1 -1
- package/atris/policies/atris-design.md +71 -71
- package/atris/policies/design-seed.md +28 -28
- package/atris/policies/writing.md +1 -1
- package/atris/skills/apps/SKILL.md +3 -3
- package/atris/skills/atris-feedback/SKILL.md +2 -2
- package/atris/skills/autopilot/SKILL.md +10 -10
- package/atris/skills/blocks/SKILL.md +16 -16
- package/atris/skills/calendar/SKILL.md +1 -1
- package/atris/skills/clawhub/atris/SKILL.md +4 -4
- package/atris/skills/clawhub/philosophy-of-work/SKILL.md +7 -7
- package/atris/skills/create-app/SKILL.md +18 -18
- package/atris/skills/create-member/SKILL.md +11 -11
- package/atris/skills/design/SKILL.md +1 -1
- package/atris/skills/drive/SKILL.md +25 -25
- package/atris/skills/email-agent/SKILL.md +11 -11
- package/atris/skills/endgame/SKILL.md +33 -33
- package/atris/skills/magic-inbox/SKILL.md +15 -15
- package/atris/skills/notion/SKILL.md +1 -1
- package/atris/skills/ramp/SKILL.md +7 -7
- package/atris/skills/research-search/SKILL.md +7 -7
- package/atris/skills/slides/SKILL.md +7 -7
- package/atris/skills/tidy/SKILL.md +11 -11
- package/atris/skills/wake/SKILL.md +11 -11
- package/atris/skills/writing/SKILL.md +5 -5
- package/atris/team/_template/SOUL.md +3 -3
- package/atris/team/brainstormer/MEMBER.md +2 -2
- package/atris/team/launcher/MEMBER.md +2 -2
- package/atris/team/mission-lead/MISSION.md +7 -7
- package/atris/team/opus-overnight/MISSION.md +16 -16
- package/atris/team/opus-overnight/STEERING.md +5 -5
- package/atris.md +9 -0
- package/bin/atris.js +16 -3
- package/commands/campaign.js +17 -2
- package/commands/card.js +19 -12
- package/commands/engine.js +92 -17
- package/commands/gm.js +30 -0
- package/commands/idea.js +263 -0
- package/commands/init.js +11 -2
- package/commands/land.js +39 -18
- package/commands/mission.js +4 -3
- package/commands/ninety-nine.js +288 -0
- package/commands/one-lap.js +46 -27
- package/commands/reel.js +12 -7
- package/commands/sync.js +10 -2
- package/lib/campaign-keep.js +23 -8
- package/lib/campaign-loop.js +24 -5
- package/lib/claude-boot-block.js +7 -4
- package/lib/engine-registry.js +139 -1
- package/lib/fleet.js +11 -2
- package/lib/gm-bet.js +338 -34
- package/lib/gm-phone-page.js +325 -33
- package/lib/gm-phone.js +5 -1
- package/lib/gm-season.js +14 -0
- package/lib/headless-chrome.js +39 -0
- package/lib/idea-gate.js +33 -0
- package/lib/idea-inbox.js +220 -0
- package/lib/idea-ledger.js +196 -0
- package/lib/idea-tick.js +384 -0
- package/lib/known-commands.js +1 -1
- package/lib/member-engine.js +1 -0
- package/lib/ninety-nine-sync.js +156 -0
- package/lib/ninety-nine.js +247 -0
- package/lib/opening-lines.js +94 -0
- package/lib/roster-markdown.js +39 -1
- package/lib/roster-models.js +1 -1
- package/lib/roster-swap.js +353 -0
- package/lib/spawn-env.js +29 -2
- package/lib/wish-audit.js +14 -0
- package/lib/yes-lock.js +343 -253
- package/lib/zen-apply.js +5 -3
- package/lib/zen.js +13 -1
- package/package.json +1 -1
- package/templates/business-starter/CLAUDE.md +7 -7
- package/templates/business-starter/MAP.md +1 -1
- package/templates/business-starter/TODO.md +1 -1
- package/templates/business-starter/atris.md +2 -2
- package/templates/business-starter/context/README.md +6 -6
- package/templates/business-starter/context/live-workspace.md +1 -1
- package/templates/business-starter/goals.md +1 -1
- package/templates/business-starter/instructions.md +1 -1
- package/templates/business-starter/memory.md +4 -4
- package/templates/business-starter/persona.md +3 -3
- package/templates/business-starter/policies/LESSONS.md +1 -1
- package/templates/business-starter/reports/README.md +1 -1
- package/templates/business-starter/skills/README.md +1 -1
- package/templates/business-starter/team/README.md +1 -1
- package/templates/business-starter/team/START_HERE.md +1 -1
- package/templates/business-starter/team/_template/SOUL.md +3 -3
- package/templates/business-starter/team/comms/SOUL.md +2 -2
- package/templates/business-starter/team/ops/SOUL.md +4 -4
- package/templates/business-starter/team/research/SOUL.md +3 -3
- package/templates/business-starter/wiki/index.md +1 -1
- package/templates/research-canonical/CLAUDE.md +7 -7
- package/templates/research-canonical/MAP.md +1 -1
- package/templates/research-canonical/TODO.md +1 -1
- package/templates/research-canonical/atris.md +2 -2
- package/templates/research-canonical/context/README.md +8 -8
- package/templates/research-canonical/context/live-workspace.md +1 -1
- package/templates/research-canonical/goals.md +2 -2
- package/templates/research-canonical/instructions.md +1 -1
- package/templates/research-canonical/memory.md +4 -4
- package/templates/research-canonical/persona.md +1 -1
- package/templates/research-canonical/policies/LESSONS.md +1 -1
- package/templates/research-canonical/reports/README.md +1 -1
- package/templates/research-canonical/skills/README.md +1 -1
- package/templates/research-canonical/team/README.md +1 -1
- package/templates/research-canonical/team/eval/SOUL.md +1 -1
- package/templates/research-canonical/team/experiment/SOUL.md +2 -2
- package/templates/research-canonical/team/hypothesis/SOUL.md +3 -3
- package/templates/research-canonical/team/literature/SOUL.md +2 -2
- package/templates/research-canonical/wiki/briefs/research-program.md +1 -1
- package/templates/research-canonical/wiki/index.md +1 -1
- package/utils/auth.js +1 -0
package/lib/campaign-loop.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
+
const fs = require('fs');
|
|
8
9
|
const path = require('path');
|
|
9
10
|
const crypto = require('crypto');
|
|
10
11
|
const { spawn } = require('child_process');
|
|
@@ -37,6 +38,18 @@ function proposalId(objective) {
|
|
|
37
38
|
return `m-${slug || 'mission'}-${crypto.randomBytes(3).toString('hex')}`;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
function briefPath(root, campaignId) {
|
|
42
|
+
return path.join(root, 'atris', 'campaigns', campaignId, 'BRIEF.md');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readBrief(root, campaignId) {
|
|
46
|
+
try {
|
|
47
|
+
return fs.readFileSync(briefPath(root, campaignId), 'utf8').trim();
|
|
48
|
+
} catch {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
function rankCampaignMembers(view, rsiSummary = null, events = null) {
|
|
41
54
|
const seats = Array.isArray(view && view.seats) ? view.seats.map((s) => s.member).filter(Boolean) : [];
|
|
42
55
|
const candidates = [...new Set([view && view.owner, ...seats].filter(Boolean))];
|
|
@@ -99,7 +112,7 @@ function completionOrderedMissions(view, events) {
|
|
|
99
112
|
|
|
100
113
|
// What the owner's engine reads before proposing: the goal, how it is judged,
|
|
101
114
|
// what has been tried, and how it went.
|
|
102
|
-
function proposalPrompt(view, count, judgeOutput = null, lessons = '', rsiSummary = null, events = null) {
|
|
115
|
+
function proposalPrompt(view, count, judgeOutput = null, lessons = '', rsiSummary = null, events = null, brief = '') {
|
|
103
116
|
const tried = view.missions.slice(-12).map((m) => `- ${m.id}: ${m.status}${m.outcome ? ` (${m.outcome})` : ''}${m.cost_usd !== null ? `, $${m.cost_usd}` : ''}${typeof m.reward === 'number' ? `, reward ${m.reward >= 0 ? '+' : ''}${m.reward}` : ''}`).join('\n') || '- none yet';
|
|
104
117
|
const ranked = rankCampaignMembers(view, rsiSummary, events);
|
|
105
118
|
const memberLine = ranked.map((r) => {
|
|
@@ -113,6 +126,7 @@ function proposalPrompt(view, count, judgeOutput = null, lessons = '', rsiSummar
|
|
|
113
126
|
`You plan the next missions for an Atris campaign. Answer with JSON only.`,
|
|
114
127
|
``,
|
|
115
128
|
`Campaign: ${view.title}`,
|
|
129
|
+
brief ? `Brief:\n${brief}` : '',
|
|
116
130
|
`Judge (the campaign is done when this command exits 0): ${view.judge}`,
|
|
117
131
|
`Last judge reading: ${view.last_judge ? `${view.last_judge.passed ? 'passed' : 'failed'}${typeof view.last_judge.reading === 'number' ? ` (${view.last_judge.reading} of 1)` : ''}: ${view.answer.text || ''}` : 'never run'}`,
|
|
118
132
|
judgeOutput ? `Last judge output:\n\`\`\`\n${judgeOutput.replace(/```/g, "'''")}\n\`\`\`` : '',
|
|
@@ -231,7 +245,8 @@ function makeLoop(root, options = {}, deps = {}) {
|
|
|
231
245
|
});
|
|
232
246
|
rsiSummary = summarizeStoreRsi(store);
|
|
233
247
|
} catch {}
|
|
234
|
-
const
|
|
248
|
+
const brief = (deps.brief ? deps.brief(id) : readBrief(root, id)).trim();
|
|
249
|
+
const prompt = proposalPrompt(current, count, latestJudgeOutput(events), deps.lessons ? deps.lessons(id) : '', rsiSummary, events, brief);
|
|
235
250
|
const answer = await ask(root, current, prompt);
|
|
236
251
|
const proposals = parseProposals(answer.text, current, count, rsiSummary, events);
|
|
237
252
|
// Planning costs money too. It rides on the first mission it produced as a
|
|
@@ -242,8 +257,10 @@ function makeLoop(root, options = {}, deps = {}) {
|
|
|
242
257
|
if (typeof answer.cost_usd === 'number') plan.cost_usd = answer.cost_usd;
|
|
243
258
|
proposals.forEach((proposal, index) => {
|
|
244
259
|
const missionId = proposalId(proposal.objective);
|
|
260
|
+
const key = claimKey(proposal.objective);
|
|
261
|
+
if (brief && !proposal.objective.includes(brief)) proposal.objective = `${proposal.objective}\n\n${brief}`;
|
|
245
262
|
append(id, 'mission_proposed', { mission_id: missionId, member: proposal.member, objective: proposal.objective, verify: proposal.verify, estimate_usd: proposal.estimate_usd || undefined }, proposal.member);
|
|
246
|
-
append(id, 'mission_claimed', { mission_id: missionId, claim_key:
|
|
263
|
+
append(id, 'mission_claimed', { mission_id: missionId, claim_key: key }, proposal.member);
|
|
247
264
|
if (index === 0) append(id, 'step', { mission_id: missionId, ...plan }, current.owner);
|
|
248
265
|
});
|
|
249
266
|
return proposals;
|
|
@@ -264,8 +281,10 @@ function makeLoop(root, options = {}, deps = {}) {
|
|
|
264
281
|
async function startAndRun(id, work) {
|
|
265
282
|
const current = view(id);
|
|
266
283
|
const reserve = work.estimate_usd || defaultReserve(current);
|
|
284
|
+
const brief = (deps.brief ? deps.brief(id) : readBrief(root, id)).trim();
|
|
285
|
+
const objective = brief && !work.objective.includes(brief) ? `${work.objective}\n\n${brief}` : work.objective;
|
|
267
286
|
const started = await cli([
|
|
268
|
-
'mission', 'start',
|
|
287
|
+
'mission', 'start', objective, '--mission-id', work.mission_id, '--owner', work.member,
|
|
269
288
|
'--runner', runner, '--verify', work.verify || current.judge, '--campaign', id, '--reserve', String(reserve),
|
|
270
289
|
'--max-wall', String(missionMinutes * 60), '--json',
|
|
271
290
|
]);
|
|
@@ -419,4 +438,4 @@ function makeLoop(root, options = {}, deps = {}) {
|
|
|
419
438
|
return { step, tick, run, propose };
|
|
420
439
|
}
|
|
421
440
|
|
|
422
|
-
module.exports = { ACTIVITY_LINE, askOwner, makeLoop, parseProposals, proposalPrompt, rankCampaignMembers, readingKey, runCli };
|
|
441
|
+
module.exports = { ACTIVITY_LINE, askOwner, briefPath, makeLoop, parseProposals, proposalPrompt, rankCampaignMembers, readBrief, readingKey, runCli };
|
package/lib/claude-boot-block.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { openingLinesForRoot, renderOpeningLinesBlock } = require('./opening-lines');
|
|
4
|
+
|
|
3
5
|
const ATRIS_START_MARKER = '<!-- ATRIS:START - Auto-generated, do not edit -->';
|
|
4
6
|
const ATRIS_END_MARKER = '<!-- ATRIS:END -->';
|
|
5
7
|
|
|
6
|
-
function renderAtrisClaudeBootBlock() {
|
|
8
|
+
function renderAtrisClaudeBootBlock(openingLines = openingLinesForRoot()) {
|
|
9
|
+
const opening = renderOpeningLinesBlock(openingLines, 'CLAUDE.md');
|
|
7
10
|
return `${ATRIS_START_MARKER}
|
|
8
11
|
## Atris System
|
|
9
12
|
|
|
@@ -14,12 +17,12 @@ You are now running inside Atris, an AI workspace operating system.
|
|
|
14
17
|
After displaying the boot output, respond to the user naturally.
|
|
15
18
|
|
|
16
19
|
The person does not know atris words. Translate their plain English; when unsure run \`atris guide "<their words>"\`.
|
|
17
|
-
${ATRIS_END_MARKER}
|
|
20
|
+
${opening ? `\n${opening}\n` : ''}${ATRIS_END_MARKER}
|
|
18
21
|
`;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
function upsertAtrisClaudeBootBlock(source = '') {
|
|
22
|
-
const atrisBlock = renderAtrisClaudeBootBlock();
|
|
24
|
+
function upsertAtrisClaudeBootBlock(source = '', options = {}) {
|
|
25
|
+
const atrisBlock = renderAtrisClaudeBootBlock(options.openingLines);
|
|
23
26
|
const startIdx = source.indexOf('<!-- ATRIS:START');
|
|
24
27
|
const prependBlock = (preserved) => {
|
|
25
28
|
const remainder = preserved.replace(/^\n+/, '');
|
package/lib/engine-registry.js
CHANGED
|
@@ -914,6 +914,7 @@ function rosterPickFromValue(value, role, options = {}) {
|
|
|
914
914
|
const primary = resolveRosterWords(main);
|
|
915
915
|
if (!options.anyEngine && !engineCanTake(primary.engine, role)) throw new Error(`${primary.engine} cannot do ${kind} work`);
|
|
916
916
|
let backup = null;
|
|
917
|
+
const moreBackups = [];
|
|
917
918
|
let until = '';
|
|
918
919
|
let untilYearless = '';
|
|
919
920
|
let maxSeconds = null;
|
|
@@ -966,6 +967,11 @@ function rosterPickFromValue(value, role, options = {}) {
|
|
|
966
967
|
} else if (backupMatch && !backup) {
|
|
967
968
|
backup = resolveRosterWords(backupMatch[1]);
|
|
968
969
|
if (!options.anyEngine && !engineCanTake(backup.engine, role)) throw new Error(`the backup ${backup.engine} cannot do ${kind} work`);
|
|
970
|
+
} else if (backupMatch) {
|
|
971
|
+
// every backup after the first steps in next, in written order
|
|
972
|
+
const more = resolveRosterWords(backupMatch[1]);
|
|
973
|
+
if (!options.anyEngine && !engineCanTake(more.engine, role)) throw new Error(`the backup ${more.engine} cannot do ${kind} work`);
|
|
974
|
+
moreBackups.push({ engine: more.engine, model: more.model || '', ...(more.effort ? { effort: more.effort } : {}) });
|
|
969
975
|
} else if (untilMatch && !until) {
|
|
970
976
|
const words = untilMatch[1].trim();
|
|
971
977
|
until = parseRosterUntilWords(words);
|
|
@@ -989,6 +995,7 @@ function rosterPickFromValue(value, role, options = {}) {
|
|
|
989
995
|
backup: backup ? backup.engine : '',
|
|
990
996
|
...(backup && backup.model ? { backup_model: backup.model } : {}),
|
|
991
997
|
...(backup && backup.effort ? { backup_effort: backup.effort } : {}),
|
|
998
|
+
...(moreBackups.length ? { more_backups: moreBackups } : {}),
|
|
992
999
|
...(maxSeconds ? { max_seconds: maxSeconds } : {}),
|
|
993
1000
|
...(prep ? { prep } : {}),
|
|
994
1001
|
until,
|
|
@@ -1102,6 +1109,19 @@ function pickWorkers(pick) {
|
|
|
1102
1109
|
...(pick.line ? { line: pick.line } : {}),
|
|
1103
1110
|
});
|
|
1104
1111
|
}
|
|
1112
|
+
for (const more of Array.isArray(pick.more_backups) ? pick.more_backups : []) {
|
|
1113
|
+
list.push({
|
|
1114
|
+
engine: more.engine,
|
|
1115
|
+
model: more.model || '',
|
|
1116
|
+
...(more.effort ? { effort: more.effort } : {}),
|
|
1117
|
+
...cap,
|
|
1118
|
+
until: '',
|
|
1119
|
+
never_expires: true,
|
|
1120
|
+
lenient_model: true,
|
|
1121
|
+
backup: true,
|
|
1122
|
+
...(pick.line ? { line: pick.line } : {}),
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1105
1125
|
return list;
|
|
1106
1126
|
}
|
|
1107
1127
|
|
|
@@ -1114,6 +1134,7 @@ function parseRosterMarkdown(text, file, options = {}) {
|
|
|
1114
1134
|
const display = rosterFileLabel(file, options.root || process.cwd());
|
|
1115
1135
|
const picks = {};
|
|
1116
1136
|
const team = [];
|
|
1137
|
+
const shortlist = [];
|
|
1117
1138
|
const warnings = [];
|
|
1118
1139
|
const warn = (entry, message) => warnings.push({ file: display, line: entry.lineNumber, text: entry.raw, message });
|
|
1119
1140
|
const scan = scanRosterMarkdown(text);
|
|
@@ -1171,6 +1192,27 @@ function parseRosterMarkdown(text, file, options = {}) {
|
|
|
1171
1192
|
}
|
|
1172
1193
|
continue;
|
|
1173
1194
|
}
|
|
1195
|
+
if (entry.section === 'picks') {
|
|
1196
|
+
// "- claude · <model>": the tool, a middle dot, then the model as the
|
|
1197
|
+
// owner writes it. Without the dot the words read like a roster line.
|
|
1198
|
+
const dot = entry.value.indexOf('\u00b7');
|
|
1199
|
+
const toolWords = dot === -1 ? '' : entry.value.slice(0, dot).trim();
|
|
1200
|
+
const modelWords = dot === -1 ? '' : entry.value.slice(dot + 1).trim();
|
|
1201
|
+
let engine = toolWords ? rosterToolName(toolWords) : '';
|
|
1202
|
+
let model = modelWords;
|
|
1203
|
+
if (dot === -1) {
|
|
1204
|
+
try {
|
|
1205
|
+
const read = resolveRosterWords(entry.value);
|
|
1206
|
+
engine = read.engine;
|
|
1207
|
+
model = read.model ? workerModelText(read.engine, read.model) : '';
|
|
1208
|
+
} catch {
|
|
1209
|
+
engine = '';
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
if (!engine) warn(entry, 'is not "tool · model", so the pick is skipped');
|
|
1213
|
+
else shortlist.push({ engine, model, line: entry.lineNumber, text: entry.raw, file: display });
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1174
1216
|
if (entry.malformed) {
|
|
1175
1217
|
warn(entry, entry.section === 'team' ? 'is not "member: job or engine", so it is skipped' : 'is not "job: engine", so it is skipped');
|
|
1176
1218
|
continue;
|
|
@@ -1217,7 +1259,7 @@ function parseRosterMarkdown(text, file, options = {}) {
|
|
|
1217
1259
|
warn(entry, `${error.message}, so ${label} uses the next pick in line`);
|
|
1218
1260
|
}
|
|
1219
1261
|
}
|
|
1220
|
-
return { picks, team, warnings };
|
|
1262
|
+
return { picks, team, warnings, ...(shortlist.length ? { shortlist } : {}) };
|
|
1221
1263
|
}
|
|
1222
1264
|
|
|
1223
1265
|
function readRosterMarkdownLayer(scope, file, options = {}) {
|
|
@@ -1602,6 +1644,25 @@ function assignedWorkerLines(key, engineName, options, context) {
|
|
|
1602
1644
|
});
|
|
1603
1645
|
const heading = jobHeadingText(key, builtIn ? '' : kind);
|
|
1604
1646
|
if (options.add) return { lines: [...currentLines, line], heading };
|
|
1647
|
+
// keepLead (roster set): the new lead goes on top and every old worker
|
|
1648
|
+
// stays below it as written, the old lead first, so it becomes the first
|
|
1649
|
+
// backup. A line that already names this tool and model moves up as
|
|
1650
|
+
// written instead. A new line takes the old lead's time cap and prep: those
|
|
1651
|
+
// belong to the job, not the tool.
|
|
1652
|
+
if (options.keepLead) {
|
|
1653
|
+
const found = currentWorkers.findIndex((worker) => worker.engine === engine.id && (worker.model || '') === model);
|
|
1654
|
+
if (found !== -1) return { lines: [currentLines[found], ...currentLines.filter((text, index) => index !== found)], heading };
|
|
1655
|
+
const old = currentWorkers[0] || {};
|
|
1656
|
+
const carried = workerLineText({
|
|
1657
|
+
engine: engine.id,
|
|
1658
|
+
model,
|
|
1659
|
+
...(effort ? { effort } : {}),
|
|
1660
|
+
...(maxSeconds || old.max_seconds ? { max_seconds: maxSeconds || old.max_seconds } : {}),
|
|
1661
|
+
...(prep || (old.prep && old.prep !== key) ? { prep: prep || old.prep } : {}),
|
|
1662
|
+
until: days ? rosterUntil(now, days) : '',
|
|
1663
|
+
});
|
|
1664
|
+
return { lines: [carried, ...currentLines], heading };
|
|
1665
|
+
}
|
|
1605
1666
|
// A note the person left at the end of the old lead line stays with the lead.
|
|
1606
1667
|
const leadNote = currentLines.length ? /\s*(<!--.*?-->)\s*$/.exec(currentLines[0]) : null;
|
|
1607
1668
|
const lead = leadNote ? `${line} ${leadNote[1]}` : line;
|
|
@@ -1690,6 +1751,79 @@ function setRosterPick(jobName, engineName, options = {}, root = process.cwd())
|
|
|
1690
1751
|
return target.scope === 'machine' ? readMachineRoster(options) : readProjectRoster(root, options).picks;
|
|
1691
1752
|
}
|
|
1692
1753
|
|
|
1754
|
+
// Give one member its own line under "## team" ("- builder: codex, model:
|
|
1755
|
+
// <name>"), replacing that member's line in place or adding it. Same files
|
|
1756
|
+
// and layers as setRosterPick (session, everywhere, or this project), and the
|
|
1757
|
+
// same rewrite into sections first. role is the member's kind of work; the
|
|
1758
|
+
// line must read back as a pick the engine can do, or nothing is written.
|
|
1759
|
+
// A member's one-line pick with only its lead changed: the old lead (its
|
|
1760
|
+
// tool, model, and effort) goes to the front of the backups, a backup that
|
|
1761
|
+
// already names the new lead goes, and every other part stays as written:
|
|
1762
|
+
// backups, max, prep, until, verified. null when the old value is not a pick
|
|
1763
|
+
// (a job name, or a line that does not read), so the whole value is replaced.
|
|
1764
|
+
function teamValueWithLead(oldValue, engineId, model, role, options = {}) {
|
|
1765
|
+
let old;
|
|
1766
|
+
try {
|
|
1767
|
+
old = rosterPickFromValue(oldValue, role, options);
|
|
1768
|
+
} catch {
|
|
1769
|
+
return null;
|
|
1770
|
+
}
|
|
1771
|
+
const parts = String(oldValue || '').split(',').map((part) => part.trim()).filter(Boolean);
|
|
1772
|
+
for (let i = parts.length - 1; i > 0; i -= 1) {
|
|
1773
|
+
if (/^\d{4}$/.test(parts[i])) parts.splice(i - 1, 2, `${parts[i - 1]} ${parts[i]}`);
|
|
1774
|
+
}
|
|
1775
|
+
const [main, ...rest] = parts;
|
|
1776
|
+
const inlineMax = /\s+(max\s+\d+(?:\.\d+)?\s*[a-z]+)$/i.exec(main || '');
|
|
1777
|
+
const extras = inlineMax ? [inlineMax[1], ...rest] : rest;
|
|
1778
|
+
const words = (engine, pinned, effort) => [rosterToolLabel(engine), pinned ? workerModelText(engine, pinned) : '', effort || ''].filter(Boolean).join(' ');
|
|
1779
|
+
const namesNewLead = (text) => {
|
|
1780
|
+
try {
|
|
1781
|
+
const read = resolveRosterWords(text);
|
|
1782
|
+
return read.engine === engineId && (read.model || '') === model;
|
|
1783
|
+
} catch {
|
|
1784
|
+
return false;
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
// model and effort parts belong to the old lead, so they go with it
|
|
1788
|
+
const kept = extras
|
|
1789
|
+
.filter((part) => !/^(model|effort)\s*(?::|\s)/i.test(part))
|
|
1790
|
+
.filter((part) => {
|
|
1791
|
+
const named = /^backup\s+(.+)$/i.exec(part);
|
|
1792
|
+
return !(named && namesNewLead(named[1]));
|
|
1793
|
+
});
|
|
1794
|
+
if (!(old.engine === engineId && (old.model || '') === model)) {
|
|
1795
|
+
const at = kept.findIndex((part) => /^backup\s+/i.test(part));
|
|
1796
|
+
kept.splice(at === -1 ? 0 : at, 0, `backup ${words(old.engine, old.model, old.effort)}`);
|
|
1797
|
+
}
|
|
1798
|
+
const lead = [rosterToolLabel(engineId), ...(model ? [`model: ${workerModelText(engineId, model)}`] : [])];
|
|
1799
|
+
return [...lead, ...kept].join(', ');
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function setTeamPick(memberName, engineName, options = {}, root = process.cwd()) {
|
|
1803
|
+
const { replaceTeamLine, scanRosterMarkdown } = require('./roster-markdown');
|
|
1804
|
+
const member = String(memberName || '').trim().toLowerCase();
|
|
1805
|
+
if (!/^[a-z0-9._-]+$/.test(member)) throw new Error(`"${memberName}" is not a member name`);
|
|
1806
|
+
const registry = readEngineRegistry(root, { persist: false });
|
|
1807
|
+
const engineId = rosterToolName(engineName);
|
|
1808
|
+
if (!registry.engines.some((entry) => entry.id === engineId)) throw new Error(`unknown engine "${engineName}"`);
|
|
1809
|
+
const model = normalizeRosterModel(engineId, options.model);
|
|
1810
|
+
if (model && !engineTakesModel(engineId)) throw new Error(`${engineId} runs one fixed model and cannot take "${model}"`);
|
|
1811
|
+
const role = options.role || 'executor';
|
|
1812
|
+
const target = rosterWriteTarget(options, root);
|
|
1813
|
+
const layer = target.layer || readProjectRoster(root, { ...options, registry });
|
|
1814
|
+
const file = target.file;
|
|
1815
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
|
|
1816
|
+
const base = existing !== null ? sectionedRosterText(existing, file, { ...options, root }) : rosterMarkdownFromPicks(layer.picks);
|
|
1817
|
+
// a swap changes only the lead of the member's line already in this file
|
|
1818
|
+
const line = scanRosterMarkdown(base).entries.find((entry) => entry.section === 'team' && !entry.malformed && entry.name.trim().toLowerCase() === member);
|
|
1819
|
+
const value = (line && teamValueWithLead(line.value, engineId, model, role, { root }))
|
|
1820
|
+
|| workerLineText({ engine: engineId, model }).replace(/^-\s+/, '');
|
|
1821
|
+
rosterPickFromValue(value, role, { root });
|
|
1822
|
+
writeAtomic(file, replaceTeamLine(base, member, value));
|
|
1823
|
+
if (target.scope === 'session') sweepStaleSessionRosters(options);
|
|
1824
|
+
return { member, engine: engineId, model, value, scope: target.scope, file };
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1693
1827
|
function renewRoster(roster, date) {
|
|
1694
1828
|
const next = { ...(roster || {}) };
|
|
1695
1829
|
for (const key of [...ENGINE_ROLES, ...customRosterJobKeys(next)]) {
|
|
@@ -2317,7 +2451,11 @@ module.exports = {
|
|
|
2317
2451
|
rosterChoiceForPick,
|
|
2318
2452
|
customRosterJobKeys,
|
|
2319
2453
|
setRosterPick,
|
|
2454
|
+
setTeamPick,
|
|
2320
2455
|
confirmRoster,
|
|
2456
|
+
engineHasRole,
|
|
2457
|
+
engineHosted,
|
|
2458
|
+
rosterToolName,
|
|
2321
2459
|
normalizeRosterModel,
|
|
2322
2460
|
currentClaudeModel,
|
|
2323
2461
|
claudeModelId,
|
package/lib/fleet.js
CHANGED
|
@@ -1922,6 +1922,10 @@ function reviewOnlyEngineEnvironment(worktreePath, options = {}) {
|
|
|
1922
1922
|
GIT_CONFIG_NOSYSTEM: '1',
|
|
1923
1923
|
CODEX_HOME: path.join(runtimeHome, '.codex'),
|
|
1924
1924
|
CLAUDE_CONFIG_DIR: path.join(runtimeHome, '.claude'),
|
|
1925
|
+
// Claude Code keeps its own temp files under /tmp/claude-<uid> unless this
|
|
1926
|
+
// is set. The sealed profile cannot read /tmp, so without it every sealed
|
|
1927
|
+
// claude build died at start ("EEXIST: mkdir /tmp/claude-501").
|
|
1928
|
+
CLAUDE_CODE_TMPDIR: runtimeTmp,
|
|
1925
1929
|
ATRIS_ONE_LAP_SANDBOX_PROFILE: sandboxProfile,
|
|
1926
1930
|
ATRIS_ONE_LAP_RUNTIME_DIR: runtimeTmp,
|
|
1927
1931
|
ATRIS_ONE_LAP_CONTROL_DIR: controlTmp,
|
|
@@ -3442,6 +3446,9 @@ async function runDispatchFlight({
|
|
|
3442
3446
|
// search" on its roster line), and an injectable ask for that prep pass.
|
|
3443
3447
|
prep = '',
|
|
3444
3448
|
prepAsk = undefined,
|
|
3449
|
+
// Extra house rules a caller appends to the builder's brief (atris idea
|
|
3450
|
+
// tick sends the four values and the no-force-push, no-hook-skip rules).
|
|
3451
|
+
brief = '',
|
|
3445
3452
|
} = {}) {
|
|
3446
3453
|
if (!engine) throw new Error('runDispatchFlight: engine is required');
|
|
3447
3454
|
if (!DISPATCH_CAPABLE.includes(engine)) {
|
|
@@ -3563,13 +3570,15 @@ async function runDispatchFlight({
|
|
|
3563
3570
|
}
|
|
3564
3571
|
const worktreePath = enforceRemoteBoundary ? remoteBoundary.worktreePath : landingWorktreePath;
|
|
3565
3572
|
const startCommit = yolo ? readStartCommit({ worktreePath }) : '';
|
|
3566
|
-
const
|
|
3573
|
+
const briefNote = String(brief || '').trim();
|
|
3574
|
+
const builtPrompt = promptOverride || buildFleetPrompt(task, { worktreePath, yolo: yolo && !localOnly });
|
|
3575
|
+
const basePrompt = briefNote ? `${builtPrompt}\n\n${briefNote}` : builtPrompt;
|
|
3567
3576
|
const safetyPrompt = proofOnly
|
|
3568
3577
|
? `${basePrompt}\n\nSafety boundary: edit and test this checkout only. Do not push, merge, deploy, publish, send messages, change cloud state, install dependencies, or use credentials.`
|
|
3569
3578
|
: basePrompt;
|
|
3570
3579
|
const trustedPrompt = trustedVerifier
|
|
3571
3580
|
? `${safetyPrompt}\n\nTrusted verifier (run it bare before reporting done): ${trustedVerifier}`
|
|
3572
|
-
: (localOnly ? safetyPrompt : promptOverride);
|
|
3581
|
+
: (localOnly ? safetyPrompt : (briefNote ? basePrompt : promptOverride));
|
|
3573
3582
|
prepared.push({
|
|
3574
3583
|
task,
|
|
3575
3584
|
taskId,
|