atris 3.64.0 → 3.66.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 +4 -0
- package/atris/skills/engines/SKILL.md +2 -2
- package/bin/atris.js +45 -2
- package/commands/agent-spawn.js +1 -0
- package/commands/autoland.js +62 -0
- package/commands/brain.js +4 -0
- package/commands/engine.js +203 -63
- package/commands/hire.js +189 -0
- package/commands/link.js +367 -0
- package/commands/loops.js +4 -1
- package/commands/mail.js +163 -0
- package/commands/member.js +8 -1
- package/commands/mission.js +3 -2
- package/commands/orb.js +1 -1
- package/commands/stream.js +22 -1
- package/commands/team.js +355 -67
- package/commands/xp.js +2 -0
- package/commands/youtube.js +1 -1
- package/lib/auto-accept-certified.js +172 -18
- package/lib/business-target.js +131 -0
- package/lib/cleanup-summary.js +129 -0
- package/lib/engine-ask.js +3 -2
- package/lib/engine-processes.js +430 -0
- package/lib/engine-registry.js +219 -18
- package/lib/idle-review-sweep.js +130 -0
- package/lib/known-commands.js +2 -2
- package/lib/land-green.js +18 -4
- package/lib/member-identity.js +56 -0
- package/lib/member-result.js +303 -0
- package/lib/roster-models.js +12 -1
- package/lib/roster-stale.js +241 -0
- package/lib/runner-command.js +4 -4
- package/lib/task-list-keeper.js +25 -4
- package/lib/team-lineup.js +10 -71
- package/lib/team-presence.js +224 -0
- package/lib/text-table.js +81 -0
- package/lib/trust-tiers.js +131 -14
- package/lib/verify-worktree-rewrite.js +115 -0
- package/lib/voice-gate.js +2 -0
- package/package.json +1 -1
- package/scripts/det/ytnotes +1 -1
package/README.md
CHANGED
|
@@ -295,10 +295,14 @@ An engine is the intelligence that builds a mission. Every installed headless co
|
|
|
295
295
|
|
|
296
296
|
The first ready worker leads and the rest are backups. `~/.atris/ROSTER.md` covers every project on the machine; `atris/ROSTER.md` overrides it for one project. A worker that stalls sits out 30 minutes and the next one takes over.
|
|
297
297
|
|
|
298
|
+
The roster warns when a worker runs an old model and a newer one in the same family is on this machine, and prints the command that moves it. It also warns when a pinned model has no proof in 30 days. A landed run is proof; so is `verified 2026-09-30` on the line after a quick smoke test.
|
|
299
|
+
|
|
298
300
|
```bash
|
|
299
301
|
atris engine roster # who does what, with each worker's recent record
|
|
300
302
|
atris engine assign build claude --model "opus 5.5" # change a job's lead
|
|
301
303
|
atris engine roster --available # tools and models on this machine
|
|
304
|
+
atris team # who really worked in the last 7 days, then quiet and idle
|
|
305
|
+
atris team --record # also save one row per member per day for the learning loop
|
|
302
306
|
```
|
|
303
307
|
|
|
304
308
|
Show every installed engine and the current default:
|
|
@@ -95,7 +95,7 @@ Smoke: `reply with the word OK`, run from `$HOME`, 120s cap, one at a time on a
|
|
|
95
95
|
| Cursor | `cursor-agent --trust --model composer-2.5 -p "<prompt>"` (run from the target repo) | `composer-2.5` | 2026-09-23 | 37.5 | `--trust` required for non-interactive. |
|
|
96
96
|
| Fable | `atris engine fable --timeout 120 --json "<question>"` | `claude-fable-5` | 2026-09-23 | 21.4 | Canonical read-only FABLE ask with receipt; scale `--timeout` to the work. |
|
|
97
97
|
| Composer | `cursor-agent --trust --model composer-2.5 -p "<prompt>"` | `composer-2.5` via Cursor | 2026-09-23 | 37.5 | Same binary as Cursor. |
|
|
98
|
-
| Haiku | `claude -p "<prompt>" --model claude-haiku-
|
|
98
|
+
| Haiku | `claude -p "<prompt>" --model claude-haiku-5-5 --output-format json --no-session-persistence` | `claude-haiku-5-5` | 2026-10-07 | 9.5 | Fast validation and bounded read-only checks. |
|
|
99
99
|
| Devin (build) | `devin -p --permission-mode dangerous --model swe-2-max --prompt-file <brief>` (run from an isolated worktree) | `swe-2-max` (Free tag in `devin models list`, 2026-09-23) | 2026-09-23 | 14.0 | In `-p` mode the default permission mode rejects shell commands too, so even a read-only audit dies after one step (2026-09-23). Keep the brief outside the worktree. Long runs (5+ min) can print nothing even on success: judge by the worktree diff. `devin cloud` outlives this machine. |
|
|
100
100
|
| Devin (search) | `devin -p --permission-mode dangerous --model swe-1.7-lightning -- "<prompt>"` | `swe-1.7-lightning` (paid, no Free tag on 2026-09-23) | 2026-09-23 | 13.9 | |
|
|
101
101
|
| Grok | `grok --always-approve --model grok-4.7 -p "<prompt>"` (run from the target repo) | `grok-4.7` | 2026-09-23 | 11.7 | Unpinned default is now `grok-4.7-build-fast`; `grok models` lists the menu. Uses grok.com login. `--best-of-n <N>` for tricky bounded builds. |
|
|
@@ -119,7 +119,7 @@ Headless dispatch permissions (verified 2026-08-11): `codex exec`, `grok`, and `
|
|
|
119
119
|
|
|
120
120
|
| Engine | Flag | Alternates on the live menu |
|
|
121
121
|
|--------|------|-----------------------------|
|
|
122
|
-
| Claude | `--model <id>` | `claude-opus-5-5`, `claude-haiku-
|
|
122
|
+
| Claude | `--model <id>` | `claude-opus-5-5`, `claude-haiku-5-5` |
|
|
123
123
|
| Devin | `--model <id>` | `swe-2-max`, `swe-2-high`, `swe-2-medium` (Free); `claude-opus-5-5-high`, `gpt-6-sol` (paid) |
|
|
124
124
|
| Cursor | `--model <id>` | `composer-2.5`, `grok-4.7-xhigh`, `claude-opus-5-thinking-high`; `--list-models` |
|
|
125
125
|
| Grok | `--model <id>` | `grok-4.7`, `grok-4.7-build-fast` (default), `grok-4.6`, `grok-4.5`; `grok models` |
|
package/bin/atris.js
CHANGED
|
@@ -632,6 +632,7 @@ function showHelpAll() {
|
|
|
632
632
|
console.log(' rsi - Read the Dream-RSI attempt ledger (trees, attempts, policy, dreams)');
|
|
633
633
|
console.log(` autoland - Approve the policy once; ${require('../lib/autoland').certifiedWorkLandsPhrase(process.cwd())}, you keep irreversible calls`);
|
|
634
634
|
console.log(' engine - engine registry, answer validation, dispatch flights, and live progress');
|
|
635
|
+
console.log(' link - What two things could become together: one line, why, a first thing to make, who makes it');
|
|
635
636
|
console.log(' ci - run github actions jobs locally with runs-on: atris');
|
|
636
637
|
console.log(' router - inspect ax lane outcomes and promote gated reflex overrides');
|
|
637
638
|
console.log(' sign - Co-author trailer on every commit in an atris workspace (on/off/status)');
|
|
@@ -719,6 +720,9 @@ function showHelpAll() {
|
|
|
719
720
|
console.log(' topup - Buy credits and print a Stripe checkout URL');
|
|
720
721
|
console.log(' buy - Quote a purchase or stock order; the person approves before anything is charged');
|
|
721
722
|
console.log(' transactions - List what you bought, paid in fees, traded, or sent');
|
|
723
|
+
console.log(' hire - Hire an agent in one step: name, job, inbox, allowance, nightly work loop');
|
|
724
|
+
console.log(' hires - Morning check-in: one line per hire (mail, spend, last run)');
|
|
725
|
+
console.log(' mail - Your agents\' own inboxes: list, read, send (not Gmail)');
|
|
722
726
|
console.log(' design - Extract a site design system, check brand adherence, search brands');
|
|
723
727
|
console.log(' mcp - Run the atris MCP server (stdio) for Claude Desktop and Cursor');
|
|
724
728
|
console.log('');
|
|
@@ -749,6 +753,7 @@ function showHelpAll() {
|
|
|
749
753
|
console.log(' member list - Show all team members');
|
|
750
754
|
console.log(' member activate <n> - Activate a member (link skills, show context)');
|
|
751
755
|
console.log(' member upgrade <n> - Convert flat file to directory format');
|
|
756
|
+
console.log(' member result <n> - Newest run of a member: run record, receipt, artifacts, result number');
|
|
752
757
|
console.log('');
|
|
753
758
|
console.log('Plugin:');
|
|
754
759
|
console.log(' plugin build - Package skills as .plugin for Cowork');
|
|
@@ -1773,9 +1778,28 @@ function showWelcomeVisualization() {
|
|
|
1773
1778
|
// Document health is advisory and must never prevent startup.
|
|
1774
1779
|
}
|
|
1775
1780
|
|
|
1781
|
+
// What the background cleanup did lately, in its own plain words: the
|
|
1782
|
+
// newest line the hourly autoland tick or a project job left in the
|
|
1783
|
+
// journal, plus agent branches parked for a person.
|
|
1784
|
+
try {
|
|
1785
|
+
const lately = require('../lib/cleanup-summary').latestCleanupLine(cwd);
|
|
1786
|
+
if (lately) console.log(row('lately', lately));
|
|
1787
|
+
} catch {
|
|
1788
|
+
// Advisory only; never blocks boot.
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// When the hourly autoland tick is alive it lands, salvages, and clears
|
|
1792
|
+
// finished branches and old agent copies itself, so boot must not hand the
|
|
1793
|
+
// owner that chore ("39 finished pieces to put away"). Only a dead or
|
|
1794
|
+
// missing heartbeat brings the tidy counts and `land --reap` back.
|
|
1795
|
+
let autolandAlive = false;
|
|
1796
|
+
try { autolandAlive = require('../lib/autoland').heartbeatIsLive(cwd); } catch { autolandAlive = false; }
|
|
1797
|
+
|
|
1776
1798
|
// landSummary is expensive (git board classification) - compute once per boot.
|
|
1777
1799
|
let landInfo = null;
|
|
1778
|
-
|
|
1800
|
+
if (!autolandAlive) {
|
|
1801
|
+
try { landInfo = require('../commands/land').landSummary(cwd); } catch (err) { landInfo = null; }
|
|
1802
|
+
}
|
|
1779
1803
|
let rotInfo = null;
|
|
1780
1804
|
try {
|
|
1781
1805
|
const { parseLessons } = require('../lib/memory-view');
|
|
@@ -1789,7 +1813,7 @@ function showWelcomeVisualization() {
|
|
|
1789
1813
|
worktreeDir = path.join(path.dirname(resolved), '.agent-worktrees', path.basename(resolved));
|
|
1790
1814
|
}
|
|
1791
1815
|
let worktrees = 0;
|
|
1792
|
-
if (fs.existsSync(worktreeDir)) {
|
|
1816
|
+
if (!autolandAlive && fs.existsSync(worktreeDir)) {
|
|
1793
1817
|
worktrees = fs.readdirSync(worktreeDir, { withFileTypes: true })
|
|
1794
1818
|
.filter((entry) => entry.isDirectory()).length;
|
|
1795
1819
|
}
|
|
@@ -2384,6 +2408,20 @@ if (command === 'guide') {
|
|
|
2384
2408
|
Promise.resolve(require('../commands/buy').buyCommand(process.argv.slice(3)))
|
|
2385
2409
|
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2386
2410
|
.catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
|
|
2411
|
+
} else if (command === 'hire') {
|
|
2412
|
+
// One-call hire: member + inbox + allowance + overnight loop (POST /business/{id}/hire).
|
|
2413
|
+
Promise.resolve(require('../commands/hire').hireCommand(process.argv.slice(3)))
|
|
2414
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2415
|
+
.catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
|
|
2416
|
+
} else if (command === 'hires') {
|
|
2417
|
+
Promise.resolve(require('../commands/hire').hiresCommand(process.argv.slice(3)))
|
|
2418
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2419
|
+
.catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
|
|
2420
|
+
} else if (command === 'mail') {
|
|
2421
|
+
// Agent inboxes (@atrismail.com), not Gmail. Send asks first unless --yes.
|
|
2422
|
+
Promise.resolve(require('../commands/mail').mailCommand(process.argv.slice(3)))
|
|
2423
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
2424
|
+
.catch((err) => { console.error(String(err.message || err).replace(/\s+/g, ' ')); process.exit(1); });
|
|
2387
2425
|
} else if (command === 'transactions') {
|
|
2388
2426
|
Promise.resolve(require('../commands/transactions').transactionsCommand(process.argv.slice(3)))
|
|
2389
2427
|
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
@@ -3154,6 +3192,11 @@ if (command === 'guide') {
|
|
|
3154
3192
|
Promise.resolve(require('../commands/engine').engineCommand(engineArgs))
|
|
3155
3193
|
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
3156
3194
|
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
3195
|
+
} else if (command === 'link') {
|
|
3196
|
+
// Link: ask the roster's engine what two things could become together.
|
|
3197
|
+
Promise.resolve(require('../commands/link').linkCommand(process.argv.slice(3)))
|
|
3198
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
3199
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
3157
3200
|
} else if (command === 'playbook') {
|
|
3158
3201
|
Promise.resolve()
|
|
3159
3202
|
.then(() => require('../commands/playbook').playbookCommand(process.argv.slice(3)))
|
package/commands/agent-spawn.js
CHANGED
package/commands/autoland.js
CHANGED
|
@@ -555,6 +555,21 @@ function showStatus(root, args) {
|
|
|
555
555
|
console.log('');
|
|
556
556
|
console.log(` waiting on a human right now: ${waiting.length}, oldest ${waiting[0].hours}h`);
|
|
557
557
|
}
|
|
558
|
+
// What the next tick's idle sweep will do, read-only (no check runs here).
|
|
559
|
+
if (enabled && policy.janitor !== false && policy.idle_sweep !== false) {
|
|
560
|
+
try {
|
|
561
|
+
const idle = require('../lib/idle-review-sweep').sweepIdleReviews(root, { tasks, policy, dryRun: true });
|
|
562
|
+
const days = Number(policy.idle_close_days) > 0 ? Number(policy.idle_close_days) : 7;
|
|
563
|
+
if (idle.closed.length) {
|
|
564
|
+
console.log(` closing on the next tick: ${idle.closed.length} idle over ${days} days that can never land on their own, each with a plain reason.`);
|
|
565
|
+
}
|
|
566
|
+
if (idle.human_waiting.length) {
|
|
567
|
+
console.log(` staying with you: ${idle.human_waiting.length} in money, deploys, security, or customer work; idle time never closes these.`);
|
|
568
|
+
}
|
|
569
|
+
} catch {
|
|
570
|
+
// Preview only.
|
|
571
|
+
}
|
|
572
|
+
}
|
|
558
573
|
console.log('');
|
|
559
574
|
if (!enabled) console.log(" flip it on: atris autoland on (one decision; everything after is receipts)");
|
|
560
575
|
console.log('');
|
|
@@ -798,9 +813,11 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
798
813
|
.filter((task) => String(task.review?.approval_status || task.metadata?.approval_status || 'pending') === 'pending')
|
|
799
814
|
.length;
|
|
800
815
|
const accept = runOwnCli(root, cliArgs, acceptSweepTimeoutMs(pendingReviewRows));
|
|
816
|
+
let acceptResults = [];
|
|
801
817
|
try {
|
|
802
818
|
const parsed = JSON.parse(accept.stdout);
|
|
803
819
|
const results = Array.isArray(parsed.results) ? parsed.results : [];
|
|
820
|
+
acceptResults = results;
|
|
804
821
|
// A refused sweep (ok:false, a guard tripped, policy race) carries no
|
|
805
822
|
// summary fields. Name the reason instead of leaving nulls that read as
|
|
806
823
|
// "no work": a blind heartbeat must say WHY it is blind.
|
|
@@ -963,6 +980,51 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
|
|
|
963
980
|
recordLandingSweepState(state, null, { error: receipt.reap_error });
|
|
964
981
|
}
|
|
965
982
|
|
|
983
|
+
// 3c2. idle work resolves itself, every tick. Finished work that sat still
|
|
984
|
+
// past the grace period (policy.idle_close_days, default 7) and can never
|
|
985
|
+
// land on its own is closed with a plain reason; done and long-idle rows
|
|
986
|
+
// leave the list. This used to run only when someone opened a session, so
|
|
987
|
+
// nights and trips piled up work nobody would ever land. Protected lanes
|
|
988
|
+
// (money, deploys, security, customer, outward) are never closed by idle
|
|
989
|
+
// time. Off switch: policy.idle_sweep === false or policy.janitor === false.
|
|
990
|
+
if (policy.janitor !== false && policy.idle_sweep !== false) {
|
|
991
|
+
try {
|
|
992
|
+
const liveReasons = new Map(acceptResults
|
|
993
|
+
.filter((r) => r && r.ref && r.reason && r.action !== 'accepted')
|
|
994
|
+
.map((r) => [r.ref, r.reason]));
|
|
995
|
+
const kept = require('../lib/task-list-keeper').keepWorkspaceTaskList(root, { actor: 'autoland', includeDone: false });
|
|
996
|
+
const idle = require('../lib/idle-review-sweep').sweepIdleReviews(root, {
|
|
997
|
+
tasks: readProjection(root),
|
|
998
|
+
policy,
|
|
999
|
+
liveReasons,
|
|
1000
|
+
});
|
|
1001
|
+
const putAway = (kept.put_away || []).length + (kept.reaped || []).length;
|
|
1002
|
+
if (putAway + idle.closed.length > 0) require('./task').refreshKeptTaskList(root);
|
|
1003
|
+
receipt.idle_sweep = {
|
|
1004
|
+
closed: idle.closed.map((c) => ({ ref: c.ref, reason: c.reason })),
|
|
1005
|
+
put_away: putAway,
|
|
1006
|
+
human_waiting: idle.human_waiting.length,
|
|
1007
|
+
};
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
receipt.idle_sweep_error = String((err && err.message) || err).slice(0, 200);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
try {
|
|
1013
|
+
const { appendOvernightNote, tickSummaryLine } = require('../lib/cleanup-summary');
|
|
1014
|
+
const line = tickSummaryLine({
|
|
1015
|
+
landed: (receipt.landed || []).length,
|
|
1016
|
+
closed: receipt.idle_sweep ? receipt.idle_sweep.closed.length : 0,
|
|
1017
|
+
putAway: receipt.idle_sweep ? receipt.idle_sweep.put_away : 0,
|
|
1018
|
+
needYou: receipt.idle_sweep ? receipt.idle_sweep.human_waiting : protectedWaiting.length,
|
|
1019
|
+
});
|
|
1020
|
+
if (line) {
|
|
1021
|
+
appendOvernightNote(root, line);
|
|
1022
|
+
receipt.summary_line = line;
|
|
1023
|
+
}
|
|
1024
|
+
} catch {
|
|
1025
|
+
// The journal line is a courtesy; it never fails the tick.
|
|
1026
|
+
}
|
|
1027
|
+
|
|
966
1028
|
// 3d. wish sweep: queued wishes are allowed to wake a mission without a
|
|
967
1029
|
// foreground `atris wish` session, but they must never endanger landing.
|
|
968
1030
|
try {
|
package/commands/brain.js
CHANGED
|
@@ -1140,6 +1140,10 @@ function taskEpisodeScorecard(root, episode, workspace, ts = new Date().toISOStr
|
|
|
1140
1140
|
task_title: episode.state && episode.state.title || null,
|
|
1141
1141
|
task_tag: episode.state && episode.state.tag || null,
|
|
1142
1142
|
actor: episode.action && episode.action.actor || null,
|
|
1143
|
+
// Who built the work. `actor` is the reviewer; trust is earned by the builder.
|
|
1144
|
+
builder: episode.state && episode.state.metadata && episode.state.metadata.built_by
|
|
1145
|
+
|| episode.state && episode.state.claimed_by
|
|
1146
|
+
|| null,
|
|
1143
1147
|
reward: Number.isFinite(reward) ? reward : 0,
|
|
1144
1148
|
reward_source: episode.reward && episode.reward.source || 'task_review',
|
|
1145
1149
|
lesson: episode.lesson || '',
|
package/commands/engine.js
CHANGED
|
@@ -35,9 +35,12 @@ const {
|
|
|
35
35
|
const { parseScopeFlag } = require('../lib/cli-scope');
|
|
36
36
|
const { isFreshWorkspace, speakFirstMinute } = require('../lib/first-minute');
|
|
37
37
|
const { teamRosterView } = require('../lib/member-engine');
|
|
38
|
-
const { engineRunsView, availableModels } = require('../lib/roster-models');
|
|
38
|
+
const { engineModelText, engineRunsView, availableModels, modelLabel } = require('../lib/roster-models');
|
|
39
|
+
const { renderTable } = require('../lib/text-table');
|
|
40
|
+
const { TEAM_FOOTER } = require('../lib/team-lineup');
|
|
39
41
|
const { readRosterRuns, workerRuns, summarizeRuns, recentRuns, renderRunLine, DEFAULT_DAYS: RUN_DAYS } = require('../lib/roster-runs');
|
|
40
42
|
const { attachSuggestions } = require('../lib/roster-suggest');
|
|
43
|
+
const { PIN_MAX_AGE_DAYS: PIN_STALE_DAYS, attachStaleModels, modelCatalog } = require('../lib/roster-stale');
|
|
41
44
|
const {
|
|
42
45
|
ENGINE_ROLES,
|
|
43
46
|
ENGINE_JOBS,
|
|
@@ -54,6 +57,8 @@ const {
|
|
|
54
57
|
rosterJobNameError,
|
|
55
58
|
rosterJobRole,
|
|
56
59
|
readRosterState,
|
|
60
|
+
retiredModelsNamed,
|
|
61
|
+
RETIRED_CLAUDE_MODELS,
|
|
57
62
|
customRosterJobKeys,
|
|
58
63
|
setRosterPick,
|
|
59
64
|
confirmRoster,
|
|
@@ -1051,6 +1056,7 @@ function workerRow(step, index, leadIndex, now = new Date()) {
|
|
|
1051
1056
|
until: worker.until || null,
|
|
1052
1057
|
ends_in_days: endsIn,
|
|
1053
1058
|
expiring_soon: endsSoon(endsIn),
|
|
1059
|
+
verified: worker.verified || null,
|
|
1054
1060
|
line: worker.line || null,
|
|
1055
1061
|
...(worker.error ? { text: worker.text || '' } : {}),
|
|
1056
1062
|
status,
|
|
@@ -1157,6 +1163,7 @@ function rosterReport(root = process.cwd(), now = new Date()) {
|
|
|
1157
1163
|
jobs,
|
|
1158
1164
|
expiring,
|
|
1159
1165
|
renew_command: expiring.length ? RENEW_COMMAND : null,
|
|
1166
|
+
upgraded: upgradedModels(state),
|
|
1160
1167
|
team: team.rows,
|
|
1161
1168
|
warnings: [...state.session.warnings, ...state.project.warnings, ...state.machine.warnings, ...team.warnings],
|
|
1162
1169
|
files: {
|
|
@@ -1168,6 +1175,25 @@ function rosterReport(root = process.cwd(), now = new Date()) {
|
|
|
1168
1175
|
};
|
|
1169
1176
|
}
|
|
1170
1177
|
|
|
1178
|
+
// Retired models a roster file still names. Every read runs them as their
|
|
1179
|
+
// replacement, so this is only a heads-up; nobody has to edit the file.
|
|
1180
|
+
function upgradedModels(state) {
|
|
1181
|
+
const found = [];
|
|
1182
|
+
for (const layer of [state.session, state.project, state.machine]) {
|
|
1183
|
+
if (!layer || layer.format === 'none' || !layer.path) continue;
|
|
1184
|
+
let text = '';
|
|
1185
|
+
try { text = fs.readFileSync(layer.path, 'utf8'); } catch { continue; }
|
|
1186
|
+
for (const from of retiredModelsNamed(text)) {
|
|
1187
|
+
if (!found.some((item) => item.from === from)) found.push({ from, to: RETIRED_CLAUDE_MODELS[from] });
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
return found;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function renderUpgradedLine(upgraded) {
|
|
1194
|
+
return (upgraded || []).map((item) => `note: ${modelLabel(item.from)} is retired, so your roster runs ${modelLabel(item.to)} in its place. no edit needed.`).join('\n');
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1171
1197
|
// Every worker, in any job, whose end date is today or within the next
|
|
1172
1198
|
// seven days. Workers already past their date show on their own lines. A
|
|
1173
1199
|
// job set for this shell only is skipped: roster confirm does not renew it.
|
|
@@ -1214,74 +1240,157 @@ function bootExpiringLine(expiring) {
|
|
|
1214
1240
|
return `${first.tool} on ${first.job} ${endsInText(first.ends_in_days)}${more}. renew: ${RENEW_COMMAND}`;
|
|
1215
1241
|
}
|
|
1216
1242
|
|
|
1217
|
-
//
|
|
1218
|
-
//
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
const until = !worker.until || ended ? '' : `until ${worker.until}${worker.expiring_soon ? `, ${endsInText(worker.ends_in_days)}` : ''}`;
|
|
1226
|
-
const extras = [worker.max_seconds ? rosterMaxText(worker.max_seconds) : '', worker.prep ? `prepped by ${worker.prep}` : '', until].filter(Boolean);
|
|
1227
|
-
const state = worker.status === 'leads' ? 'leads now' : worker.status === 'backup' ? 'backup'
|
|
1228
|
-
: ended ? `ended ${shortDay(worker.until)}, skipped` : `skipped, ${worker.why}`;
|
|
1229
|
-
return ` ${index + 1}. ${what} ${[state, ...extras].join(', ')}`.trimEnd();
|
|
1230
|
-
});
|
|
1243
|
+
// --- the roster as tables -------------------------------------------------
|
|
1244
|
+
//
|
|
1245
|
+
// One row per job, one row per member. Plain columns, a header row, two
|
|
1246
|
+
// spaces between; a cell that needs more than one line (effort, several
|
|
1247
|
+
// backups) carries on underneath in its own column.
|
|
1248
|
+
|
|
1249
|
+
function workerCellText(view) {
|
|
1250
|
+
return view ? engineModelText(view.engine, view.model) : '';
|
|
1231
1251
|
}
|
|
1232
1252
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
return (
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
const width = Math.max(7, ...rows.map((row) => row.job.length));
|
|
1242
|
-
const ownerWidth = Math.max(24, ...rows.filter((row) => row.pick && row.runs).map((row) => row.runs.text.length));
|
|
1243
|
-
const backupWidth = Math.max(16, ...rows.filter((row) => row.backup_runs).map((row) => row.backup_runs.text.length + 7));
|
|
1244
|
-
return rows.map((row) => {
|
|
1245
|
-
const label = row.job.padEnd(width);
|
|
1246
|
-
const nowText = row.now_runs ? row.now_runs.text : 'no ready engine';
|
|
1247
|
-
const fallsTo = `${row.like ? `falls back to ${row.like}` : 'router decides'}: ${nowText}`;
|
|
1248
|
-
if (!row.pick) return `${label} no pick, ${fallsTo}`;
|
|
1249
|
-
const owner = row.runs.text.padEnd(ownerWidth);
|
|
1250
|
-
const backup = row.backup_runs ? `backup ${row.backup_runs.text}${row.backup_prep ? ` prepped by ${row.backup_prep}` : ''}` : 'no backup';
|
|
1251
|
-
const cap = `${row.max_seconds ? `${rosterMaxText(row.max_seconds)}, ` : ''}${row.prep ? `prepped by ${row.prep}, ` : ''}`;
|
|
1252
|
-
const date = untilText(row.pick, row.ends_in_days);
|
|
1253
|
-
const fallback = row.lead === 'backup' ? 'using backup' : row.lead === 'later' ? `using ${nowText}` : fallsTo;
|
|
1254
|
-
const ended = row.ends_in_days !== null && row.ends_in_days !== undefined ? `ended ${shortDay(row.pick.until)}, skipped` : 'expired';
|
|
1255
|
-
const status = row.status === 'expired' ? `${ended}, ${fallback}`
|
|
1256
|
-
: row.status === 'cooling' ? `${row.cooling}, ${fallback}`
|
|
1257
|
-
: row.status === 'not ready' ? `not ready, ${fallback}`
|
|
1258
|
-
: date;
|
|
1259
|
-
const where = row.file ? `${row.from} (${row.file})` : row.from;
|
|
1260
|
-
const head = `${label} ${owner} ${backup.padEnd(backupWidth)} ${cap}${status}, ${where}`.trimEnd();
|
|
1261
|
-
const suggestion = row.suggestion ? [` suggestion: ${row.suggestion.text}`] : [];
|
|
1262
|
-
return [head, ...renderWorkerLines(row), ...renderWorkerRecords(row), ...suggestion].join('\n');
|
|
1263
|
-
}).join('\n');
|
|
1253
|
+
function maxCellText(seconds) {
|
|
1254
|
+
const text = rosterMaxText(seconds);
|
|
1255
|
+
return text ? text.replace(/^max /, '') : '-';
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Workers that name a tool the roster can run, in written order.
|
|
1259
|
+
function goodWorkers(row) {
|
|
1260
|
+
return (row.workers || []).filter((worker) => worker.engine && worker.runs);
|
|
1264
1261
|
}
|
|
1265
1262
|
|
|
1266
|
-
|
|
1263
|
+
// Why a backup is passed over, in two or three words.
|
|
1264
|
+
function backupSkipText(worker) {
|
|
1265
|
+
if (worker.status !== 'skipped') return '';
|
|
1266
|
+
if (worker.why === 'expired' && worker.ends_in_days !== null && worker.ends_in_days !== undefined) return `ended ${shortDay(worker.until)}`;
|
|
1267
|
+
return worker.why || 'skipped';
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// "newer <model> out" or "pin 45 days old": the outdated-model flags,
|
|
1271
|
+
// short enough for the note column. The full line with its fix prints
|
|
1272
|
+
// under the table.
|
|
1273
|
+
function staleNote(worker) {
|
|
1274
|
+
if (worker.stale_model && worker.newer_model) return `newer ${modelLabel(worker.newer_model)} out`;
|
|
1275
|
+
if (worker.pin_age_days !== null && worker.pin_age_days !== undefined && worker.pin_age_days > PIN_STALE_DAYS) return `pin ${worker.pin_age_days} days old`;
|
|
1276
|
+
return '';
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function jobNoteText(row, lead, backups, mainFrom) {
|
|
1280
|
+
// A tool on its own default model is named alone here, to keep notes short.
|
|
1281
|
+
const nowText = !row.now_runs ? 'no ready engine' : row.now_runs.model ? workerCellText(row.now_runs) : row.now_runs.engine;
|
|
1282
|
+
if (!row.pick) return row.like ? `no pick, same as ${row.like}` : 'no pick, router decides';
|
|
1283
|
+
const fallback = row.lead === 'backup' ? 'using backup'
|
|
1284
|
+
: row.lead === 'later' ? `using ${nowText}`
|
|
1285
|
+
: row.like ? `falls back to ${row.like}` : `router decides: ${nowText}`;
|
|
1286
|
+
const ended = row.ends_in_days !== null && row.ends_in_days !== undefined ? `ended ${shortDay(row.pick.until)}` : 'expired';
|
|
1287
|
+
const parts = [];
|
|
1288
|
+
if (row.status === 'expired') parts.push(`${ended}, ${fallback}`);
|
|
1289
|
+
else if (row.status === 'cooling') parts.push(`${row.cooling}, ${fallback}`);
|
|
1290
|
+
else if (row.status === 'not ready') parts.push(`not ready, ${fallback}`);
|
|
1291
|
+
else if (row.expiring_soon) parts.push(endsInText(row.ends_in_days));
|
|
1292
|
+
if (lead && lead.prep) parts.push(`prep: ${lead.prep}`);
|
|
1293
|
+
if (lead && staleNote(lead)) parts.push(staleNote(lead));
|
|
1294
|
+
for (const worker of backups) if (staleNote(worker)) parts.push(`backup ${staleNote(worker)}`);
|
|
1295
|
+
if (row.from && row.from !== mainFrom) parts.push(`from ${row.from}`);
|
|
1296
|
+
return parts.join(', ');
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// The roster file most jobs come from; the table heading names it.
|
|
1300
|
+
function mainRosterSource(rows) {
|
|
1301
|
+
const counts = new Map();
|
|
1302
|
+
for (const row of rows) if (row.pick && row.from) counts.set(row.from, (counts.get(row.from) || 0) + 1);
|
|
1303
|
+
const [from] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0] || [];
|
|
1304
|
+
const row = rows.find((item) => item.from === from);
|
|
1305
|
+
return { from: from || null, file: row ? row.file : null };
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function jobTableRow(row, mainFrom) {
|
|
1309
|
+
const [lead, ...backups] = row.pick ? goodWorkers(row) : [];
|
|
1310
|
+
const leadView = row.pick ? (lead && lead.runs) : row.now_runs;
|
|
1311
|
+
const leads = leadView
|
|
1312
|
+
? [workerCellText(leadView), leadView.effort ? `(${leadView.effort} effort)` : null]
|
|
1313
|
+
: [row.pick ? 'no ready tool' : 'no ready engine'];
|
|
1314
|
+
const backupCell = backups.length
|
|
1315
|
+
? backups.map((worker) => {
|
|
1316
|
+
// Effort, max time, and prep for a backup stay in --json; the cell keeps
|
|
1317
|
+
// what changes who runs: a skip reason or an end date.
|
|
1318
|
+
const skip = backupSkipText(worker);
|
|
1319
|
+
const until = !skip && parseRosterUntil(worker.until) ? `until ${shortDay(worker.until)}${worker.expiring_soon ? `, ${endsInText(worker.ends_in_days)}` : ''}` : '';
|
|
1320
|
+
const extras = [skip, until].filter(Boolean);
|
|
1321
|
+
return `${workerCellText(worker.runs)}${extras.length ? ` (${extras.join(', ')})` : ''}`;
|
|
1322
|
+
})
|
|
1323
|
+
: ['none'];
|
|
1324
|
+
const until = lead && parseRosterUntil(lead.until) ? lead.until : '-';
|
|
1325
|
+
return [
|
|
1326
|
+
row.job,
|
|
1327
|
+
leads,
|
|
1328
|
+
backupCell,
|
|
1329
|
+
lead ? maxCellText(lead.max_seconds) : '-',
|
|
1330
|
+
until,
|
|
1331
|
+
jobNoteText(row, lead, backups, mainFrom) || '-',
|
|
1332
|
+
];
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function renderJobRoster(rows, { width, heading = true } = {}) {
|
|
1267
1336
|
if (!rows.length) return '';
|
|
1268
|
-
const
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1337
|
+
const { from, file } = mainRosterSource(rows);
|
|
1338
|
+
const columns = [
|
|
1339
|
+
{ header: 'JOB' }, { header: 'LEADS' }, { header: 'BACKUP' }, { header: 'MAX' }, { header: 'UNTIL' },
|
|
1340
|
+
{ header: 'NOTE', wrap: true, min: 16 },
|
|
1341
|
+
];
|
|
1342
|
+
const table = renderTable(columns, rows.map((row) => jobTableRow(row, from)), { width });
|
|
1343
|
+
const title = !heading ? '' : file ? `jobs, from ${file}` : 'jobs';
|
|
1344
|
+
const suggestions = rows.filter((row) => row.suggestion).map((row) => `suggestion for ${row.job}: ${row.suggestion.text}`);
|
|
1345
|
+
return [title, table, ...renderWorkerRecords(rows, { width }), ...suggestions].filter(Boolean).join('\n');
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// Each worker's recent record (last 7 days), only for workers with runs, as
|
|
1349
|
+
// one small table under the jobs.
|
|
1350
|
+
function renderWorkerRecords(rows, { width } = {}) {
|
|
1351
|
+
const records = [];
|
|
1352
|
+
for (const row of rows) {
|
|
1353
|
+
for (const worker of row.workers || []) {
|
|
1354
|
+
if (worker.record) records.push([row.job, worker.runs ? workerCellText(worker.runs) : worker.engine, worker.record.text.replace(/^last \d+ days: /, '')]);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
if (!records.length) return [];
|
|
1358
|
+
return ['\nlast 7 days', renderTable([{ header: 'JOB' }, { header: 'WORKER' }, { header: 'RECORD', clip: true, min: 20 }], records, { width })];
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// "sep 30, 3 runs" or "quiet 8 to 14 days", from the same evidence atris
|
|
1362
|
+
// team reads. '-' for idle members and when activity was not read.
|
|
1363
|
+
function teamActivityTag(activity) {
|
|
1364
|
+
if (!activity || activity.status === 'idle') return '-';
|
|
1365
|
+
if (activity.status === 'quiet') return 'quiet 8 to 14 days';
|
|
1366
|
+
const runs = Number(activity.runs_7d) || 0;
|
|
1367
|
+
const day = activity.days_since === 0 ? 'today' : activity.days_since === 1 ? 'yesterday' : shortDay(activity.last_active);
|
|
1368
|
+
return `${day}, ${runs} run${runs === 1 ? '' : 's'}`;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function renderTeamRoster(rows, { width } = {}) {
|
|
1372
|
+
if (!rows.length) return '';
|
|
1373
|
+
const showActivity = rows.some((row) => row.activity !== undefined);
|
|
1374
|
+
const columns = [{ header: 'MEMBER' }, { header: 'JOB' }, { header: 'ENGINE · MODEL' }, { header: 'SOURCE' }];
|
|
1375
|
+
if (showActivity) columns.push({ header: 'LAST 7 DAYS', clip: true, min: 11 });
|
|
1376
|
+
const cells = rows.map((row) => {
|
|
1377
|
+
const engine = row.engine ? workerCellText(engineRunsView(row.engine, { model: row.model || '', effort: row.effort || '' })) : 'no ready engine';
|
|
1378
|
+
const stale = row.stale_model && row.newer_model ? ` (newer ${modelLabel(row.newer_model)} out)` : '';
|
|
1379
|
+
const cell = [row.member, row.job || '-', `${engine}${stale}`, row.source === 'file' ? (row.file || 'roster file') : 'automatic'];
|
|
1380
|
+
if (showActivity) cell.push(teamActivityTag(row.activity));
|
|
1381
|
+
return cell;
|
|
1274
1382
|
});
|
|
1275
|
-
return ['team',
|
|
1383
|
+
return ['team', renderTable(columns, cells, { width })].join('\n');
|
|
1276
1384
|
}
|
|
1277
1385
|
|
|
1278
1386
|
function renderRosterWarnings(warnings) {
|
|
1279
1387
|
return warnings.map((warning) => `warning: ${warning.file} line ${warning.line} "${warning.text}" ${warning.message}.`).join('\n');
|
|
1280
1388
|
}
|
|
1281
1389
|
|
|
1282
|
-
function renderRosterReport(report) {
|
|
1283
|
-
const
|
|
1284
|
-
|
|
1390
|
+
function renderRosterReport(report, { width } = {}) {
|
|
1391
|
+
const stale = (report.stale || []).map((item) => item.text);
|
|
1392
|
+
const jobs = [renderJobRoster(report.jobs, { width }), renderExpiringLine(report.expiring), ...stale, renderUpgradedLine(report.upgraded)].filter(Boolean).join('\n');
|
|
1393
|
+
return [jobs, renderTeamRoster(report.team, { width }), renderRosterWarnings(report.warnings)]
|
|
1285
1394
|
.filter(Boolean)
|
|
1286
1395
|
.join('\n\n');
|
|
1287
1396
|
}
|
|
@@ -1290,8 +1399,6 @@ function printJobRoster(root, now = new Date()) {
|
|
|
1290
1399
|
console.log(renderRosterReport(rosterReport(root, now)));
|
|
1291
1400
|
}
|
|
1292
1401
|
|
|
1293
|
-
const AVAILABLE_HINT = 'see which tools and models this machine has: atris engine roster --available';
|
|
1294
|
-
|
|
1295
1402
|
// Each installed tool and the models it offers, from local files only.
|
|
1296
1403
|
function availableReport(root) {
|
|
1297
1404
|
const engines = readEngineRegistry(root, { persist: false }).engines
|
|
@@ -1312,7 +1419,7 @@ function renderSessionRoster(report, key) {
|
|
|
1312
1419
|
const team = report.team.filter((row) => row.source === 'file' && row.file === report.files.session);
|
|
1313
1420
|
if (!jobs.length && !team.length) return `this session (${key}) has no roster changes. add one with: atris engine assign <job> <tool> --session`;
|
|
1314
1421
|
const head = `this session (${key}) changes${report.files.session ? `, from ${report.files.session}` : ''}`;
|
|
1315
|
-
return [head, renderJobRoster(jobs), renderTeamRoster(team)].filter(Boolean).join('\n\n');
|
|
1422
|
+
return [head, renderJobRoster(jobs, { heading: false }), renderTeamRoster(team)].filter(Boolean).join('\n\n');
|
|
1316
1423
|
}
|
|
1317
1424
|
|
|
1318
1425
|
function runRosterSessionCommand(rest, json, root, now) {
|
|
@@ -1361,6 +1468,38 @@ function attachRunRecords(report, root, now = new Date()) {
|
|
|
1361
1468
|
return report;
|
|
1362
1469
|
}
|
|
1363
1470
|
|
|
1471
|
+
// Pins are proven by landed runs; a pin older than 30 days is flagged, so
|
|
1472
|
+
// the look-back reaches well past that.
|
|
1473
|
+
const STALE_PROOF_DAYS = 90;
|
|
1474
|
+
|
|
1475
|
+
// Each team row gets the member's activity (last 7 days, quiet, or idle)
|
|
1476
|
+
// from the same evidence atris team reads: dated logs, commits that name
|
|
1477
|
+
// the member, roster runs, and stream events, read once for the team.
|
|
1478
|
+
function attachTeamActivity(report, root, now = new Date()) {
|
|
1479
|
+
const rows = report.team || [];
|
|
1480
|
+
if (!rows.length) return report;
|
|
1481
|
+
try {
|
|
1482
|
+
const { collectMemberActivity } = require('./team');
|
|
1483
|
+
const activity = collectMemberActivity(root, rows.map((row) => ({ name: row.member })), { nowMs: new Date(now).getTime() });
|
|
1484
|
+
const byName = new Map(activity.map(({ days, ...rest }) => [rest.name, rest]));
|
|
1485
|
+
for (const row of rows) row.activity = byName.get(String(row.member).toLowerCase()) || null;
|
|
1486
|
+
} catch {
|
|
1487
|
+
// Activity is a view; a read that fails leaves the roster as it was.
|
|
1488
|
+
}
|
|
1489
|
+
return report;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// Mark workers that run an outdated model or an unproven old pin. Local
|
|
1493
|
+
// files only: the registry, codex's models cache, and the run record.
|
|
1494
|
+
function attachStaleCheck(report, root, now = new Date()) {
|
|
1495
|
+
const engines = readEngineRegistry(root, { persist: false }).engines;
|
|
1496
|
+
return attachStaleModels(report, {
|
|
1497
|
+
catalog: modelCatalog(engines),
|
|
1498
|
+
runs: readRosterRuns(root, { now, days: STALE_PROOF_DAYS }),
|
|
1499
|
+
now,
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1364
1503
|
const RUNS_LIST_DAYS = 30;
|
|
1365
1504
|
const RUNS_LIST_LIMIT = 20;
|
|
1366
1505
|
|
|
@@ -1407,9 +1546,9 @@ function runRosterCommand(args, root, now = new Date()) {
|
|
|
1407
1546
|
}
|
|
1408
1547
|
if (rest[0] === 'session') return runRosterSessionCommand(rest, json, root, now);
|
|
1409
1548
|
if (rest[0] === 'confirm') confirmRoster(root, now);
|
|
1410
|
-
const report = attachRunRecords(rosterReport(root, now), root, now);
|
|
1549
|
+
const report = attachTeamActivity(attachStaleCheck(attachRunRecords(rosterReport(root, now), root, now), root, now), root, now);
|
|
1411
1550
|
if (json) console.log(JSON.stringify(report, null, 2));
|
|
1412
|
-
else console.log(`${renderRosterReport(report)}\n\n${
|
|
1551
|
+
else console.log(`${renderRosterReport(report)}\n\n${TEAM_FOOTER.join('\n')}`);
|
|
1413
1552
|
return 0;
|
|
1414
1553
|
}
|
|
1415
1554
|
|
|
@@ -2267,7 +2406,7 @@ function engineCommand(args = [], deps = {}) {
|
|
|
2267
2406
|
if (sub === 'help' || args.includes('--help') || args.includes('-h')) {
|
|
2268
2407
|
console.log('\n atris engine watch [<id>|latest] [--no-follow]\n follow one live transcript or list running engine work');
|
|
2269
2408
|
console.log('\n long read-only asks: atris engine ask "..." --engine agy --timeout <seconds>\n quick asks default to 120 seconds; explicit jobs allow up to 3600\n follow live work with atris engine watch latest');
|
|
2270
|
-
console.log('\n atris engine roster + current default\n atris engine roster [--json] show every job and its workers in order: search, build, review, and your own\n atris engine roster confirm renew every dated worker for 30 days\n atris engine roster session [clear]\n show or drop the roster changes for this shell only\n atris engine roster --available\n tools on this machine and the models each one offers\n atris engine roster --runs [job]\n the last 20 runs, newest first, with tool, model, time, and outcome\n atris engine assign <job> <tool> [--like <kind>] [--model <m>] [--effort <level>] [--max "20 min"] [--prep <job>] [--backup "<tool> [model]"] [--days <n>] [--add] [--session | --everywhere]\n atris engine assign <job> --promote "<tool> [model]" | --remove <tool> | --clear [--session | --everywhere]\n sets the lead worker of the job; --promote moves one of its workers to the top as written;\n --add puts one more at the end; jobs: search, build, review,\n or your own like "small build" (--like search|build|review when the name does not say);\n --prep search has that job read the task first and hand this worker a short brief;\n --everywhere is for all projects; --session is this shell only (set ATRIS_ROSTER_SESSION=<name>)\n atris engines --chart show the fleet as an org chart\n atris engine list --json [--all] full registry: default + engines with tier, roles, fallback, health (--all includes hidden engines)\n atris engine set <name> --duty leader|errands|learning [--models "a, b"]\n arrange the fleet and save its model policy\n atris engine resolve <role> or <job> [--json]\n choose the best ready engine for navigator|executor|validator or a roster job;\n notes always answers: its roster pick, else agy then haiku\n atris engine health <name> --set ready|not_installed|credit_out\n flip runtime health, for example when credits run out\n atris engine doctor [--json]\n probe which engine CLIs are installed here and sync that into health policy\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine bench [names...] [--runs N]\n ranked latency scoreboard of engine passes\n atris engine ask "<question>" --engine <name> [--engine <name> ...]\n ask several engines in parallel without allowing edits\n atris engine ask --jobs <jobs.json>\n ask different read-only questions in parallel\n atris engine validate <receipt-path|latest> [--engine <name>]\n check ask answers with a different read-only referee\n atris engine validate scoreboard\n show pass rates by worker engine\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]\n one-command claim, worktree, build, verify, ship, ready\n atris engine login <provider> --yes\n upload a local provider CLI login to the backend vault\n atris engine login <provider> --computer [--seat <name>]\n atris engine login <provider> --business <id> [--seat <name>]\n sign in on an Atris computer by device flow\n atris engine login --list | --remove <provider>\n list or remove vaulted provider logins\n atris engine seats show which named accounts are ready to work\n atris engine seed <provider> --business <id>|--user\n push a vaulted login onto an Atris computer\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
|
|
2409
|
+
console.log('\n atris engine roster + current default\n atris engine roster [--json] show every job and its workers in order: search, build, review, and your own\n flags a model with a newer version on this machine, or a pin not proven in 30 days\n (a landed run, or "verified 2026-09-30" on its line, proves it)\n atris engine roster confirm renew every dated worker for 30 days\n atris engine roster session [clear]\n show or drop the roster changes for this shell only\n atris engine roster --available\n tools on this machine and the models each one offers\n atris engine roster --runs [job]\n the last 20 runs, newest first, with tool, model, time, and outcome\n atris engine assign <job> <tool> [--like <kind>] [--model <m>] [--effort <level>] [--max "20 min"] [--prep <job>] [--backup "<tool> [model]"] [--days <n>] [--add] [--session | --everywhere]\n atris engine assign <job> --promote "<tool> [model]" | --remove <tool> | --clear [--session | --everywhere]\n sets the lead worker of the job; --promote moves one of its workers to the top as written;\n --add puts one more at the end; jobs: search, build, review,\n or your own like "small build" (--like search|build|review when the name does not say);\n --prep search has that job read the task first and hand this worker a short brief;\n --everywhere is for all projects; --session is this shell only (set ATRIS_ROSTER_SESSION=<name>)\n atris engines --chart show the fleet as an org chart\n atris engine list --json [--all] full registry: default + engines with tier, roles, fallback, health (--all includes hidden engines)\n atris engine set <name> --duty leader|errands|learning [--models "a, b"]\n arrange the fleet and save its model policy\n atris engine resolve <role> or <job> [--json]\n choose the best ready engine for navigator|executor|validator or a roster job;\n notes always answers: its roster pick, else agy then haiku\n atris engine health <name> --set ready|not_installed|credit_out\n flip runtime health, for example when credits run out\n atris engine doctor [--json]\n probe which engine CLIs are installed here and sync that into health policy\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine bench [names...] [--runs N]\n ranked latency scoreboard of engine passes\n atris engine ask "<question>" --engine <name> [--engine <name> ...]\n ask several engines in parallel without allowing edits\n atris engine ask --jobs <jobs.json>\n ask different read-only questions in parallel\n atris engine validate <receipt-path|latest> [--engine <name>]\n check ask answers with a different read-only referee\n atris engine validate scoreboard\n show pass rates by worker engine\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]\n one-command claim, worktree, build, verify, ship, ready\n atris engine login <provider> --yes\n upload a local provider CLI login to the backend vault\n atris engine login <provider> --computer [--seat <name>]\n atris engine login <provider> --business <id> [--seat <name>]\n sign in on an Atris computer by device flow\n atris engine login --list | --remove <provider>\n list or remove vaulted provider logins\n atris engine seats show which named accounts are ready to work\n atris engine seed <provider> --business <id>|--user\n push a vaulted login onto an Atris computer\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
|
|
2271
2410
|
return 0;
|
|
2272
2411
|
}
|
|
2273
2412
|
if (isFreshWorkspace(root)) {
|
|
@@ -2316,6 +2455,7 @@ module.exports = {
|
|
|
2316
2455
|
readSavedEngine,
|
|
2317
2456
|
roster,
|
|
2318
2457
|
rosterReport,
|
|
2458
|
+
renderRosterReport,
|
|
2319
2459
|
jobRosterView,
|
|
2320
2460
|
expiringWorkers,
|
|
2321
2461
|
bootExpiringLine,
|