atris 3.58.8 → 3.60.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/AGENTS.md +3 -2
- package/atris/skills/engines/SKILL.md +28 -1
- package/bin/atris.js +13 -0
- package/commands/autopilot.js +75 -5
- package/commands/clean.js +17 -0
- package/commands/doc-health.js +11 -6
- package/commands/engine.js +386 -4
- package/commands/host.js +62 -0
- package/commands/member.js +57 -6
- package/commands/mission.js +72 -8
- package/commands/one-lap.js +58 -3
- package/commands/run.js +4 -1
- package/commands/scout.js +1 -1
- package/commands/search.js +5 -3
- package/commands/team.js +20 -3
- package/commands/unknowns.js +5 -0
- package/commands/verify.js +12 -4
- package/commands/visualize.js +20 -3
- package/commands/workflow.js +17 -6
- package/lib/dispatch-scout.js +31 -22
- package/lib/engine-registry.js +1482 -13
- package/lib/feedback-triage.js +2 -0
- package/lib/fleet.js +107 -11
- package/lib/host.js +461 -0
- package/lib/intents.js +15 -0
- package/lib/known-commands.js +1 -1
- package/lib/map-refs.js +44 -1
- package/lib/member-engine.js +253 -0
- package/lib/roster-markdown.js +206 -0
- package/lib/roster-models.js +179 -0
- package/lib/runner-command.js +94 -14
- package/lib/self-drive.js +9 -1
- package/lib/team-lineup.js +167 -0
- package/lib/wish-audit.js +10 -6
- package/lib/wish-delegate.js +10 -0
- package/package.json +1 -1
- package/templates/members/host/MEMBER.md +23 -0
- package/templates/members/host/MISSION.md +11 -0
- package/templates/members/host/SOUL.md +7 -0
- package/templates/members/host/context/why.md +3 -0
- package/templates/members/host/goals.json +5 -0
- package/templates/members/host/goals.md +3 -0
- package/templates/members/host/skills/host/SKILL.md +15 -0
package/AGENTS.md
CHANGED
|
@@ -171,8 +171,9 @@ named none. These rules are the difference, in priority order:
|
|
|
171
171
|
7. **Final report = files changed + verify command + its exact exit/output.**
|
|
172
172
|
Judges read the worktree diff, not your prose; an empty report with a clean
|
|
173
173
|
diff beats a confident report with no diff.
|
|
174
|
-
8. **Update
|
|
175
|
-
|
|
174
|
+
8. **Update the map sections you touched:** routing rows in `atris/MAP.md`,
|
|
175
|
+
file:line refs in `atris/refs/MAP-NOTES.md` (refs drift on their own clock;
|
|
176
|
+
stale refs contradict closed lessons for months).
|
|
176
177
|
9. **Real runtime over mocks:** if you fixed live behavior, the regression test
|
|
177
178
|
reproduces it against the real runtime, not a mock that can stay green
|
|
178
179
|
through breakage.
|
|
@@ -24,6 +24,32 @@ tags:
|
|
|
24
24
|
|
|
25
25
|
One contract, eleven live profiles. The orchestrator writes a bounded task prompt, dispatches it to an engine, then **independently verifies, lands, and pushes** the result. Engines never self-certify.
|
|
26
26
|
|
|
27
|
+
## Pick the job owner first
|
|
28
|
+
|
|
29
|
+
Before choosing an engine for search, build, or review, run `atris engine roster` and use that job's lead worker. Only deviate when the user names an engine.
|
|
30
|
+
|
|
31
|
+
The roster is a markdown file the owner can edit any time: `atris/ROSTER.md` for this project, `~/.atris/ROSTER.md` for every project on this machine. A project job wins over the machine job with the same name. Each `## job` heading lists its workers in order, one per line, naming the tool and the model:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
## review
|
|
35
|
+
- codex, model: gpt-6-astra, effort: medium, max: 20 min
|
|
36
|
+
- claude code, model: opus 5.5
|
|
37
|
+
|
|
38
|
+
## small build
|
|
39
|
+
- devin, model: swe-2-max, until 2026-10-24
|
|
40
|
+
- grok, model: grok 4.7 fast
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The first worker that is ready and not expired leads; the rest are its backups, in order. Tools take engine ids or `claude code`, `atris fast`, and `gemini`. The older one-line shape (`build: claude opus 5.5, backup codex`) still reads the same, and the first `atris engine assign` rewrites the file into sections. `assign <job> <tool>` sets the lead and keeps the rest; `--add` puts a worker at the end, `--remove <tool>` drops one, `--clear` drops the job, and `--everywhere` writes the machine file. `atris engine roster --available` lists the tools and models on this machine.
|
|
44
|
+
|
|
45
|
+
A job's list is also its team. When a task splits into parallel parts, give the parts to that job's workers in order, first worker first, instead of piling them all on the lead. Heavy models like astra and fable should only judge: have the search lead write them a trimmed brief first.
|
|
46
|
+
|
|
47
|
+
Changes for one shell only go in a session file above both rosters. Set `ATRIS_ROSTER_SESSION=<name>` (agent shells have no terminal, so they must), then `atris engine assign <job> <tool> --session`. A session job replaces that job's whole list for this shell; `atris engine roster session` shows the changes and `atris engine roster session clear` drops them. A session nobody used for a day is dropped on its own.
|
|
48
|
+
|
|
49
|
+
Jobs are open-ended: besides search, build, and review, the owner can add a job like `## small build`, or `## quick fixes (like build)` when the name does not say its kind. For a small, tightly specified build slice, use the small build lead when `atris engine roster` shows one; bigger builds use build.
|
|
50
|
+
|
|
51
|
+
Team members pick automatically. Navigators, researchers, and scouts do search; validators, reviewers, judges, critics, and verifiers do review; everyone else builds. A line under `## team` overrides that, with a job (`- researcher: search`) or a tool and model (`- judge: claude code, model: opus 5.5`). `member run`, missions with an owner, and autopilot's plan, do, and review use the member's pick; `--engine` still wins. `atris engine roster` lists the team and warns about any line it could not use.
|
|
52
|
+
|
|
27
53
|
## three verbs
|
|
28
54
|
|
|
29
55
|
- ask: `atris engine <name> "<question>"`; pin a model with `atris engine <name> --model <model> "<question>"`
|
|
@@ -80,7 +106,8 @@ Headless dispatch permissions (verified 2026-08-11): `codex exec`, `grok`, and `
|
|
|
80
106
|
|
|
81
107
|
## Picking an engine
|
|
82
108
|
|
|
83
|
-
- **
|
|
109
|
+
- **Cheap bounded errands**: Devin `swe-2-max` while its Free tag holds (check `devin models list`).
|
|
110
|
+
- **Multi-file or long build**: Use the project's build pick. If there is no pick, use the router's choice.
|
|
84
111
|
- **Judgment-heavy build**: Opus 5.5 subagent (Claude row).
|
|
85
112
|
- **Review / deep judgment**: FABLE profile; Codex `gpt-6-sol` or Opus 5.5 as second validator.
|
|
86
113
|
- **Quick fix**: Cursor.
|
package/bin/atris.js
CHANGED
|
@@ -1248,6 +1248,7 @@ if (!command || !knownCommands.includes(command)) {
|
|
|
1248
1248
|
error: command ? `unknown command: ${command}` : 'unknown command',
|
|
1249
1249
|
command: command || null,
|
|
1250
1250
|
input: rawNaturalArgs.join(' '),
|
|
1251
|
+
suggestions: suggestCommands(command),
|
|
1251
1252
|
usage: 'atris help',
|
|
1252
1253
|
}, null, 2));
|
|
1253
1254
|
process.exit(2);
|
|
@@ -1849,6 +1850,16 @@ function showWelcomeVisualization() {
|
|
|
1849
1850
|
console.log(row('goal', endgameState.horizon || endgameState.slug));
|
|
1850
1851
|
}
|
|
1851
1852
|
|
|
1853
|
+
// Every new chat sees which tool and model does each job, from the same
|
|
1854
|
+
// resolver as atris engine roster. A roster that cannot be read drops the
|
|
1855
|
+
// line; it never stops the boot.
|
|
1856
|
+
try {
|
|
1857
|
+
const teamLine = require('../lib/team-lineup').bootTeamLine(cwd, { width: 80 - 11 });
|
|
1858
|
+
if (teamLine) console.log(row('team', teamLine));
|
|
1859
|
+
} catch {
|
|
1860
|
+
// Silent: the lineup is a hint, not a gate.
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1852
1863
|
const hasNoTasks = glance.active === 0
|
|
1853
1864
|
&& glance.review === 0
|
|
1854
1865
|
&& glance.reviewCertified === 0;
|
|
@@ -3288,6 +3299,8 @@ if (command === 'guide') {
|
|
|
3288
3299
|
.then(() => process.exit(0))
|
|
3289
3300
|
.catch((err) => { console.error(`✗ Error: ${err.message || err}`); process.exit(1); });
|
|
3290
3301
|
}
|
|
3302
|
+
} else if (command === 'host') {
|
|
3303
|
+
require('../commands/host').hostCommand();
|
|
3291
3304
|
} else if (command === 'social') {
|
|
3292
3305
|
require('../commands/social').socialCommand()
|
|
3293
3306
|
.then(() => process.exit(0))
|
package/commands/autopilot.js
CHANGED
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
runnerAvailabilityFailureMessage,
|
|
19
19
|
} = require('../lib/runner-command');
|
|
20
20
|
const { findStalePages, findStaleTasks, healBrokenMapRefs } = require('./clean');
|
|
21
|
+
const { mapNotesPath } = require('../lib/map-refs');
|
|
21
22
|
const {
|
|
22
23
|
buildScorecardData,
|
|
23
24
|
readScorecards,
|
|
@@ -483,19 +484,73 @@ function execPhaseCommandSync(cmd, opts = {}) {
|
|
|
483
484
|
}
|
|
484
485
|
}
|
|
485
486
|
|
|
487
|
+
// plan, do, and review run as the navigator, executor, and validator. When
|
|
488
|
+
// the roster names an engine for that member, the phase spawns it. A runner
|
|
489
|
+
// set in the environment still wins, and with no roster line behind the
|
|
490
|
+
// member the configured runner runs exactly as before.
|
|
491
|
+
const AUTOPILOT_PHASE_MEMBERS = Object.freeze({ plan: 'navigator', do: 'executor', review: 'validator' });
|
|
492
|
+
const RUNNER_ENV_OVERRIDES = Object.freeze([
|
|
493
|
+
'ATRIS_RUNNER_PROFILE',
|
|
494
|
+
'ATRIS_RUNNER_COMMAND_TEMPLATE',
|
|
495
|
+
'ATRIS_RUNNER_BIN',
|
|
496
|
+
'ATRIS_CLAUDE_COMMAND_TEMPLATE',
|
|
497
|
+
'ATRIS_CLAUDE_BIN',
|
|
498
|
+
]);
|
|
499
|
+
|
|
500
|
+
function autopilotPhaseEngine(phase, cwd = process.cwd()) {
|
|
501
|
+
const member = AUTOPILOT_PHASE_MEMBERS[phase];
|
|
502
|
+
if (!member) return null;
|
|
503
|
+
if (RUNNER_ENV_OVERRIDES.some((name) => String(process.env[name] || '').trim())) return null;
|
|
504
|
+
const { memberRosterEngine } = require('../lib/member-engine');
|
|
505
|
+
return memberRosterEngine(member, cwd, { requireMember: false });
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// The phase's launch command and, when its roster line sets "max 20 min",
|
|
509
|
+
// the time cap in ms (null means the caller's own timeout stands).
|
|
510
|
+
function buildPhaseRunner(phase, promptFile, cwd = process.cwd(), allowedTools = 'Bash,Read,Write,Edit,Glob,Grep') {
|
|
511
|
+
const picked = autopilotPhaseEngine(phase, cwd);
|
|
512
|
+
if (!picked) return { command: buildRunnerCommand({ promptFile, allowedTools }), timeoutMs: null };
|
|
513
|
+
if (process.env.ATRIS_ROUTER_EXPLAIN !== '0') console.error(picked.reason);
|
|
514
|
+
const previous = process.env.ATRIS_RUNNER_PROFILE;
|
|
515
|
+
process.env.ATRIS_RUNNER_PROFILE = picked.engine.id;
|
|
516
|
+
try {
|
|
517
|
+
const command = buildRunnerCommand({
|
|
518
|
+
promptFile,
|
|
519
|
+
allowedTools,
|
|
520
|
+
...(picked.model ? { model: picked.model } : {}),
|
|
521
|
+
...(picked.engine.roster_effort ? { effort: picked.engine.roster_effort } : {}),
|
|
522
|
+
});
|
|
523
|
+
const maxSeconds = Number(picked.engine.roster_max_seconds) || 0;
|
|
524
|
+
return { command, timeoutMs: maxSeconds > 0 ? maxSeconds * 1000 : null };
|
|
525
|
+
} finally {
|
|
526
|
+
if (previous === undefined) delete process.env.ATRIS_RUNNER_PROFILE;
|
|
527
|
+
else process.env.ATRIS_RUNNER_PROFILE = previous;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function buildPhaseRunnerCommand(phase, promptFile, cwd = process.cwd()) {
|
|
532
|
+
return buildPhaseRunner(phase, promptFile, cwd).command;
|
|
533
|
+
}
|
|
534
|
+
|
|
486
535
|
/**
|
|
487
536
|
* Run a phase via the configured runner subprocess.
|
|
488
537
|
*/
|
|
489
538
|
function executePhaseDetailed(phase, context, options = {}) {
|
|
490
|
-
const { verbose = false
|
|
539
|
+
const { verbose = false } = options;
|
|
540
|
+
let timeout = options.timeout || PHASE_TIMEOUT;
|
|
491
541
|
|
|
492
542
|
const prompt = buildPrompt(phase, context, options);
|
|
493
543
|
const tmpFile = path.join(process.cwd(), '.autopilot-prompt.tmp');
|
|
494
544
|
fs.writeFileSync(tmpFile, prompt);
|
|
495
545
|
|
|
496
546
|
try {
|
|
497
|
-
|
|
498
|
-
|
|
547
|
+
let cmd = options.cmdOverride;
|
|
548
|
+
if (!cmd) {
|
|
549
|
+
const runner = buildPhaseRunner(phase, tmpFile);
|
|
550
|
+
cmd = runner.command;
|
|
551
|
+
// A roster time cap stops the phase at that time.
|
|
552
|
+
if (runner.timeoutMs) timeout = runner.timeoutMs;
|
|
553
|
+
}
|
|
499
554
|
const env = { ...process.env };
|
|
500
555
|
delete env.CLAUDECODE;
|
|
501
556
|
const output = execPhaseCommandSync(cmd, {
|
|
@@ -625,6 +680,8 @@ function getContextFiles(phase, options = {}) {
|
|
|
625
680
|
agentSpec && fs.existsSync(path.join(cwd, agentSpec)) ? agentSpec : null,
|
|
626
681
|
'atris/PERSONA.md',
|
|
627
682
|
'atris/MAP.md',
|
|
683
|
+
// Exact file:line refs live beside the short routing map.
|
|
684
|
+
mapNotesPath(cwd),
|
|
628
685
|
'atris/TODO.md',
|
|
629
686
|
fs.existsSync(path.join(cwd, 'atris/lessons.md')) ? 'atris/lessons.md' : null,
|
|
630
687
|
(() => { const { logFile } = getLogPath(); return fs.existsSync(logFile) ? path.relative(cwd, logFile) : null; })(),
|
|
@@ -1357,15 +1414,25 @@ function parseProposedBlock(lines) {
|
|
|
1357
1414
|
return Object.keys(proposed).length ? proposed : null;
|
|
1358
1415
|
}
|
|
1359
1416
|
|
|
1417
|
+
// Plan review is review work, so it runs on the review pick like the review
|
|
1418
|
+
// phase, with that line's model, effort, and time cap. With no roster line
|
|
1419
|
+
// behind it, the configured runner runs exactly as before.
|
|
1420
|
+
function buildPlanReviewRunner(promptFile, cwd = process.cwd(), timeout = 180000) {
|
|
1421
|
+
const runner = buildPhaseRunner('review', promptFile, cwd, 'Bash,Read,Grep,Glob');
|
|
1422
|
+
return { command: runner.command, timeoutMs: runner.timeoutMs || timeout };
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1360
1425
|
/**
|
|
1361
|
-
* Default executor for plan-review: spawn a fresh
|
|
1426
|
+
* Default executor for plan-review: spawn a fresh runner call on the review pick.
|
|
1362
1427
|
* Kept thin so tests can inject a stub via options.planReviewExec.
|
|
1363
1428
|
*/
|
|
1364
1429
|
function defaultPlanReviewExecutor(prompt, { cwd, timeout = 180000 } = {}) {
|
|
1365
1430
|
const tmpFile = path.join(cwd, '.autopilot-plan-review.tmp');
|
|
1366
1431
|
fs.writeFileSync(tmpFile, prompt);
|
|
1367
1432
|
try {
|
|
1368
|
-
const
|
|
1433
|
+
const runner = buildPlanReviewRunner(tmpFile, cwd, timeout);
|
|
1434
|
+
const cmd = runner.command;
|
|
1435
|
+
timeout = runner.timeoutMs;
|
|
1369
1436
|
const env = { ...process.env };
|
|
1370
1437
|
delete env.CLAUDECODE;
|
|
1371
1438
|
const output = execPhaseCommandSync(cmd, {
|
|
@@ -3872,6 +3939,9 @@ module.exports = {
|
|
|
3872
3939
|
isPhaseKillError,
|
|
3873
3940
|
execPhaseCommandSync,
|
|
3874
3941
|
executePhaseDetailed,
|
|
3942
|
+
buildPhaseRunnerCommand,
|
|
3943
|
+
buildPhaseRunner,
|
|
3944
|
+
buildPlanReviewRunner,
|
|
3875
3945
|
buildAutopilotGlassLog,
|
|
3876
3946
|
writeAutopilotGlassLog,
|
|
3877
3947
|
lessonSlug
|
package/commands/clean.js
CHANGED
|
@@ -358,9 +358,26 @@ function findStaleTasks(atrisDir) {
|
|
|
358
358
|
* Detects both out-of-bounds AND drift (symbol moved to different line)
|
|
359
359
|
* Returns { healed: number, unhealable: array }
|
|
360
360
|
*/
|
|
361
|
+
// The boot map routes; atris/refs/MAP-NOTES.md holds the deep file:line refs.
|
|
362
|
+
// Heal both. Entries from the notes file carry `map` so reports can say where.
|
|
363
|
+
const MAP_NOTES_FILE = path.join('refs', 'MAP-NOTES.md');
|
|
364
|
+
|
|
361
365
|
function healBrokenMapRefs(cwd, atrisDir, dryRun = false, homeDir = os.homedir()) {
|
|
362
366
|
const mapFile = path.join(atrisDir, 'MAP.md');
|
|
363
367
|
if (!fs.existsSync(mapFile)) return { healed: 0, unhealable: [] };
|
|
368
|
+
const result = healMapFileRefs(cwd, mapFile, dryRun, homeDir);
|
|
369
|
+
const notesFile = path.join(atrisDir, MAP_NOTES_FILE);
|
|
370
|
+
if (!fs.existsSync(notesFile)) return result;
|
|
371
|
+
const notes = healMapFileRefs(cwd, notesFile, dryRun, homeDir);
|
|
372
|
+
const map = path.relative(cwd, notesFile).split(path.sep).join('/');
|
|
373
|
+
return {
|
|
374
|
+
healed: result.healed + notes.healed,
|
|
375
|
+
unhealable: [...result.unhealable, ...notes.unhealable.map((entry) => ({ ...entry, map }))],
|
|
376
|
+
replacements: [...result.replacements, ...notes.replacements.map((entry) => ({ ...entry, map }))],
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function healMapFileRefs(cwd, mapFile, dryRun, homeDir) {
|
|
364
381
|
|
|
365
382
|
let mapContent = fs.readFileSync(mapFile, 'utf8');
|
|
366
383
|
const unhealable = [];
|
package/commands/doc-health.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { resolveWorkspaceRoot } = require('../lib/mission-root');
|
|
6
6
|
const { readText } = require('./brain');
|
|
7
|
-
const {
|
|
7
|
+
const { checkMapDocs, fixMapDocs } = require('../lib/map-refs');
|
|
8
8
|
|
|
9
9
|
const BOOT_FILES = [
|
|
10
10
|
'CLAUDE.md', 'AGENTS.md', 'atris/atris.md', 'atris/MAP.md',
|
|
@@ -76,9 +76,10 @@ function collectMap(root, text, featureNames, memberNames) {
|
|
|
76
76
|
const files = [...paths].map(file => ({ path: file, exists: fs.existsSync(path.resolve(root, file)) }));
|
|
77
77
|
const existing = files.filter(file => file.exists).length;
|
|
78
78
|
const pathScore = files.length ? existing / files.length : 0;
|
|
79
|
-
|
|
79
|
+
// Line refs are checked in the boot map and in the notes file it points to.
|
|
80
|
+
const { refs: all, ...refs } = checkMapDocs(root, text);
|
|
80
81
|
refs.flagged = all.filter(ref => ref.status !== 'ok').map(ref => ({
|
|
81
|
-
map_line: ref.map_line, ref: ref.raw, symbol: ref.symbol, status: ref.status,
|
|
82
|
+
doc: ref.doc, map_line: ref.map_line, ref: ref.raw, symbol: ref.symbol, status: ref.status,
|
|
82
83
|
...(ref.line ? { line: ref.line } : {}), ...(ref.lines ? { lines: ref.lines } : {}),
|
|
83
84
|
...(ref.reason ? { reason: ref.reason } : {}),
|
|
84
85
|
}));
|
|
@@ -286,21 +287,25 @@ function refsLine(refs) {
|
|
|
286
287
|
return `map refs: ${refs.ok} right, ${refs.moved} moved, ${refs.ambiguous} unclear, ${gone} gone`;
|
|
287
288
|
}
|
|
288
289
|
|
|
290
|
+
function docLine(ref) {
|
|
291
|
+
return `${ref.doc === 'atris/MAP.md' ? 'MAP' : ref.doc} line ${ref.map_line}`;
|
|
292
|
+
}
|
|
293
|
+
|
|
289
294
|
function describeLeft(ref) {
|
|
290
|
-
const where =
|
|
295
|
+
const where = `${docLine(ref)}: ${ref.raw}${ref.symbol ? ` (${ref.symbol})` : ''}`;
|
|
291
296
|
if (ref.status === 'ambiguous') return `${where} found on lines ${ref.lines.join(', ')}`;
|
|
292
297
|
if (ref.status === 'missing-file') return `${where} file is gone`;
|
|
293
298
|
return `${where} ${ref.reason}`;
|
|
294
299
|
}
|
|
295
300
|
|
|
296
301
|
function fixRefsCommand(root, asJson) {
|
|
297
|
-
const { changes, left } =
|
|
302
|
+
const { changes, left } = fixMapDocs(root);
|
|
298
303
|
if (asJson) {
|
|
299
304
|
console.log(JSON.stringify({ ok: true, action: 'doc-health fix-refs', changes, left }, null, 2));
|
|
300
305
|
return 0;
|
|
301
306
|
}
|
|
302
307
|
const lines = [changes.length ? `map refs: moved ${changes.length}` : 'map refs: nothing to move.'];
|
|
303
|
-
for (const change of changes) lines.push(`
|
|
308
|
+
for (const change of changes) lines.push(` ${docLine(change)}: ${change.from} -> ${change.to} (${change.symbol})`);
|
|
304
309
|
if (left.length) {
|
|
305
310
|
lines.push(`left for a human: ${left.length}`);
|
|
306
311
|
for (const ref of left) lines.push(` ${describeLeft(ref)}`);
|