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
package/lib/engine-registry.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const
|
|
5
|
+
const childProcess = require('child_process');
|
|
6
6
|
const {
|
|
7
7
|
RUNNER_PROFILE_DEFS,
|
|
8
8
|
RUNNER_PROFILE_ALIASES,
|
|
@@ -13,30 +13,33 @@ const { rankEnginesDetailed, routerPickExplanation } = require('./router-brain')
|
|
|
13
13
|
const ENGINE_REGISTRY_SCHEMA = 'atris.engine_registry.v2';
|
|
14
14
|
const ENGINE_TIERS = Object.freeze(['fast', 'pro', 'max']);
|
|
15
15
|
const ENGINE_ROLES = Object.freeze(['navigator', 'executor', 'validator']);
|
|
16
|
+
const ENGINE_DUTIES = Object.freeze(['leader', 'errands', 'learning']);
|
|
16
17
|
const ENGINE_HEALTH_STATUSES = Object.freeze(['ready', 'not_installed', 'credit_out', 'error']);
|
|
17
18
|
|
|
18
19
|
const ENGINE_SEED_META = Object.freeze({
|
|
19
|
-
'atris-fast': Object.freeze({ tier: 'fast', roles: Object.freeze(['navigator']), fallback_order: 10 }),
|
|
20
|
-
codex: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), fallback_order: 10 }),
|
|
21
|
-
claude: Object.freeze({ tier: 'max', roles: Object.freeze(['validator', 'executor']), fallback_order: 20 }),
|
|
22
|
-
cursor: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), fallback_order: 30 }),
|
|
23
|
-
devin: Object.freeze({ tier: 'max', roles: Object.freeze(['executor']), fallback_order: 40 }),
|
|
24
|
-
grok: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), fallback_order: 45 }),
|
|
25
|
-
fable: Object.freeze({ tier: 'max', roles: Object.freeze(['validator', 'executor']), fallback_order: 50 }),
|
|
26
|
-
composer: Object.freeze({ tier: 'fast', roles: Object.freeze(['navigator', 'executor']), fallback_order: 60 }),
|
|
27
|
-
haiku: Object.freeze({ tier: 'fast', roles: Object.freeze(['validator']), fallback_order: 70 }),
|
|
28
|
-
|
|
29
|
-
droid: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), fallback_order: 90 }),
|
|
20
|
+
'atris-fast': Object.freeze({ tier: 'fast', roles: Object.freeze(['navigator']), models: Object.freeze(['atris fast']), duty: 'learning', fallback_order: 10 }),
|
|
21
|
+
codex: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), models: Object.freeze(['codex']), fallback_order: 10 }),
|
|
22
|
+
claude: Object.freeze({ tier: 'max', roles: Object.freeze(['validator', 'executor']), models: Object.freeze(['opus 5', 'opus 4.8', 'fable', 'haiku']), fallback_order: 20 }),
|
|
23
|
+
cursor: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), models: Object.freeze(['composer 2.5', 'grok 4.5', 'kimi 3']), fallback_order: 30 }),
|
|
24
|
+
devin: Object.freeze({ tier: 'max', roles: Object.freeze(['executor']), models: Object.freeze(['built-in router']), duty: 'errands', fallback_order: 40 }),
|
|
25
|
+
grok: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), models: Object.freeze(['grok 4.5']), fallback_order: 45 }),
|
|
26
|
+
fable: Object.freeze({ tier: 'max', roles: Object.freeze(['validator', 'executor']), models: Object.freeze(['opus 5', 'opus 4.8', 'fable', 'haiku']), duty: 'leader', fallback_order: 50 }),
|
|
27
|
+
composer: Object.freeze({ tier: 'fast', roles: Object.freeze(['navigator', 'executor']), models: Object.freeze(['composer 2.5']), fallback_order: 60 }),
|
|
28
|
+
haiku: Object.freeze({ tier: 'fast', roles: Object.freeze(['validator']), models: Object.freeze(['haiku']), fallback_order: 70 }),
|
|
29
|
+
droid: Object.freeze({ tier: 'pro', roles: Object.freeze(['executor']), models: Object.freeze(['built-in router']), duty: 'errands', fallback_order: 90 }),
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
function engineRegistryFile(root = process.cwd()) {
|
|
33
33
|
return path.join(root, '.atris', 'state', 'engines.json');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Machine probe. Routing never calls this on a settled registry: it runs once
|
|
37
|
+
// when an engine first appears (seeding the policy file), at the execution
|
|
38
|
+
// stage right before a spawn, and on the explicit `atris engine doctor`.
|
|
36
39
|
function binInstalled(bin) {
|
|
37
40
|
const safe = String(bin || '').replace(/[^A-Za-z0-9_.-]/g, '');
|
|
38
41
|
if (!safe) return false;
|
|
39
|
-
const probe = spawnSync('sh', ['-c', `command -v ${safe}`], { encoding: 'utf8' });
|
|
42
|
+
const probe = childProcess.spawnSync('sh', ['-c', `command -v ${safe}`], { encoding: 'utf8' });
|
|
40
43
|
return probe.status === 0 && Boolean(String(probe.stdout || '').trim());
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -68,6 +71,18 @@ function normalizeRoles(value, fallback = ['executor']) {
|
|
|
68
71
|
return filtered.length ? filtered : fallback;
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
function normalizeModels(value, fallback = []) {
|
|
75
|
+
const models = Array.isArray(value) ? value.map((model) => String(model || '').trim()) : [];
|
|
76
|
+
const filtered = models.filter((model, index) => model && models.indexOf(model) === index);
|
|
77
|
+
return filtered.length ? filtered : fallback;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeDuty(value, fallback = '') {
|
|
81
|
+
if (value === undefined) return fallback || '';
|
|
82
|
+
const duty = String(value || '').trim();
|
|
83
|
+
return ENGINE_DUTIES.includes(duty) ? duty : '';
|
|
84
|
+
}
|
|
85
|
+
|
|
71
86
|
function normalizeFallbackOrder(value, fallback) {
|
|
72
87
|
const order = Number(value);
|
|
73
88
|
return Number.isInteger(order) ? order : fallback;
|
|
@@ -75,22 +90,32 @@ function normalizeFallbackOrder(value, fallback) {
|
|
|
75
90
|
|
|
76
91
|
function normalizeEngineEntry(id, saved = {}) {
|
|
77
92
|
const def = RUNNER_PROFILE_DEFS[id];
|
|
78
|
-
const seed = ENGINE_SEED_META[id] || { tier: 'pro', roles: ['executor'], fallback_order: 100 };
|
|
79
|
-
const installed = binInstalled(def.bin);
|
|
93
|
+
const seed = ENGINE_SEED_META[id] || { tier: 'pro', roles: ['executor'], models: [id], fallback_order: 100 };
|
|
80
94
|
const savedHealth = saved && saved.health && typeof saved.health === 'object' ? saved.health : {};
|
|
81
95
|
const savedStatus = String(savedHealth.status || '').trim();
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
// Policy over probes: a saved health status is the routing truth, verbatim.
|
|
97
|
+
// Only an engine the registry has never seen gets one seeding probe; after
|
|
98
|
+
// that, installed-state changes flow through `atris engine doctor` (or
|
|
99
|
+
// `atris engine health`), never through the resolve path.
|
|
100
|
+
let status;
|
|
101
|
+
let installed;
|
|
102
|
+
if (ENGINE_HEALTH_STATUSES.includes(savedStatus)) {
|
|
103
|
+
status = savedStatus;
|
|
104
|
+
installed = typeof saved.installed === 'boolean' ? saved.installed : status !== 'not_installed';
|
|
105
|
+
} else {
|
|
106
|
+
installed = binInstalled(def.bin);
|
|
107
|
+
status = installed ? 'ready' : 'not_installed';
|
|
108
|
+
}
|
|
86
109
|
const health = { status };
|
|
87
|
-
if (
|
|
110
|
+
if (status !== 'ready' && savedHealth.last_failure_ts) health.last_failure_ts = String(savedHealth.last_failure_ts);
|
|
88
111
|
return {
|
|
89
112
|
id,
|
|
90
113
|
name: id,
|
|
91
114
|
bin: def.bin,
|
|
92
115
|
tier: normalizeTier(saved.tier, seed.tier),
|
|
93
116
|
roles: normalizeRoles(saved.roles, Array.from(seed.roles)),
|
|
117
|
+
models: normalizeModels(saved.models, Array.from(seed.models)),
|
|
118
|
+
duty: normalizeDuty(saved.duty, seed.duty),
|
|
94
119
|
fallback_order: normalizeFallbackOrder(saved.fallback_order, seed.fallback_order),
|
|
95
120
|
installed,
|
|
96
121
|
health,
|
|
@@ -118,6 +143,62 @@ function writeEngineRegistry(root, registry) {
|
|
|
118
143
|
fs.writeFileSync(file, `${JSON.stringify(registry, null, 2)}\n`, 'utf8');
|
|
119
144
|
}
|
|
120
145
|
|
|
146
|
+
function setEngineOverrides(name, overrides = {}, root = process.cwd()) {
|
|
147
|
+
const id = canonicalEngineName(name);
|
|
148
|
+
const knownIds = RUNNER_PROFILE_NAMES.filter((engineId) => ENGINE_SEED_META[engineId]);
|
|
149
|
+
if (!id || !knownIds.includes(id)) {
|
|
150
|
+
throw new Error(`Unknown engine "${name}". Known engines: ${knownIds.join(', ')}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const nextOverrides = {};
|
|
154
|
+
if (Object.prototype.hasOwnProperty.call(overrides, 'duty')) {
|
|
155
|
+
const duty = String(overrides.duty || '').trim();
|
|
156
|
+
if (!ENGINE_DUTIES.includes(duty)) {
|
|
157
|
+
throw new Error(`Unknown duty "${overrides.duty}". Known duties: ${ENGINE_DUTIES.join(', ')}`);
|
|
158
|
+
}
|
|
159
|
+
nextOverrides.duty = duty;
|
|
160
|
+
}
|
|
161
|
+
if (Object.prototype.hasOwnProperty.call(overrides, 'models')) {
|
|
162
|
+
const models = normalizeModels(overrides.models, []);
|
|
163
|
+
if (!models.length) throw new Error('models must include at least one name');
|
|
164
|
+
nextOverrides.models = models;
|
|
165
|
+
}
|
|
166
|
+
if (!Object.keys(nextOverrides).length) throw new Error('set requires --duty or --models');
|
|
167
|
+
|
|
168
|
+
const file = engineRegistryFile(root);
|
|
169
|
+
const raw = readRawRegistry(file);
|
|
170
|
+
const savedById = new Map();
|
|
171
|
+
for (const entry of raw.engines || []) {
|
|
172
|
+
const savedId = canonicalEngineName(entry && (entry.id || entry.name));
|
|
173
|
+
if (savedId && knownIds.includes(savedId)) savedById.set(savedId, entry);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (nextOverrides.duty === 'leader' || nextOverrides.duty === 'learning') {
|
|
177
|
+
for (const engineId of knownIds) {
|
|
178
|
+
if (engineId === id) continue;
|
|
179
|
+
const saved = savedById.get(engineId) || {};
|
|
180
|
+
const effectiveDuty = Object.prototype.hasOwnProperty.call(saved, 'duty')
|
|
181
|
+
? normalizeDuty(saved.duty, '')
|
|
182
|
+
: normalizeDuty(undefined, ENGINE_SEED_META[engineId].duty);
|
|
183
|
+
if (effectiveDuty === nextOverrides.duty) {
|
|
184
|
+
savedById.set(engineId, { ...saved, id: engineId, name: engineId, duty: '' });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const saved = savedById.get(id) || {};
|
|
190
|
+
savedById.set(id, { ...saved, id, name: id, ...nextOverrides });
|
|
191
|
+
const ordered = knownIds.map((engineId) => savedById.get(engineId)).filter(Boolean);
|
|
192
|
+
const next = {
|
|
193
|
+
...raw,
|
|
194
|
+
schema: ENGINE_REGISTRY_SCHEMA,
|
|
195
|
+
updated_at: new Date().toISOString(),
|
|
196
|
+
engines: ordered,
|
|
197
|
+
};
|
|
198
|
+
writeEngineRegistry(root, next);
|
|
199
|
+
return { id, ...nextOverrides };
|
|
200
|
+
}
|
|
201
|
+
|
|
121
202
|
function readEngineRegistry(root = process.cwd(), options = {}) {
|
|
122
203
|
const registry = seededRegistry(root);
|
|
123
204
|
if (options.persist !== false) writeEngineRegistry(root, registry);
|
|
@@ -159,6 +240,8 @@ function resolveEngineForRoleRanked(role, root = process.cwd(), options = {}) {
|
|
|
159
240
|
const ranked = rankEnginesDetailed(engines, {
|
|
160
241
|
root,
|
|
161
242
|
taskType: options.taskType || options.task_type || normalizedRole,
|
|
243
|
+
lowStakes: options.lowStakes,
|
|
244
|
+
stakes: options.stakes,
|
|
162
245
|
});
|
|
163
246
|
return {
|
|
164
247
|
engine: ranked.candidates[0] || null,
|
|
@@ -224,17 +307,55 @@ function setEngineHealth(name, status, root = process.cwd()) {
|
|
|
224
307
|
return engines.find((engine) => engine.id === id);
|
|
225
308
|
}
|
|
226
309
|
|
|
310
|
+
// Execution-stage guard. Routing hands out engines from policy without ever
|
|
311
|
+
// touching the machine, so the moment we are about to spawn one is where a
|
|
312
|
+
// missing binary has to fail loudly, in one plain sentence naming the binary.
|
|
313
|
+
function requireEngineBin(engineOrId) {
|
|
314
|
+
const id = typeof engineOrId === 'string'
|
|
315
|
+
? canonicalEngineName(engineOrId)
|
|
316
|
+
: canonicalEngineName(engineOrId && engineOrId.id);
|
|
317
|
+
const def = RUNNER_PROFILE_DEFS[id];
|
|
318
|
+
if (!def) {
|
|
319
|
+
throw new Error(`Unknown engine "${engineOrId && engineOrId.id ? engineOrId.id : engineOrId}". Known engines: ${RUNNER_PROFILE_NAMES.join(', ')}`);
|
|
320
|
+
}
|
|
321
|
+
if (!binInstalled(def.bin)) {
|
|
322
|
+
throw new Error(`${id} CLI (${def.bin}) is not installed here, so this run cannot start.`);
|
|
323
|
+
}
|
|
324
|
+
return def.bin;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// The explicit opt-in probe pass: check every engine binary on this machine,
|
|
328
|
+
// fold the result back into the policy file (a ready/not_installed flip only;
|
|
329
|
+
// credit_out and error are operator policy and survive), and report.
|
|
330
|
+
function engineDoctorReport(root = process.cwd()) {
|
|
331
|
+
const registry = readEngineRegistry(root, { persist: false });
|
|
332
|
+
const engines = registry.engines.map((engine) => {
|
|
333
|
+
const installed = binInstalled(engine.bin);
|
|
334
|
+
let health = engine.health && engine.health.status ? engine.health : { status: installed ? 'ready' : 'not_installed' };
|
|
335
|
+
if (installed && health.status === 'not_installed') health = { status: 'ready' };
|
|
336
|
+
if (!installed && health.status === 'ready') health = { status: 'not_installed' };
|
|
337
|
+
return { ...engine, installed, health };
|
|
338
|
+
});
|
|
339
|
+
const next = { ...registry, updated_at: new Date().toISOString(), engines };
|
|
340
|
+
writeEngineRegistry(root, next);
|
|
341
|
+
return engines;
|
|
342
|
+
}
|
|
343
|
+
|
|
227
344
|
module.exports = {
|
|
228
345
|
ENGINE_ROLES,
|
|
346
|
+
ENGINE_DUTIES,
|
|
229
347
|
ENGINE_HEALTH_STATUSES,
|
|
230
348
|
engineRegistryFile,
|
|
231
349
|
binInstalled,
|
|
232
350
|
canonicalEngineName,
|
|
233
351
|
readEngineRegistry,
|
|
352
|
+
requireEngineBin,
|
|
353
|
+
engineDoctorReport,
|
|
234
354
|
engineRegistryView,
|
|
235
355
|
resolveRegisteredEngine,
|
|
236
356
|
resolveEngineForRoleRanked,
|
|
237
357
|
resolveEngineForRole,
|
|
238
358
|
resolveEngineForRoleWithPreference,
|
|
359
|
+
setEngineOverrides,
|
|
239
360
|
setEngineHealth,
|
|
240
361
|
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// A check that cannot fail is not a check. `task ready --verify` runs a command
|
|
4
|
+
// and accepts the work on exit 0, which catches a command that fails but never
|
|
5
|
+
// catches one that could not have failed. The 2026-07-26 reward-ledger audit
|
|
6
|
+
// found 131 of 802 accepted proofs carrying nothing falsifiable.
|
|
7
|
+
//
|
|
8
|
+
// The probe: run the same command again in an empty directory, with none of
|
|
9
|
+
// the work present. A check anchored to this codebase fails there. One that
|
|
10
|
+
// passes anywhere - `true`, `echo done`, a check of something outside the
|
|
11
|
+
// repo - passes there too, and proves nothing about the work.
|
|
12
|
+
//
|
|
13
|
+
// Scope, stated plainly: this asks whether a check depends on the codebase at
|
|
14
|
+
// all. It does not ask whether the check exercises the change. `git diff
|
|
15
|
+
// --check` fails in an empty directory and still says nothing about whether
|
|
16
|
+
// the code works, so it clears this probe. The probe raises the floor; it is
|
|
17
|
+
// not a ceiling.
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { spawnSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
const PROBE_TIMEOUT_MS = 60_000;
|
|
25
|
+
|
|
26
|
+
// An absolute path reaches back into a real checkout no matter where the
|
|
27
|
+
// command runs, so the empty directory proves nothing about it. Say so rather
|
|
28
|
+
// than reporting a false verdict.
|
|
29
|
+
function absolutePathIn(command) {
|
|
30
|
+
const match = String(command || '').match(/(?:^|[\s"'`=(])(\/[^\s"'`)]+)/);
|
|
31
|
+
return match ? match[1] : '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function probeVerifierCanFail({ command, runner, tmpRoot } = {}) {
|
|
35
|
+
const cmd = String(command || '').trim();
|
|
36
|
+
if (!cmd) return { probed: false, canFail: null, reason: 'no command to probe' };
|
|
37
|
+
|
|
38
|
+
const absolute = absolutePathIn(cmd);
|
|
39
|
+
if (absolute) {
|
|
40
|
+
return {
|
|
41
|
+
probed: false,
|
|
42
|
+
canFail: null,
|
|
43
|
+
reason: `not probed: the command reaches an absolute path (${absolute}), so running it away from this checkout proves nothing`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const dir = fs.mkdtempSync(path.join(tmpRoot || os.tmpdir(), 'atris-falsifier-probe-'));
|
|
48
|
+
try {
|
|
49
|
+
const run = runner || spawnSync;
|
|
50
|
+
const result = run('bash', ['-lc', cmd], {
|
|
51
|
+
cwd: dir,
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
54
|
+
});
|
|
55
|
+
if (result.error) {
|
|
56
|
+
const timedOut = /ETIMEDOUT/i.test(String(result.error.code || result.error.message || ''));
|
|
57
|
+
return {
|
|
58
|
+
probed: false,
|
|
59
|
+
canFail: null,
|
|
60
|
+
reason: timedOut
|
|
61
|
+
? `not probed: the command ran past ${PROBE_TIMEOUT_MS / 1000}s with none of the work present`
|
|
62
|
+
: `not probed: the command could not start away from this checkout (${result.error.message})`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (result.status === 0) {
|
|
66
|
+
return {
|
|
67
|
+
probed: true,
|
|
68
|
+
canFail: false,
|
|
69
|
+
exit: 0,
|
|
70
|
+
reason: 'this check passes in an empty directory, with none of the work present, so passing here says nothing about the work',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
probed: true,
|
|
75
|
+
canFail: true,
|
|
76
|
+
exit: result.status,
|
|
77
|
+
reason: 'this check fails when the work is absent, so passing it means something',
|
|
78
|
+
};
|
|
79
|
+
} finally {
|
|
80
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { probeVerifierCanFail, absolutePathIn, PROBE_TIMEOUT_MS };
|
package/lib/fleet.js
CHANGED
|
@@ -24,13 +24,15 @@ const {
|
|
|
24
24
|
} = require('./brief-ledger');
|
|
25
25
|
const { RUNNER_PROFILE_DEFS, buildRunnerCommand } = require('./runner-command');
|
|
26
26
|
const { resolveDefaultVerifier } = require('./default-verifier');
|
|
27
|
-
const {
|
|
27
|
+
const { rankEnginesDetailed } = require('./router-brain');
|
|
28
28
|
const {
|
|
29
29
|
buildOneLapValidatorPrompt,
|
|
30
30
|
parseOneLapValidatorVerdict,
|
|
31
31
|
} = require('./one-lap-validator');
|
|
32
32
|
const { listWorktrees } = require('../commands/worktree');
|
|
33
33
|
const { isConductorStatusLine } = require('./conductor-artifacts');
|
|
34
|
+
const { matchLessons } = require('./lesson-preflight');
|
|
35
|
+
const { matchTaste } = require('./taste-lessons');
|
|
34
36
|
|
|
35
37
|
// Lanes a fleet may never staff on its own: the human keeps irreversible
|
|
36
38
|
// calls. Mirrors the autoland denied lanes.
|
|
@@ -94,6 +96,30 @@ function buildFleetPrompt(task, { worktreePath, yolo = false } = {}) {
|
|
|
94
96
|
'',
|
|
95
97
|
'Final report (plain text): files changed, test command + result, commit sha (or say the commit failed and why).'
|
|
96
98
|
);
|
|
99
|
+
const lessonFiles = [
|
|
100
|
+
...(Array.isArray(task.files) ? task.files : []),
|
|
101
|
+
...(Array.isArray(task.metadata && task.metadata.files) ? task.metadata.files : []),
|
|
102
|
+
...fileSurface(task),
|
|
103
|
+
];
|
|
104
|
+
const briefText = lines.join('\n');
|
|
105
|
+
const preflightRoot = worktreePath || process.cwd();
|
|
106
|
+
const lessons = matchLessons({
|
|
107
|
+
briefText,
|
|
108
|
+
files: lessonFiles,
|
|
109
|
+
root: preflightRoot,
|
|
110
|
+
});
|
|
111
|
+
if (lessons.length) {
|
|
112
|
+
lines.push('', '## lessons that apply', ...lessons.map((lesson) => `- ${lesson.text}`));
|
|
113
|
+
}
|
|
114
|
+
const requestedTasteScope = String(
|
|
115
|
+
task.scope || (task.metadata && task.metadata.scope) || task.tag || 'any'
|
|
116
|
+
).toLowerCase();
|
|
117
|
+
const taste = matchTaste({ briefText, scope: requestedTasteScope, root: preflightRoot });
|
|
118
|
+
if (taste.length) {
|
|
119
|
+
lines.push('', "## the owner's taste", ...taste.map((entry) => (
|
|
120
|
+
`- The operator's verdict is ${entry.verdict} for "${entry.subject}". The reason is: ${entry.why}`
|
|
121
|
+
)));
|
|
122
|
+
}
|
|
97
123
|
return lines.join('\n');
|
|
98
124
|
}
|
|
99
125
|
|
|
@@ -396,13 +422,18 @@ function installedFleetEngines(root) {
|
|
|
396
422
|
return normalizeInstalledEngines(roster(root).filter((e) => e.installed));
|
|
397
423
|
}
|
|
398
424
|
|
|
399
|
-
function
|
|
400
|
-
return
|
|
425
|
+
function rankFleetEnginesDetailed(engines, root = process.cwd(), options = {}) {
|
|
426
|
+
return rankEnginesDetailed(normalizeInstalledEngines(engines), {
|
|
401
427
|
root,
|
|
402
428
|
taskType: 'executor',
|
|
429
|
+
lowStakes: options.lowStakes === true,
|
|
403
430
|
});
|
|
404
431
|
}
|
|
405
432
|
|
|
433
|
+
function rankFleetEngines(engines, root = process.cwd()) {
|
|
434
|
+
return rankFleetEnginesDetailed(engines, root).candidates;
|
|
435
|
+
}
|
|
436
|
+
|
|
406
437
|
function nextInstalledFleetEngine(current, { root = process.cwd(), installedEngines = null } = {}) {
|
|
407
438
|
const engines = installedEngines ? normalizeInstalledEngines(installedEngines) : installedFleetEngines(root);
|
|
408
439
|
const currentName = String(current || '').trim();
|
|
@@ -570,27 +601,30 @@ function dispatchToEngine({ task, engine, worktreePath, root = process.cwd(), ti
|
|
|
570
601
|
// ---------------------------------------------------------------------------
|
|
571
602
|
// T2 — staffing
|
|
572
603
|
|
|
604
|
+
const {
|
|
605
|
+
taskTagTokens,
|
|
606
|
+
isDecisionHoldTag,
|
|
607
|
+
isDecisionTask,
|
|
608
|
+
DECISION_REFUSE_REASON,
|
|
609
|
+
} = require('./task-decision');
|
|
610
|
+
|
|
573
611
|
function taskTags(task) {
|
|
574
|
-
const fromTags = Array.isArray(task.tags) ? task.tags : [];
|
|
575
|
-
const fromTag = task && task.tag ? [task.tag] : [];
|
|
576
612
|
// Tags added after creation live in metadata.tags (`atris task tag`); a
|
|
577
613
|
// fleet that only read task.tags/title hashtags would ignore an owner-hold
|
|
578
614
|
// flag stamped on a live task and keep restaffing it (CLI-879).
|
|
579
|
-
|
|
580
|
-
const fromTitle = (String(task.title || '').match(/#([a-z0-9-]+)/gi) || []).map((t) => t.slice(1));
|
|
581
|
-
return [...fromTag, ...fromTags, ...fromMeta, ...fromTitle].map((t) => String(t).toLowerCase());
|
|
615
|
+
return taskTagTokens(task);
|
|
582
616
|
}
|
|
583
617
|
|
|
584
618
|
// A task flagged for a human decision is never fleet-staffable, whatever its
|
|
585
|
-
// lane. Mirrors the sweep's needs-human hold so both loops agree.
|
|
619
|
+
// lane. Mirrors the sweep's needs-human hold so both loops agree. Also honors
|
|
620
|
+
// the clearer `decision` tag so policy questions stay off autonomous lanes.
|
|
586
621
|
function isHumanHoldTag(tag) {
|
|
587
|
-
|
|
588
|
-
return normalized === 'needs-human' || normalized === 'needshuman';
|
|
622
|
+
return isDecisionHoldTag(tag);
|
|
589
623
|
}
|
|
590
624
|
|
|
591
625
|
function isSafeLane(task) {
|
|
626
|
+
if (isDecisionTask(task)) return false;
|
|
592
627
|
const tags = taskTags(task);
|
|
593
|
-
if (tags.some(isHumanHoldTag)) return false;
|
|
594
628
|
return !tags.some((t) => DENIED_TAGS.includes(t));
|
|
595
629
|
}
|
|
596
630
|
|
|
@@ -822,9 +856,12 @@ module.exports = {
|
|
|
822
856
|
isSafeLane,
|
|
823
857
|
taskTags,
|
|
824
858
|
nextInstalledFleetEngine,
|
|
859
|
+
rankFleetEnginesDetailed,
|
|
825
860
|
dispatchToEngine,
|
|
826
861
|
taskTags,
|
|
827
862
|
isHumanHoldTag,
|
|
863
|
+
isDecisionTask,
|
|
864
|
+
DECISION_REFUSE_REASON,
|
|
828
865
|
isSafeLane,
|
|
829
866
|
staffFlight,
|
|
830
867
|
assignEngines,
|
|
@@ -2105,15 +2142,24 @@ async function runFleetFlight({
|
|
|
2105
2142
|
guardCliLink = guardGlobalCliLink,
|
|
2106
2143
|
} = {}) {
|
|
2107
2144
|
const cli = ownCli || defaultOwnCli(root);
|
|
2108
|
-
|
|
2145
|
+
// Staff first, rank second: every task staffFlight returns already cleared
|
|
2146
|
+
// the safe-lane filter (not a decision row, no denied tags), so the rank
|
|
2147
|
+
// that pairs engines to those tasks is low stakes and the stretch zone
|
|
2148
|
+
// rule may trade the strongest engine for the cheapest learnable one.
|
|
2149
|
+
// Protected or denied work never reaches staffFlight's output, so it never
|
|
2150
|
+
// gets the flag; when nothing is staffed there is no task to rank for and
|
|
2151
|
+
// the flag stays off.
|
|
2152
|
+
const staffedTasks = staffFlight(readProjectionTasks(root), { slots });
|
|
2153
|
+
const rosterDetail = engines ? null : (() => {
|
|
2109
2154
|
const { roster: fullRoster } = require('../commands/engine');
|
|
2110
2155
|
const installed = fullRoster(root)
|
|
2111
2156
|
.filter((e) => e.installed && FLEET_CAPABLE.includes(e.name))
|
|
2112
2157
|
.map((e) => e.name);
|
|
2113
|
-
return
|
|
2158
|
+
return rankFleetEnginesDetailed(installed, root, { lowStakes: staffedTasks.length > 0 });
|
|
2114
2159
|
})();
|
|
2160
|
+
const roster = engines || rosterDetail.candidates;
|
|
2115
2161
|
|
|
2116
|
-
const staffed = assignEngines(
|
|
2162
|
+
const staffed = assignEngines(staffedTasks, roster);
|
|
2117
2163
|
// The receipt path is decided BEFORE landings so each task's ready-proof
|
|
2118
2164
|
// can cite it — the proof policy certifies receipt-backed proofs agent-side.
|
|
2119
2165
|
const receiptPath = path.join(root, 'atris', 'runs', `fleet-${nowStamp()}.json`);
|
|
@@ -2123,10 +2169,14 @@ async function runFleetFlight({
|
|
|
2123
2169
|
slots,
|
|
2124
2170
|
roster,
|
|
2125
2171
|
staffed: staffed.map((s) => ({ task: s.task.display_id, title: String(s.task.title || '').slice(0, 140), engine: s.engine, surface: s.surface })),
|
|
2172
|
+
stretch_zone_pick: rosterDetail && rosterDetail.stretch_zone_pick ? rosterDetail.stretch_zone_pick : null,
|
|
2126
2173
|
results: [],
|
|
2127
2174
|
landed: [],
|
|
2128
2175
|
paused: [],
|
|
2129
2176
|
};
|
|
2177
|
+
if (flight.stretch_zone_pick) {
|
|
2178
|
+
flight.stretch_zone_note = `low-stakes staffing picked ${flight.stretch_zone_pick} from the stretch zone: the cheapest engine with a learnable track record won this lane.`;
|
|
2179
|
+
}
|
|
2130
2180
|
|
|
2131
2181
|
log('');
|
|
2132
2182
|
log(` fleet — ${roster.length} engine${roster.length === 1 ? '' : 's'} ready, ${staffed.length} task${staffed.length === 1 ? '' : 's'} staffed`);
|
package/lib/git-spawn.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
// One synchronous git runner for commands that shell out to git.
|
|
6
|
+
// check: true throws on a non-zero exit; check: false hands back the raw result.
|
|
7
|
+
function runGit(args, { cwd = process.cwd(), check = true, maxBuffer } = {}) {
|
|
8
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8', ...(maxBuffer ? { maxBuffer } : {}) });
|
|
9
|
+
if (check && result.status !== 0) {
|
|
10
|
+
throw new Error(`git ${args.join(' ')} failed: ${(result.stderr || result.stdout || '').trim()}`);
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { runGit };
|
package/lib/json-file.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Shared JSON file read/write so every call site fails the same way.
|
|
2
|
+
//
|
|
3
|
+
// Bare `JSON.parse(fs.readFileSync(...))` drifted across commands: some sites
|
|
4
|
+
// caught, some returned null, some returned {}. These two helpers keep the
|
|
5
|
+
// tolerant shape in one place.
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Read and parse a JSON file, returning `fallback` when it is missing or corrupt.
|
|
12
|
+
* @param {string} filePath
|
|
13
|
+
* @param {*} [fallback=null]
|
|
14
|
+
* @returns {*}
|
|
15
|
+
*/
|
|
16
|
+
function readJson(filePath, fallback = null) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Write a value as pretty JSON with a trailing newline, creating parent dirs.
|
|
26
|
+
* @param {string} filePath
|
|
27
|
+
* @param {*} value
|
|
28
|
+
* @param {{ indent?: number }} [options]
|
|
29
|
+
* @returns {string} the path written
|
|
30
|
+
*/
|
|
31
|
+
function writeJson(filePath, value, { indent = 2 } = {}) {
|
|
32
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
33
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, indent) + '\n', 'utf8');
|
|
34
|
+
return filePath;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { readJson, writeJson };
|
package/lib/known-commands.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const knownCommands = ['init', 'log', 'logs', 'wish', 'drill', 'dream', 'now', 'goal', 'wtf', 'orb', 'radar', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', '_start', 'plan', 'do', 'review', 'release',
|
|
4
4
|
'activate', '_activate', 'agent', 'team', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
5
|
-
'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'teach', 'plugin', 'experiments', 'bench', 'router', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube',
|
|
6
|
-
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'agents', 'probe', 'worktree', 'land', 'autoland', 'drive', 'aeo', 'slop', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'sync-checkout',
|
|
5
|
+
'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'taste', 'teach', 'plugin', 'experiments', 'bench', 'router', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube',
|
|
6
|
+
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'agents', 'probe', 'worktree', 'land', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'sync-checkout',
|
|
7
7
|
'github', 'vercel', 'supabase', 'linear', 'stripe', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
8
8
|
'fork', 'browse', 'publish', 'pack', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'fleet-report', 'loops', 'self-improve', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines', 'feed', 'brief'];
|
|
9
9
|
|