@feigi/fleet-ctl 3.17.0 → 3.17.2
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/package.json +1 -1
- package/scripts/tier-roles.mjs +37 -11
- package/scripts/tier-roles.test.mjs +55 -1
package/package.json
CHANGED
package/scripts/tier-roles.mjs
CHANGED
|
@@ -31,7 +31,10 @@
|
|
|
31
31
|
// flagged as a notice — legal, but every `slow-*` cell then measures
|
|
32
32
|
// the same model as its `task-*` twin;
|
|
33
33
|
// (f) the agents directory holds at least one `*.agent.md` definition —
|
|
34
|
-
// with none, (a), (b) and (d) have nothing to check
|
|
34
|
+
// with none, (a), (b) and (d) have nothing to check; an agents
|
|
35
|
+
// directory that cannot be read (missing, not a directory) or a
|
|
36
|
+
// definition that cannot be read (a directory named `*.agent.md`)
|
|
37
|
+
// fails too.
|
|
35
38
|
|
|
36
39
|
import { readFileSync, readdirSync } from "node:fs";
|
|
37
40
|
import { execFileSync } from "node:child_process";
|
|
@@ -202,19 +205,35 @@ function kindOf(v) {
|
|
|
202
205
|
// definitions need. `violations` stop a run (ADR 0014); `notices` never do —
|
|
203
206
|
// they flag a hazard (two roles' cells measuring one model) that is legal
|
|
204
207
|
// configuration, just probably not what the operator meant.
|
|
205
|
-
export function checkRoutes({ agentsDir, modelRoles, overrides, catalog }) {
|
|
208
|
+
export function checkRoutes({ agentsDir, agentsSource = `--agents ${agentsDir}`, modelRoles, overrides, catalog }) {
|
|
206
209
|
const violations = [];
|
|
207
210
|
const notices = [];
|
|
208
211
|
const usedBy = {}; // role -> [{ file, level }, ...]
|
|
209
212
|
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
// A path `readdirSync`/`readFileSync` cannot read (missing, a regular file,
|
|
214
|
+
// a directory named `*.agent.md`, no permission) is a wrong agents
|
|
215
|
+
// directory, not a crash: name the path and where it came from
|
|
216
|
+
// (`agentsSource` — `--agents <dir>`, or the built-in default).
|
|
217
|
+
const unreadable = (what, e) => `${agentsSource}: cannot read ${what} (${e.code ?? e.message})`;
|
|
218
|
+
let files = [];
|
|
219
|
+
try {
|
|
220
|
+
files = readdirSync(agentsDir).filter((f) => f.endsWith(".agent.md")).sort();
|
|
221
|
+
// A directory with no definitions has nothing to route, so every
|
|
222
|
+
// per-definition check below (the model route, the role's resolution, the
|
|
223
|
+
// level) would pass over it — a wrong `--agents` path that exists must not
|
|
224
|
+
// read as a healthy install.
|
|
225
|
+
if (files.length === 0) violations.push(`no *.agent.md definition found in ${agentsDir}`);
|
|
226
|
+
} catch (e) {
|
|
227
|
+
violations.push(unreadable("the agents directory", e));
|
|
228
|
+
}
|
|
216
229
|
for (const file of files) {
|
|
217
|
-
|
|
230
|
+
let text;
|
|
231
|
+
try {
|
|
232
|
+
text = readFileSync(join(agentsDir, file), "utf8");
|
|
233
|
+
} catch (e) {
|
|
234
|
+
violations.push(unreadable(file, e));
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
218
237
|
const fm = parseFrontmatter(text);
|
|
219
238
|
if (!fm.role || !fm.level) {
|
|
220
239
|
violations.push(`${file}: model ${JSON.stringify(fm.model)} is not @<role>:<level>`);
|
|
@@ -317,13 +336,20 @@ function main() {
|
|
|
317
336
|
const check = has("check");
|
|
318
337
|
if (!check) die("--check is required (usage: tier-roles.mjs --check [--agents <dir>] [--model-roles <path>] [--overrides <path>] [--catalog <path>])");
|
|
319
338
|
|
|
320
|
-
const
|
|
339
|
+
const agentsFlag = arg("agents");
|
|
340
|
+
const agentsDir = agentsFlag ?? join(SCRIPT_DIR, "..", "agents");
|
|
321
341
|
const config = (key) => [() => readOmpConfigValue(key), `omp config get ${key} --json`];
|
|
322
342
|
const overrides = loadJsonObject(arg("overrides"), ...config("task.agentModelOverrides"), "overrides");
|
|
323
343
|
const modelRoles = loadJsonObject(arg("model-roles"), ...config("modelRoles"), "model-roles");
|
|
324
344
|
const catalog = loadJsonObject(arg("catalog"), readOmpModelCatalog, "omp models --json", "catalog");
|
|
325
345
|
|
|
326
|
-
const { violations, notices } = checkRoutes({
|
|
346
|
+
const { violations, notices } = checkRoutes({
|
|
347
|
+
agentsDir,
|
|
348
|
+
agentsSource: agentsFlag === null ? `the default agents directory ${agentsDir}` : undefined,
|
|
349
|
+
modelRoles,
|
|
350
|
+
overrides,
|
|
351
|
+
catalog,
|
|
352
|
+
});
|
|
327
353
|
|
|
328
354
|
if (violations.length === 0) {
|
|
329
355
|
console.log("tier-roles: every definition routes to a resolvable model at a level it runs, no shadowing override");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { chmodSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { chmodSync, copyFileSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { tempDir } from "./temp-dir.mjs";
|
|
6
6
|
import { delimiter, join } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "./tier-roles.mjs";
|
|
13
13
|
|
|
14
14
|
const SCRIPT = fileURLToPath(new URL("./tier-roles.mjs", import.meta.url));
|
|
15
|
+
const SCRIPTS_DIR = fileURLToPath(new URL(".", import.meta.url));
|
|
15
16
|
const REPO_AGENTS = fileURLToPath(new URL("../agents", import.meta.url));
|
|
16
17
|
|
|
17
18
|
function dir() {
|
|
@@ -211,6 +212,32 @@ test("checkRoutes: an empty agents directory still reports the other violations
|
|
|
211
212
|
assert.match(violations.join("\n"), /task\.agentModelOverrides\.fleet-a shadows/);
|
|
212
213
|
});
|
|
213
214
|
|
|
215
|
+
test("checkRoutes: an --agents path that is missing, or a regular file, is a violation naming the flag and path, not a crash", () => {
|
|
216
|
+
const d = dir();
|
|
217
|
+
const missing = join(d, "nope");
|
|
218
|
+
const file = join(d, "plain.txt");
|
|
219
|
+
writeFileSync(file, "x\n");
|
|
220
|
+
for (const [path, code] of [[missing, "ENOENT"], [file, "ENOTDIR"]]) {
|
|
221
|
+
const { violations } = checkRoutes({ agentsDir: path, modelRoles: MODEL_ROLES, overrides: {}, catalog: CATALOG });
|
|
222
|
+
assert.deepEqual(violations, [`--agents ${path}: cannot read the agents directory (${code})`]);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("checkRoutes: a directory named *.agent.md is a violation naming the entry, and the readable definitions after it are still checked", () => {
|
|
227
|
+
const agents = agentsDir({ "fleet-a": "@slow:xhigh", "fleet-b": "@smol:max" });
|
|
228
|
+
mkdirSync(join(agents, "a.agent.md"));
|
|
229
|
+
const { violations } = checkRoutes({ agentsDir: agents, modelRoles: MODEL_ROLES, overrides: {}, catalog: CATALOG });
|
|
230
|
+
assert.equal(violations.length, 2);
|
|
231
|
+
assert.equal(violations[0], `--agents ${agents}: cannot read a.agent.md (EISDIR)`);
|
|
232
|
+
assert.match(violations[1], /^fleet-b\.agent\.md: level max is not one modelRoles\.smol's target/);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("checkRoutes: an unreadable agents directory names the source the caller gives, not --agents", () => {
|
|
236
|
+
const missing = join(dir(), "nope");
|
|
237
|
+
const { violations } = checkRoutes({ agentsDir: missing, agentsSource: "the default agents directory X", modelRoles: MODEL_ROLES, overrides: {}, catalog: CATALOG });
|
|
238
|
+
assert.deepEqual(violations, ["the default agents directory X: cannot read the agents directory (ENOENT)"]);
|
|
239
|
+
});
|
|
240
|
+
|
|
214
241
|
test("checkRoutes (a): a definition whose model: is not a route is a violation naming the file", () => {
|
|
215
242
|
const { violations } = routes({ "fleet-a": "opus" });
|
|
216
243
|
assert.equal(violations.length, 1);
|
|
@@ -358,6 +385,33 @@ test("CLI: --check on an agents directory with no definitions exits 1 naming the
|
|
|
358
385
|
assert.doesNotMatch(r.stdout, /every definition routes/);
|
|
359
386
|
});
|
|
360
387
|
|
|
388
|
+
test("CLI: --check on an --agents path that is missing, a regular file, or holds a directory named *.agent.md exits 1 with a refusal naming the cause, no stack trace", () => {
|
|
389
|
+
const d = dir();
|
|
390
|
+
const file = join(d, "plain.txt");
|
|
391
|
+
writeFileSync(file, "x\n");
|
|
392
|
+
const trap = agentsDir({ "fleet-a": "@slow:xhigh" });
|
|
393
|
+
mkdirSync(join(trap, "x.agent.md"));
|
|
394
|
+
for (const [path, code] of [[join(d, "nope"), "ENOENT"], [file, "ENOTDIR"], [trap, "EISDIR"]]) {
|
|
395
|
+
const r = runCli(["--check", "--agents", path, ...installFlags(d)], d);
|
|
396
|
+
assert.equal(r.status, 1, path);
|
|
397
|
+
assert.match(r.stderr, new RegExp(`^tier-roles: --agents ${path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}: cannot read .*\\(${code}\\)`), path);
|
|
398
|
+
assert.doesNotMatch(r.stderr, /\n\s+at /, path);
|
|
399
|
+
assert.doesNotMatch(r.stdout, /every definition routes/);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("CLI: --check with no --agents and an unreadable built-in agents directory refuses without naming a flag it was not given", () => {
|
|
404
|
+
const d = dir();
|
|
405
|
+
const scripts = join(d, "scripts");
|
|
406
|
+
mkdirSync(scripts);
|
|
407
|
+
for (const f of ["tier-roles.mjs", "arg.mjs", "is-cli.mjs"]) copyFileSync(join(SCRIPTS_DIR, f), join(scripts, f));
|
|
408
|
+
const r = spawnSync(process.execPath, [join(scripts, "tier-roles.mjs"), "--check", ...installFlags(d)], { cwd: d, encoding: "utf8" });
|
|
409
|
+
assert.equal(r.status, 1, r.stderr);
|
|
410
|
+
assert.match(r.stderr, /^tier-roles: the default agents directory .*agents: cannot read the agents directory \(ENOENT\)/);
|
|
411
|
+
assert.doesNotMatch(r.stderr, /--agents/);
|
|
412
|
+
assert.doesNotMatch(r.stderr, /\n\s+at /);
|
|
413
|
+
});
|
|
414
|
+
|
|
361
415
|
test("CLI: --check with a non-route model: exits 1 naming the file", () => {
|
|
362
416
|
const d = dir();
|
|
363
417
|
const agents = agentsDir({ "fleet-a": "opus" });
|