atris 3.38.0 → 3.41.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 +25 -6
- package/atris/PERSONA.md +8 -4
- package/atris.md +7 -0
- package/ax +2 -1
- package/bin/atris.js +31 -6
- package/commands/agent-spawn.js +13 -11
- package/commands/autoland.js +28 -77
- package/commands/bench.js +10 -12
- package/commands/business.js +345 -0
- package/commands/chat-scan.js +5 -7
- package/commands/codex-goal.js +8 -10
- package/commands/computer.js +20 -0
- package/commands/console.js +19 -3
- package/commands/decide.js +166 -0
- package/commands/deck.js +1 -4
- package/commands/drill.js +14 -24
- package/commands/engine.js +196 -8
- package/commands/gm.js +8 -6
- package/commands/harvest.js +1 -4
- package/commands/init.js +23 -3
- package/commands/land.js +8 -14
- package/commands/launchpad.js +1 -14
- package/commands/lifecycle.js +5 -5
- package/commands/log.js +55 -5
- package/commands/member.js +558 -574
- package/commands/mission.js +456 -237
- package/commands/pack.js +2746 -164
- package/commands/play.js +6 -4
- package/commands/probe.js +2 -2
- package/commands/pulse.js +15 -16
- package/commands/release.js +10 -9
- package/commands/router.js +5 -4
- package/commands/site-deploy.js +885 -0
- package/commands/site.js +11 -2
- package/commands/slop.js +14 -2
- package/commands/stream.js +4 -18
- package/commands/task.js +880 -558
- package/commands/taste.js +101 -0
- package/commands/team.js +176 -3
- package/commands/vercel.js +4 -2
- package/commands/voice.js +195 -0
- package/commands/watch.js +1 -22
- package/commands/wiki.js +1 -4
- package/commands/workflow.js +2 -2
- package/commands/worktree.js +1 -14
- package/commands/xp.js +27 -24
- package/lib/accept-verify-gate.js +5 -1
- package/lib/arg-parser.js +41 -0
- package/lib/auto-accept-certified.js +116 -1
- package/lib/autoland.js +66 -0
- package/lib/bench/runner.js +19 -1
- package/lib/context-gatherer.js +7 -1
- package/lib/engine-registry.js +141 -20
- package/lib/falsifier-probe.js +84 -0
- package/lib/fleet.js +65 -15
- package/lib/git-spawn.js +15 -0
- package/lib/json-file.js +37 -0
- package/lib/known-commands.js +2 -2
- package/lib/lesson-preflight.js +146 -0
- package/lib/loop-doctor.js +0 -2
- package/lib/mission-human-asks.js +28 -0
- package/lib/mission-protected-lane.js +4 -1
- package/lib/official-cli-integration.js +47 -2
- package/lib/orb-context.js +8 -1
- package/lib/pack-capabilities.js +685 -0
- package/lib/router-brain.js +51 -1
- package/lib/runner-command.js +0 -6
- package/lib/self-drive.js +44 -13
- package/lib/task-db.js +137 -3
- package/lib/task-decision.js +50 -0
- package/lib/taste-lessons.js +153 -0
- package/lib/tool-result-encode.js +17 -1
- package/lib/voice-gate.js +66 -0
- package/lib/wish-audit.js +1 -1
- package/lib/wish-delegate.js +1 -1
- package/lib/zip.js +95 -7
- package/package.json +2 -1
- package/templates/business-starter/persona.md +9 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
answerMissionHumanAsk,
|
|
5
|
+
listMissions,
|
|
6
|
+
listWorktreeRollupMissions,
|
|
7
|
+
pingMission,
|
|
8
|
+
} = require('./mission');
|
|
9
|
+
const { openHumanAsks, normalizeHumanAsks } = require('../lib/mission-human-asks');
|
|
10
|
+
const { redirectToWorkspaceRoot } = require('../lib/mission-root');
|
|
11
|
+
const { shortId } = require('../lib/short-name');
|
|
12
|
+
|
|
13
|
+
const TERMINAL_STATUSES = new Set(['stopped', 'complete']);
|
|
14
|
+
|
|
15
|
+
function missionTouchedAt(mission) {
|
|
16
|
+
return String(mission.updated_at || mission.created_at || '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function liveMissions(root = process.cwd()) {
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
return [...listMissions(root), ...listWorktreeRollupMissions(root)]
|
|
22
|
+
.filter((mission) => {
|
|
23
|
+
if (!mission || !mission.id || seen.has(mission.id) || TERMINAL_STATUSES.has(mission.status)) return false;
|
|
24
|
+
seen.add(mission.id);
|
|
25
|
+
return true;
|
|
26
|
+
})
|
|
27
|
+
.sort((left, right) => (
|
|
28
|
+
missionTouchedAt(right).localeCompare(missionTouchedAt(left))
|
|
29
|
+
|| String(left.id).localeCompare(String(right.id))
|
|
30
|
+
));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function collectOpenDecisions(root = process.cwd()) {
|
|
34
|
+
const decisions = [];
|
|
35
|
+
for (const mission of liveMissions(root)) {
|
|
36
|
+
const normalized = normalizeHumanAsks(mission.human_asks);
|
|
37
|
+
normalized.forEach((ask, askIndex) => {
|
|
38
|
+
if (!ask.text.trim() || ask.answered_at) return;
|
|
39
|
+
decisions.push({
|
|
40
|
+
number: decisions.length + 1,
|
|
41
|
+
owner: String(mission.owner || 'unowned'),
|
|
42
|
+
mission_id: mission.id,
|
|
43
|
+
mission_short_id: shortId(mission.id),
|
|
44
|
+
mission_status: mission.status,
|
|
45
|
+
mission_updated_at: missionTouchedAt(mission),
|
|
46
|
+
ask_index: askIndex,
|
|
47
|
+
text: ask.text,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return decisions;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function printHelp() {
|
|
55
|
+
console.log('Usage:');
|
|
56
|
+
console.log(' atris decide');
|
|
57
|
+
console.log(' atris decide <n> y|n|yes|no [--note "<text>"]');
|
|
58
|
+
console.log(' atris decide --json');
|
|
59
|
+
console.log(' atris decide <n> y --json');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fail(message, asJson, code = 2) {
|
|
63
|
+
if (asJson) {
|
|
64
|
+
console.log(JSON.stringify({ ok: false, action: 'decide_error', error: message }));
|
|
65
|
+
} else {
|
|
66
|
+
console.error(message);
|
|
67
|
+
}
|
|
68
|
+
process.exitCode = code;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseArgs(args) {
|
|
72
|
+
const asJson = args.includes('--json');
|
|
73
|
+
const rest = args.filter((arg) => arg !== '--json');
|
|
74
|
+
let note = '';
|
|
75
|
+
const noteIndex = rest.findIndex((arg) => arg === '--note' || String(arg).startsWith('--note='));
|
|
76
|
+
if (noteIndex !== -1) {
|
|
77
|
+
const noteArg = String(rest[noteIndex]);
|
|
78
|
+
if (noteArg === '--note') {
|
|
79
|
+
if (rest[noteIndex + 1] == null) return { asJson, error: '--note requires text' };
|
|
80
|
+
note = String(rest[noteIndex + 1]).trim();
|
|
81
|
+
rest.splice(noteIndex, 2);
|
|
82
|
+
} else {
|
|
83
|
+
note = noteArg.slice('--note='.length).trim();
|
|
84
|
+
rest.splice(noteIndex, 1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { asJson, note, rest };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function decideCommand(args = []) {
|
|
91
|
+
redirectToWorkspaceRoot();
|
|
92
|
+
const parsed = parseArgs(args);
|
|
93
|
+
if (parsed.error) return fail(parsed.error, parsed.asJson);
|
|
94
|
+
const { asJson, note, rest } = parsed;
|
|
95
|
+
if (rest.includes('--help') || rest.includes('-h') || rest[0] === 'help') {
|
|
96
|
+
printHelp();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const decisions = collectOpenDecisions();
|
|
101
|
+
if (!rest.length) {
|
|
102
|
+
if (asJson) {
|
|
103
|
+
console.log(JSON.stringify({
|
|
104
|
+
ok: true,
|
|
105
|
+
action: 'decide_list',
|
|
106
|
+
count: decisions.length,
|
|
107
|
+
decisions,
|
|
108
|
+
}, null, 2));
|
|
109
|
+
} else if (!decisions.length) {
|
|
110
|
+
console.log('nothing is waiting for a decision.');
|
|
111
|
+
} else {
|
|
112
|
+
for (const decision of decisions) {
|
|
113
|
+
console.log(`[${decision.number}] ${decision.owner} · ${decision.mission_short_id} · ${decision.text}`);
|
|
114
|
+
}
|
|
115
|
+
console.log('atris decide <n> y|n');
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (rest.length !== 2) {
|
|
121
|
+
return fail('usage: atris decide <n> y|n|yes|no [--note "<text>"]', asJson);
|
|
122
|
+
}
|
|
123
|
+
const number = Number(rest[0]);
|
|
124
|
+
if (!Number.isInteger(number) || number < 1) {
|
|
125
|
+
return fail('decision number must be a positive integer', asJson);
|
|
126
|
+
}
|
|
127
|
+
const answerToken = String(rest[1]).toLowerCase();
|
|
128
|
+
const answer = answerToken === 'y' || answerToken === 'yes'
|
|
129
|
+
? 'yes'
|
|
130
|
+
: (answerToken === 'n' || answerToken === 'no' ? 'no' : null);
|
|
131
|
+
if (!answer) return fail('answer must be y, n, yes, or no', asJson);
|
|
132
|
+
const decision = decisions[number - 1];
|
|
133
|
+
if (!decision) return fail(`decision ${number} is not open`, asJson, 1);
|
|
134
|
+
|
|
135
|
+
const message = `Decision on "${decision.text}": ${answer.toUpperCase()}${note ? ` — ${note}` : ''}`;
|
|
136
|
+
pingMission([decision.mission_id, message, '--from', 'decide'], { silent: true });
|
|
137
|
+
const mission = answerMissionHumanAsk(decision.mission_id, decision.ask_index, answer, note);
|
|
138
|
+
const remainingOpenAsks = openHumanAsks(mission.human_asks).length;
|
|
139
|
+
const payload = {
|
|
140
|
+
ok: true,
|
|
141
|
+
action: 'decision_answered',
|
|
142
|
+
decision: {
|
|
143
|
+
...decision,
|
|
144
|
+
answer,
|
|
145
|
+
note,
|
|
146
|
+
message,
|
|
147
|
+
},
|
|
148
|
+
mission: {
|
|
149
|
+
id: mission.id,
|
|
150
|
+
short_id: decision.mission_short_id,
|
|
151
|
+
owner: mission.owner,
|
|
152
|
+
status: mission.status,
|
|
153
|
+
remaining_open_asks: remainingOpenAsks,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
if (asJson) {
|
|
157
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
158
|
+
} else {
|
|
159
|
+
console.log(`sent to ${decision.mission_short_id}: ${message}`);
|
|
160
|
+
console.log(`mission ${decision.mission_short_id} will read it on its next tick.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
decideCommand,
|
|
166
|
+
};
|
package/commands/deck.js
CHANGED
|
@@ -17,6 +17,7 @@ const fs = require('fs');
|
|
|
17
17
|
const https = require('https');
|
|
18
18
|
const os = require('os');
|
|
19
19
|
const path = require('path');
|
|
20
|
+
const { hasFlag } = require('../lib/arg-parser');
|
|
20
21
|
const { buildDeck, THEMES, notesRequests } = require('../lib/slides-deck');
|
|
21
22
|
const {
|
|
22
23
|
lintSpec,
|
|
@@ -118,10 +119,6 @@ function flag(argv, name) {
|
|
|
118
119
|
return i !== -1 ? argv[i + 1] : null;
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
function hasFlag(argv, name) {
|
|
122
|
-
return argv.includes(name);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
122
|
// Flags that take a value, so their following token is consumed (not a path).
|
|
126
123
|
const VALUE_FLAGS = new Set(['--theme', '--title', '--update', '--out', '--style', '--url', '--md']);
|
|
127
124
|
|
package/commands/drill.js
CHANGED
|
@@ -3,22 +3,12 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const {
|
|
6
|
+
const { hasFlag } = require('../lib/arg-parser');
|
|
7
|
+
const { runGit } = require('../lib/git-spawn');
|
|
7
8
|
|
|
8
9
|
const VERIFY_COMMAND = 'node -e "process.exit(0)"';
|
|
9
10
|
const COAUTHOR = 'Co-authored-by: Atris <299057014+atris-builder[bot]@users.noreply.github.com>';
|
|
10
11
|
|
|
11
|
-
function hasFlag(args, name) {
|
|
12
|
-
return args.includes(name);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function runGit(args, cwd, options = {}) {
|
|
16
|
-
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
17
|
-
if (options.check === false || result.status === 0) return result;
|
|
18
|
-
const output = (result.stderr || result.stdout || `git ${args.join(' ')} failed`).trim();
|
|
19
|
-
throw new Error(output);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
12
|
function parseJsonOutput(text) {
|
|
23
13
|
const raw = String(text || '').trim();
|
|
24
14
|
if (!raw) throw new Error('expected JSON output, got empty output');
|
|
@@ -97,9 +87,9 @@ async function createSandbox(ctx) {
|
|
|
97
87
|
ctx.tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-drill-'));
|
|
98
88
|
ctx.sandboxPath = path.join(ctx.tmpRoot, 'sandbox');
|
|
99
89
|
fs.mkdirSync(ctx.sandboxPath, { recursive: true });
|
|
100
|
-
runGit(['init', '-b', 'master'], ctx.sandboxPath);
|
|
101
|
-
runGit(['config', 'user.email', 'drill@example.invalid'], ctx.sandboxPath);
|
|
102
|
-
runGit(['config', 'user.name', 'Atris Drill'], ctx.sandboxPath);
|
|
90
|
+
runGit(['init', '-b', 'master'], { cwd: ctx.sandboxPath });
|
|
91
|
+
runGit(['config', 'user.email', 'drill@example.invalid'], { cwd: ctx.sandboxPath });
|
|
92
|
+
runGit(['config', 'user.name', 'Atris Drill'], { cwd: ctx.sandboxPath });
|
|
103
93
|
fs.writeFileSync(path.join(ctx.sandboxPath, 'README.md'), '# Atris drill sandbox\n', 'utf8');
|
|
104
94
|
await withSandboxProcessState(ctx.sandboxPath, {
|
|
105
95
|
ATRIS_SKIP_UPDATE_CHECK: '1',
|
|
@@ -108,8 +98,8 @@ async function createSandbox(ctx) {
|
|
|
108
98
|
}, async () => {
|
|
109
99
|
await callInitAtris(ctx.sandboxPath);
|
|
110
100
|
});
|
|
111
|
-
runGit(['add', '-A'], ctx.sandboxPath);
|
|
112
|
-
runGit(['commit', '-m', 'initial sandbox'], ctx.sandboxPath);
|
|
101
|
+
runGit(['add', '-A'], { cwd: ctx.sandboxPath });
|
|
102
|
+
runGit(['commit', '-m', 'initial sandbox'], { cwd: ctx.sandboxPath });
|
|
113
103
|
}
|
|
114
104
|
|
|
115
105
|
async function wishCaptured(ctx) {
|
|
@@ -199,10 +189,10 @@ async function landingShippedLocal(ctx) {
|
|
|
199
189
|
try {
|
|
200
190
|
require('../lib/task-db').close();
|
|
201
191
|
} catch {}
|
|
202
|
-
const dirty = runGit(['status', '--porcelain'], ctx.sandboxPath).stdout.trim();
|
|
192
|
+
const dirty = runGit(['status', '--porcelain'], { cwd: ctx.sandboxPath }).stdout.trim();
|
|
203
193
|
if (dirty) {
|
|
204
|
-
runGit(['add', '-A'], ctx.sandboxPath);
|
|
205
|
-
runGit(['commit', '-m', 'record drill pipeline state'], ctx.sandboxPath);
|
|
194
|
+
runGit(['add', '-A'], { cwd: ctx.sandboxPath });
|
|
195
|
+
runGit(['commit', '-m', 'record drill pipeline state'], { cwd: ctx.sandboxPath });
|
|
206
196
|
}
|
|
207
197
|
ctx.landingWorktree = path.join(ctx.tmpRoot, 'landing-worktree');
|
|
208
198
|
const created = worktree.createAgentWorktree({
|
|
@@ -229,9 +219,9 @@ async function landingShippedLocal(ctx) {
|
|
|
229
219
|
assertOk(shipped.value === 0, `worktree ship exited ${shipped.value}`);
|
|
230
220
|
assertOk(/local mode/.test(`${shipped.stdout}\n${shipped.stderr}`), 'worktree ship did not report local mode');
|
|
231
221
|
});
|
|
232
|
-
runGit(['worktree', 'remove', ctx.landingWorktree, '--force'], ctx.sandboxPath);
|
|
233
|
-
runGit(['branch', '-d', ctx.landingBranch], ctx.sandboxPath);
|
|
234
|
-
const landed = runGit(['show', 'master:drill-landing.txt'], ctx.sandboxPath).stdout;
|
|
222
|
+
runGit(['worktree', 'remove', ctx.landingWorktree, '--force'], { cwd: ctx.sandboxPath });
|
|
223
|
+
runGit(['branch', '-d', ctx.landingBranch], { cwd: ctx.sandboxPath });
|
|
224
|
+
const landed = runGit(['show', 'master:drill-landing.txt'], { cwd: ctx.sandboxPath }).stdout;
|
|
235
225
|
assertOk(landed.trim() === 'local landing ok', 'landing file was not merged into sandbox master');
|
|
236
226
|
}
|
|
237
227
|
|
|
@@ -244,7 +234,7 @@ async function ledgerChecked(ctx) {
|
|
|
244
234
|
assertOk(currentMission, `mission not found: ${ctx.missionId}`);
|
|
245
235
|
assertOk(currentMission.verifier_result && currentMission.verifier_result.passed === true, 'mission verifier result is not passing');
|
|
246
236
|
assertOk(currentMission.receipt_path && fs.existsSync(path.join(ctx.sandboxPath, currentMission.receipt_path)), `mission receipt missing: ${currentMission.receipt_path}`);
|
|
247
|
-
const unmerged = runGit(['branch', '--no-merged', 'master'], ctx.sandboxPath).stdout
|
|
237
|
+
const unmerged = runGit(['branch', '--no-merged', 'master'], { cwd: ctx.sandboxPath }).stdout
|
|
248
238
|
.split(/\r?\n/)
|
|
249
239
|
.map((line) => line.replace(/^\*/, '').trim())
|
|
250
240
|
.filter(Boolean)
|
package/commands/engine.js
CHANGED
|
@@ -26,6 +26,7 @@ const {
|
|
|
26
26
|
} = require('../lib/runner-command');
|
|
27
27
|
const {
|
|
28
28
|
ENGINE_ROLES,
|
|
29
|
+
ENGINE_DUTIES,
|
|
29
30
|
ENGINE_HEALTH_STATUSES,
|
|
30
31
|
binInstalled,
|
|
31
32
|
canonicalEngineName,
|
|
@@ -34,6 +35,9 @@ const {
|
|
|
34
35
|
readEngineRegistry,
|
|
35
36
|
resolveEngineForRole,
|
|
36
37
|
resolveEngineForRoleRanked,
|
|
38
|
+
requireEngineBin,
|
|
39
|
+
engineDoctorReport,
|
|
40
|
+
setEngineOverrides,
|
|
37
41
|
setEngineHealth,
|
|
38
42
|
} = require('../lib/engine-registry');
|
|
39
43
|
const { FLEET_CAPABLE, runDispatchFlight } = require('../lib/fleet');
|
|
@@ -90,14 +94,22 @@ function readSavedEngine(root = process.cwd()) {
|
|
|
90
94
|
|
|
91
95
|
// The default engine for this workspace, in precedence order:
|
|
92
96
|
// env (per-run flags land here) -> .atris/engine.json -> house default.
|
|
93
|
-
//
|
|
97
|
+
// When nothing is configured the pick comes from the registry's saved health
|
|
98
|
+
// (seeded once on first sight, refreshed by `atris engine doctor`), never a
|
|
99
|
+
// machine probe: policy over probes. A stale "ready" is the execution stage's
|
|
100
|
+
// problem, where requireEngineBin fails in one plain sentence.
|
|
94
101
|
function resolveDefaultEngine(root = process.cwd()) {
|
|
95
102
|
const env = canonicalEngineName(process.env.ATRIS_RUNNER_PROFILE);
|
|
96
103
|
if (env) return { name: env, source: 'env' };
|
|
97
104
|
const saved = readSavedEngine(root);
|
|
98
105
|
if (saved) return { name: saved, source: 'saved' };
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
const ready = new Set(
|
|
107
|
+
engineRegistryView(root)
|
|
108
|
+
.filter((engine) => engine.health && engine.health.status === 'ready')
|
|
109
|
+
.map((engine) => engine.id)
|
|
110
|
+
);
|
|
111
|
+
if (ready.has(HOUSE_ENGINE)) return { name: HOUSE_ENGINE, source: 'house' };
|
|
112
|
+
const fallback = RUNNER_PROFILE_NAMES.find((name) => ready.has(name));
|
|
101
113
|
return fallback ? { name: fallback, source: 'detected' } : { name: HOUSE_ENGINE, source: 'none' };
|
|
102
114
|
}
|
|
103
115
|
|
|
@@ -887,13 +899,16 @@ function printRoster(root) {
|
|
|
887
899
|
const found = list.filter((e) => e.installed).length;
|
|
888
900
|
const current = resolveDefaultEngine(root);
|
|
889
901
|
console.log('');
|
|
890
|
-
console.log(` engines
|
|
902
|
+
console.log(` engines: ${found} intelligence${found === 1 ? '' : 's'} found`);
|
|
891
903
|
console.log('');
|
|
892
904
|
for (const engine of list) {
|
|
893
905
|
const mark = engine.default ? '→' : ' ';
|
|
894
906
|
const state = engine.health.status === 'not_installed' ? 'not installed' : engine.health.status.replace(/_/g, ' ');
|
|
895
907
|
const roles = engine.roles.join(',');
|
|
896
908
|
console.log(` ${mark} ${engine.id.padEnd(12)} ${state.padEnd(13)} ${engine.tier.padEnd(4)} ${roles}`);
|
|
909
|
+
const details = [`models: ${engine.models.join(', ')}`];
|
|
910
|
+
if (engine.duty) details.push(`duty: ${engine.duty}`);
|
|
911
|
+
console.log(` ${details.join(' ')}`);
|
|
897
912
|
}
|
|
898
913
|
console.log('');
|
|
899
914
|
console.log(` default: ${current.name}${current.source === 'saved' ? ' (set here)' : current.source === 'env' ? ' (this session)' : ''}`);
|
|
@@ -901,6 +916,139 @@ function printRoster(root) {
|
|
|
901
916
|
console.log('');
|
|
902
917
|
}
|
|
903
918
|
|
|
919
|
+
function chartEngineLabel(engine) {
|
|
920
|
+
const id = String(engine && (engine.id || engine.name) || '').trim();
|
|
921
|
+
const models = Array.isArray(engine && engine.models)
|
|
922
|
+
? engine.models.map((model) => String(model || '').trim()).filter(Boolean)
|
|
923
|
+
: [];
|
|
924
|
+
return models.length ? `${id}: ${models.join(', ')}` : id;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function chartBox(title, engines, note = '', emptyLabel = 'unassigned') {
|
|
928
|
+
const labels = engines.length ? engines.map(chartEngineLabel) : (emptyLabel ? [emptyLabel] : []);
|
|
929
|
+
const body = [title, ...labels, ...(note ? [note] : [])];
|
|
930
|
+
const innerWidth = Math.max(...body.map((line) => line.length)) + 2;
|
|
931
|
+
return {
|
|
932
|
+
width: innerWidth + 2,
|
|
933
|
+
lines: [
|
|
934
|
+
`┌${'─'.repeat(innerWidth)}┐`,
|
|
935
|
+
...body.map((line) => `│ ${line.padEnd(innerWidth - 2)} │`),
|
|
936
|
+
`└${'─'.repeat(innerWidth)}┘`,
|
|
937
|
+
],
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function centerChartLine(line, width, center = Math.floor((width - 1) / 2)) {
|
|
942
|
+
const left = Math.max(0, center - Math.floor(line.length / 2));
|
|
943
|
+
return `${' '.repeat(left)}${line}`.padEnd(width);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function renderEngineChart(registry) {
|
|
947
|
+
const engines = Array.isArray(registry) ? registry : ((registry && registry.engines) || []);
|
|
948
|
+
const specialDuties = new Set(ENGINE_DUTIES);
|
|
949
|
+
const leaders = engines.filter((entry) => entry.duty === 'leader');
|
|
950
|
+
const rolePool = engines.filter((entry) => !specialDuties.has(entry.duty));
|
|
951
|
+
const builders = rolePool.filter((entry) => Array.isArray(entry.roles) && entry.roles.includes('executor'));
|
|
952
|
+
const checkers = rolePool.filter((entry) => Array.isArray(entry.roles) && entry.roles.includes('validator'));
|
|
953
|
+
const errands = engines.filter((entry) => entry.duty === 'errands');
|
|
954
|
+
const apprentices = engines.filter((entry) => entry.duty === 'learning');
|
|
955
|
+
|
|
956
|
+
const ownerBox = chartBox('owner', [], '', '');
|
|
957
|
+
const leaderBox = chartBox('leader', leaders);
|
|
958
|
+
const columns = [
|
|
959
|
+
chartBox('builders', builders),
|
|
960
|
+
chartBox('checkers', checkers),
|
|
961
|
+
chartBox('errands', errands),
|
|
962
|
+
];
|
|
963
|
+
const gap = 3;
|
|
964
|
+
const totalWidth = columns.reduce((sum, column) => sum + column.width, 0) + (gap * (columns.length - 1));
|
|
965
|
+
const columnHeight = Math.max(...columns.map((column) => column.lines.length));
|
|
966
|
+
const columnLines = [];
|
|
967
|
+
for (let row = 0; row < columnHeight; row += 1) {
|
|
968
|
+
columnLines.push(columns.map((column) => (column.lines[row] || '').padEnd(column.width)).join(' '.repeat(gap)));
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const centers = [];
|
|
972
|
+
let offset = 0;
|
|
973
|
+
for (const column of columns) {
|
|
974
|
+
centers.push(offset + Math.floor(column.width / 2));
|
|
975
|
+
offset += column.width + gap;
|
|
976
|
+
}
|
|
977
|
+
const fleetCenter = centers[1];
|
|
978
|
+
const branch = Array(totalWidth).fill(' ');
|
|
979
|
+
for (let i = centers[0]; i <= centers[centers.length - 1]; i += 1) branch[i] = '─';
|
|
980
|
+
branch[centers[0]] = '┌';
|
|
981
|
+
branch[centers[centers.length - 1]] = '┐';
|
|
982
|
+
for (const center of centers.slice(1, -1)) branch[center] = '┬';
|
|
983
|
+
branch[fleetCenter] = centers.includes(fleetCenter) ? '┼' : '┴';
|
|
984
|
+
const stems = Array(totalWidth).fill(' ');
|
|
985
|
+
for (const center of centers) stems[center] = '│';
|
|
986
|
+
|
|
987
|
+
const lines = [
|
|
988
|
+
...ownerBox.lines.map((line) => centerChartLine(line, totalWidth, fleetCenter)),
|
|
989
|
+
centerChartLine('│', totalWidth, fleetCenter),
|
|
990
|
+
...leaderBox.lines.map((line) => centerChartLine(line, totalWidth, fleetCenter)),
|
|
991
|
+
centerChartLine('│', totalWidth, fleetCenter),
|
|
992
|
+
branch.join(''),
|
|
993
|
+
stems.join(''),
|
|
994
|
+
...columnLines,
|
|
995
|
+
];
|
|
996
|
+
if (apprentices.length) {
|
|
997
|
+
const apprenticeBox = chartBox('apprentice', apprentices, 'learning the system');
|
|
998
|
+
lines.push(centerChartLine('│', totalWidth, fleetCenter));
|
|
999
|
+
lines.push(...apprenticeBox.lines.map((line) => centerChartLine(line, totalWidth, fleetCenter)));
|
|
1000
|
+
}
|
|
1001
|
+
return lines.map((line) => line.replace(/\s+$/, '')).join('\n');
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function printEngineChart(root) {
|
|
1005
|
+
console.log(renderEngineChart(readEngineRegistry(root)));
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function flagValue(args, flag) {
|
|
1009
|
+
const equals = `${flag}=`;
|
|
1010
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1011
|
+
const value = String(args[i] || '');
|
|
1012
|
+
if (value === flag) return { present: true, value: args[i + 1] || '' };
|
|
1013
|
+
if (value.startsWith(equals)) return { present: true, value: value.slice(equals.length) };
|
|
1014
|
+
}
|
|
1015
|
+
return { present: false, value: '' };
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function runSetEngineCommand(args, root) {
|
|
1019
|
+
const name = String(args[0] || '').trim();
|
|
1020
|
+
const dutyFlag = flagValue(args.slice(1), '--duty');
|
|
1021
|
+
const modelsFlag = flagValue(args.slice(1), '--models');
|
|
1022
|
+
const duty = String(dutyFlag.value || '').trim();
|
|
1023
|
+
const models = String(modelsFlag.value || '').split(',').map((model) => model.trim()).filter(Boolean);
|
|
1024
|
+
if (!name || (!dutyFlag.present && !modelsFlag.present)) {
|
|
1025
|
+
console.error('usage: atris engine set <name> [--duty leader|errands|learning] [--models "a, b"]');
|
|
1026
|
+
return 2;
|
|
1027
|
+
}
|
|
1028
|
+
if (dutyFlag.present && !ENGINE_DUTIES.includes(duty)) {
|
|
1029
|
+
console.error(`unknown duty "${duty}". known duties: ${ENGINE_DUTIES.join(', ')}`);
|
|
1030
|
+
return 2;
|
|
1031
|
+
}
|
|
1032
|
+
if (modelsFlag.present && !models.length) {
|
|
1033
|
+
console.error('models must include at least one name');
|
|
1034
|
+
return 2;
|
|
1035
|
+
}
|
|
1036
|
+
try {
|
|
1037
|
+
const updated = setEngineOverrides(name, {
|
|
1038
|
+
...(dutyFlag.present ? { duty } : {}),
|
|
1039
|
+
...(modelsFlag.present ? { models } : {}),
|
|
1040
|
+
}, root);
|
|
1041
|
+
const details = [];
|
|
1042
|
+
if (updated.duty) details.push(`duty ${updated.duty}`);
|
|
1043
|
+
if (updated.models) details.push(`models ${updated.models.join(', ')}`);
|
|
1044
|
+
console.log(`${updated.id} updated: ${details.join('; ')}`);
|
|
1045
|
+
return 0;
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
console.error(String(err.message || err).replace(/^Unknown/, 'unknown'));
|
|
1048
|
+
return 2;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
904
1052
|
function registryPayload(root) {
|
|
905
1053
|
const current = resolveDefaultEngine(root);
|
|
906
1054
|
const registry = readEngineRegistry(root);
|
|
@@ -956,6 +1104,28 @@ function runResolveCommand(args, root) {
|
|
|
956
1104
|
return 0;
|
|
957
1105
|
}
|
|
958
1106
|
|
|
1107
|
+
// Doctor is the one opt-in place that probes the machine: it checks every
|
|
1108
|
+
// engine binary, reports installed state, and folds ready/not_installed flips
|
|
1109
|
+
// back into the policy file. Routing itself never probes.
|
|
1110
|
+
function runDoctorCommand(args, root) {
|
|
1111
|
+
const json = args.includes('--json');
|
|
1112
|
+
const engines = engineDoctorReport(root);
|
|
1113
|
+
if (json) {
|
|
1114
|
+
console.log(JSON.stringify({ ok: true, engines }, null, 2));
|
|
1115
|
+
return 0;
|
|
1116
|
+
}
|
|
1117
|
+
console.log('');
|
|
1118
|
+
for (const engine of engines) {
|
|
1119
|
+
const state = engine.installed ? 'installed' : 'missing';
|
|
1120
|
+
const health = engine.health.status.replace(/_/g, ' ');
|
|
1121
|
+
console.log(` ${engine.id.padEnd(12)} ${String(engine.bin).padEnd(14)} ${state.padEnd(10)} health: ${health}`);
|
|
1122
|
+
}
|
|
1123
|
+
console.log('');
|
|
1124
|
+
console.log(' probed just now and saved to the registry; routing reads that saved policy, never the machine.');
|
|
1125
|
+
console.log('');
|
|
1126
|
+
return 0;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
959
1129
|
function runHealthCommand(args, root) {
|
|
960
1130
|
const json = args.includes('--json');
|
|
961
1131
|
const positional = args.filter((a) => !String(a).startsWith('--'));
|
|
@@ -1203,9 +1373,12 @@ function runDispatchCommand(args, root) {
|
|
|
1203
1373
|
return 2;
|
|
1204
1374
|
}
|
|
1205
1375
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1376
|
+
// Execution stage: this is where the binary must exist. Routing above never
|
|
1377
|
+
// probed the machine; a missing CLI fails loudly here instead.
|
|
1378
|
+
try {
|
|
1379
|
+
requireEngineBin(canonical);
|
|
1380
|
+
} catch (err) {
|
|
1381
|
+
console.error(`engine dispatch: ${err.message}`);
|
|
1209
1382
|
return 2;
|
|
1210
1383
|
}
|
|
1211
1384
|
return runDispatchFlight({ root, taskIds, engine: canonical, prompt: promptOverride, yolo, ...(base ? { checkoutBase: base } : {}) }).then((flight) => {
|
|
@@ -1248,6 +1421,19 @@ function engineCommand(args = [], deps = {}) {
|
|
|
1248
1421
|
return runHealthCommand(args.slice(args.indexOf('health') + 1), root);
|
|
1249
1422
|
}
|
|
1250
1423
|
|
|
1424
|
+
if (sub === 'doctor') {
|
|
1425
|
+
return runDoctorCommand(args.slice(args.indexOf('doctor') + 1), root);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
if (sub === 'set') {
|
|
1429
|
+
return runSetEngineCommand(args.slice(args.indexOf('set') + 1), root);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if (sub === 'chart' || args.includes('--chart')) {
|
|
1433
|
+
printEngineChart(root);
|
|
1434
|
+
return 0;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1251
1437
|
if (!sub || sub === 'list' || sub === 'status') {
|
|
1252
1438
|
if (json) {
|
|
1253
1439
|
console.log(JSON.stringify(registryPayload(root), null, 2));
|
|
@@ -1266,7 +1452,7 @@ function engineCommand(args = [], deps = {}) {
|
|
|
1266
1452
|
}
|
|
1267
1453
|
|
|
1268
1454
|
if (sub === 'help') {
|
|
1269
|
-
console.log('\n atris engine roster + current default\n atris engine list --json full registry: default + engines with tier, roles, fallback, health\n atris engine resolve <role> [--json]\n choose the best ready engine for navigator|executor|validator\n atris engine health <name> --set ready|not_installed|credit_out\n flip runtime health, for example when credits run out\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 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');
|
|
1455
|
+
console.log('\n atris engine roster + current default\n atris engines --chart show the fleet as an org chart\n atris engine list --json full registry: default + engines with tier, roles, fallback, health\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> [--json]\n choose the best ready engine for navigator|executor|validator\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 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');
|
|
1270
1456
|
return 0;
|
|
1271
1457
|
}
|
|
1272
1458
|
|
|
@@ -1304,6 +1490,8 @@ module.exports = {
|
|
|
1304
1490
|
readEngineRegistry,
|
|
1305
1491
|
resolveEngineForRole,
|
|
1306
1492
|
setEngineHealth,
|
|
1493
|
+
setEngineOverrides,
|
|
1494
|
+
renderEngineChart,
|
|
1307
1495
|
parseDispatchArgs,
|
|
1308
1496
|
runDispatchCommand,
|
|
1309
1497
|
ENGINE_LOGIN_MANIFESTS,
|
package/commands/gm.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { hasFlag: hasExactFlag } = require('../lib/arg-parser');
|
|
6
7
|
|
|
7
8
|
const AGENTXP_LEADERBOARD_URL = 'https://api.atris.ai/api/agentxp/leaderboard';
|
|
8
9
|
const AGENTXP_GLOBAL_SYNC_RULE = 'Run atris login, then sync. Owner-provided sync tokens are guided-demo fallback only.';
|
|
@@ -54,8 +55,9 @@ function flag(args, name) {
|
|
|
54
55
|
return null;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
// This command treats --name=value as flag presence as well as a standalone flag.
|
|
59
|
+
function hasFlagOrValue(args, name) {
|
|
60
|
+
return hasExactFlag(args, name) || args.some(arg => arg.startsWith(`${name}=`));
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
function positional(args) {
|
|
@@ -185,7 +187,7 @@ function pickSeedPlayer(workspaceRoot, tasks, args = []) {
|
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
function ensureStarterMission(taskDb, db, workspaceRoot, tasks, args = []) {
|
|
188
|
-
if (
|
|
190
|
+
if (hasFlagOrValue(args, '--no-seed')) return { tasks, seeded: null };
|
|
189
191
|
if (!fs.existsSync(path.join(workspaceRoot, 'atris'))) return { tasks, seeded: null };
|
|
190
192
|
if (activeAgentXpTasks(tasks).length) return { tasks, seeded: null };
|
|
191
193
|
|
|
@@ -658,7 +660,7 @@ async function gmCommand(...args) {
|
|
|
658
660
|
return memberCommand('wake', member, ...passthrough);
|
|
659
661
|
}
|
|
660
662
|
|
|
661
|
-
if (!
|
|
663
|
+
if (!hasFlagOrValue(args, '--json') && !hasFlagOrValue(args, '--watch')) {
|
|
662
664
|
try {
|
|
663
665
|
const { gameCommand } = require('./game');
|
|
664
666
|
const dashboardShown = await gameCommand([], { silentMissing: true });
|
|
@@ -669,12 +671,12 @@ async function gmCommand(...args) {
|
|
|
669
671
|
}
|
|
670
672
|
|
|
671
673
|
const state = gmState(args);
|
|
672
|
-
if (
|
|
674
|
+
if (hasFlagOrValue(args, '--json')) {
|
|
673
675
|
console.log(JSON.stringify(state, null, 2));
|
|
674
676
|
return;
|
|
675
677
|
}
|
|
676
678
|
render(state);
|
|
677
|
-
if (
|
|
679
|
+
if (hasFlagOrValue(args, '--watch')) {
|
|
678
680
|
await gmWatch(args);
|
|
679
681
|
}
|
|
680
682
|
}
|
package/commands/harvest.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const { hasFlag } = require('../lib/arg-parser');
|
|
3
4
|
const {
|
|
4
5
|
isGenericInboxPlaceholder,
|
|
5
6
|
seedInboxFromMove,
|
|
6
7
|
} = require('../lib/next-moves');
|
|
7
8
|
|
|
8
|
-
function hasFlag(args, name) {
|
|
9
|
-
return args.includes(name);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
9
|
function safeRead(file) {
|
|
13
10
|
try { return fs.readFileSync(file, 'utf8'); } catch { return ''; }
|
|
14
11
|
}
|