agent-lyceum 0.26.0 → 0.28.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/dist/status.js CHANGED
@@ -319,7 +319,14 @@ export function buildStatusReport(base, runId, now = Date.now()) {
319
319
  catch {
320
320
  /* a hint only: never let it break status */
321
321
  }
322
- return {
322
+ const wakes = run?.state.wakes ?? [];
323
+ const below = new Set();
324
+ for (const a of Object.values(project.agents)) {
325
+ const last = wakes.filter((w) => w.agent === a.name).at(-1);
326
+ if (last?.tier && !last.fallback_from && last.runtime === a.runtime && !candidatesFor(a).some((c) => c.primary && c.key === `${last.runtime}/${(last.model ?? "").trim().toLowerCase()}`))
327
+ below.add(a.name);
328
+ }
329
+ const out = {
323
330
  schema_version: 1,
324
331
  project: projectInfo(base),
325
332
  agents: Object.values(project.agents).map((a) => ({
@@ -335,6 +342,8 @@ export function buildStatusReport(base, runId, now = Date.now()) {
335
342
  run: report,
336
343
  ...(advice.length ? { memory_advice: advice } : {}),
337
344
  };
345
+ belowCeiling.set(out, below);
346
+ return out;
338
347
  }
339
348
  /** Every run of the project, newest first. */
340
349
  export function buildTaskListReport(project) {
@@ -513,6 +522,19 @@ function fallbackLines(agents, c) {
513
522
  }
514
523
  return out;
515
524
  }
525
+ /** Agents whose latest wake ran on a tiered model below the ceiling; kept off the report so `status --json` is unchanged. */
526
+ const belowCeiling = new WeakMap();
527
+ /** An agent whose latest wake ran on a model below its ceiling (spec 24.9): one line each, in agent order. */
528
+ function tierLines(report, c) {
529
+ const out = [];
530
+ const wakes = report.run?.wakes ?? [];
531
+ for (const a of report.agents) {
532
+ const last = wakes.filter((w) => w.agent === a.name).at(-1);
533
+ if (last?.tier && last.runtime && belowCeiling.get(report)?.has(a.name))
534
+ out.push(` ${c.cyan(a.name)} ${c.yellow(`tier ${last.tier}`)}(${modelName(`${last.runtime}/${last.model ?? ""}`)})`);
535
+ }
536
+ return out;
537
+ }
516
538
  export function formatStatusReport(report, now = Date.now(), color = false) {
517
539
  const c = paint(color);
518
540
  const run = report.run;
@@ -543,6 +565,9 @@ export function formatStatusReport(report, now = Date.now(), color = false) {
543
565
  const fb = fallbackLines(report.agents, c);
544
566
  if (fb.length)
545
567
  lines.push("", ...fb);
568
+ const tl = tierLines(report, c);
569
+ if (tl.length)
570
+ lines.push(...(fb.length ? [] : [""]), ...tl);
546
571
  if (run)
547
572
  lines.push("", `輸出 tokens:${tokens(run.output_tokens)}${cacheText(run.cache_read_tokens, run.cache_write_tokens) ? ` ${cacheText(run.cache_read_tokens, run.cache_write_tokens)}` : ""}`);
548
573
  if (report.memory_advice?.length)
@@ -0,0 +1,27 @@
1
+ import { type Tier } from "./schema.js";
2
+ export declare function isTier(v: unknown): v is Tier;
3
+ /** Index of `model` on `ladder` (trim + lowercase, like the fallback keys), or -1. */
4
+ export declare function ladderIndex(ladder: readonly string[], model: string | undefined): number;
5
+ /**
6
+ * The tiers an agent can run on: the ladder from its lowest entry up to and including `model` (the ceiling).
7
+ * `undefined` when there is nothing below the ceiling (no ladder, no model, model not on it, or it is the lowest entry).
8
+ */
9
+ export declare function availableTiers(ladder: readonly string[] | undefined, model: string | undefined): string[] | undefined;
10
+ /** `low` = lowest, `high` = ceiling, `mid` = `tiers[floor(n/2)]` (the higher middle when n is even). */
11
+ export declare function modelForTier(tiers: readonly string[], tier: Tier): string;
12
+ export type TierFlag = "absent" | "invalid" | Tier | {
13
+ ignored: "not-lead" | "not-a-task" | "no-tiers";
14
+ };
15
+ /** Judge the `tier` frontmatter of one outgoing mail, in the order of spec 24.5.2. Not wired anywhere yet. */
16
+ export declare function tierFlag(i: {
17
+ value: unknown;
18
+ senderIsLead: boolean;
19
+ type: string;
20
+ recipientTiers?: string[];
21
+ }): TierFlag;
22
+ /** A mail without a valid tier counts as `high` (the ceiling). */
23
+ export declare function effectiveTier(meta: {
24
+ tier?: unknown;
25
+ }): Tier;
26
+ /** The tier a wake runs at: the highest of the batch when every mail has a valid one, else `undefined` (the ceiling). */
27
+ export declare function wakeTier(tiers: readonly unknown[]): Tier | undefined;
package/dist/tiers.js ADDED
@@ -0,0 +1,49 @@
1
+ import { TIERS } from "./schema.js";
2
+ const norm = (m) => (m ?? "").trim().toLowerCase();
3
+ export function isTier(v) {
4
+ return typeof v === "string" && TIERS.includes(v);
5
+ }
6
+ /** Index of `model` on `ladder` (trim + lowercase, like the fallback keys), or -1. */
7
+ export function ladderIndex(ladder, model) {
8
+ const m = norm(model);
9
+ return m ? ladder.findIndex((e) => norm(e) === m) : -1;
10
+ }
11
+ /**
12
+ * The tiers an agent can run on: the ladder from its lowest entry up to and including `model` (the ceiling).
13
+ * `undefined` when there is nothing below the ceiling (no ladder, no model, model not on it, or it is the lowest entry).
14
+ */
15
+ export function availableTiers(ladder, model) {
16
+ if (!ladder?.length)
17
+ return undefined;
18
+ const i = ladderIndex(ladder, model);
19
+ return i >= 1 ? ladder.slice(0, i + 1) : undefined;
20
+ }
21
+ /** `low` = lowest, `high` = ceiling, `mid` = `tiers[floor(n/2)]` (the higher middle when n is even). */
22
+ export function modelForTier(tiers, tier) {
23
+ const n = tiers.length;
24
+ return tiers[tier === "low" ? 0 : tier === "high" ? n - 1 : Math.floor(n / 2)];
25
+ }
26
+ /** Judge the `tier` frontmatter of one outgoing mail, in the order of spec 24.5.2. Not wired anywhere yet. */
27
+ export function tierFlag(i) {
28
+ if (i.value === undefined || i.value === null || i.value === "")
29
+ return "absent";
30
+ if (!i.senderIsLead)
31
+ return { ignored: "not-lead" };
32
+ if (!isTier(i.value))
33
+ return "invalid";
34
+ if (i.type !== "task")
35
+ return { ignored: "not-a-task" };
36
+ if (!i.recipientTiers || i.recipientTiers.length < 2)
37
+ return { ignored: "no-tiers" };
38
+ return i.value;
39
+ }
40
+ /** A mail without a valid tier counts as `high` (the ceiling). */
41
+ export function effectiveTier(meta) {
42
+ return isTier(meta.tier) ? meta.tier : "high";
43
+ }
44
+ /** The tier a wake runs at: the highest of the batch when every mail has a valid one, else `undefined` (the ceiling). */
45
+ export function wakeTier(tiers) {
46
+ if (tiers.length === 0 || !tiers.every(isTier))
47
+ return undefined;
48
+ return TIERS[Math.max(...tiers.map((t) => TIERS.indexOf(t)))];
49
+ }
package/dist/validate.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  import { enforcementFor, memoryDirs } from "./policy.js";
3
+ import { describeConflict, fallbackCapExcess, sandboxConflictsOf } from "./sandbox-invocation.js";
3
4
  import { isInside } from "./paths.js";
4
- import { inferRuntime, RUNTIME_EFFORTS } from "./schema.js";
5
+ import { inferRuntime, RUNTIME_EFFORTS, RUNTIMES } from "./schema.js";
6
+ import { ladderIndex } from "./tiers.js";
5
7
  import { must } from "./assert.js";
6
8
  export function validateProject(project) {
7
9
  const issues = [];
@@ -15,7 +17,30 @@ export function validateProject(project) {
15
17
  if (!fs.existsSync(project.dir) || !fs.statSync(project.dir).isDirectory()) {
16
18
  err(`Project dir does not exist: ${project.dir}`);
17
19
  }
20
+ for (const rt of RUNTIMES) {
21
+ const ladder = project.modelTiers[rt];
22
+ if (!ladder)
23
+ continue;
24
+ const seen = new Set();
25
+ ladder.forEach((m, i) => {
26
+ const other = inferRuntime(m);
27
+ if (other !== rt)
28
+ err(`model_tiers.${rt}[${i}]: "${m}" is not a ${rt} model (recognised as ${other ?? "nothing"}). List only ${rt} models, lowest first.`);
29
+ const k = m.trim().toLowerCase();
30
+ if (seen.has(k))
31
+ err(`model_tiers.${rt}: "${m}" is listed twice.`);
32
+ seen.add(k);
33
+ });
34
+ }
18
35
  for (const a of Object.values(project.agents)) {
36
+ const ladder = a.runtime ? project.modelTiers[a.runtime] : undefined;
37
+ if (a.runtime && ladder) {
38
+ if (!a.model)
39
+ warn(`Agent "${a.name}" has no model, so model_tiers.${a.runtime} does not apply to it: it always runs on its runtime's default model.`);
40
+ else if (ladderIndex(ladder, a.model) < 0) {
41
+ err(`Agent "${a.name}": model "${a.model}" is not on model_tiers.${a.runtime} (${ladder.join(", ")}). Add it to the ladder, or set model_tiers.${a.runtime}: [] in project.yaml to turn the ladder off for this project.`);
42
+ }
43
+ }
19
44
  if (!a.runtime)
20
45
  err(`Agent "${a.name}" has no runtime (set runtime, or a recognizable model such as opus/sonnet/haiku/claude-* or gpt-*/codex-*).`);
21
46
  const modelRuntime = inferRuntime(a.model);
@@ -26,6 +51,10 @@ export function validateProject(project) {
26
51
  err(`Agent "${a.name}": effort "${a.effort}" is not supported by ${a.runtime} (use ${RUNTIME_EFFORTS[a.runtime].join(", ")}).`);
27
52
  }
28
53
  validateFallback(project, a, err);
54
+ for (const c of sandboxConflictsOf(project, a, false))
55
+ err(describeConflict(a.name, c, "load"));
56
+ for (const f of fallbackCapExcess(a))
57
+ err(`agent ${a.name}: fallback ${f.label} would apply more sandbox allowance than the primary (${f.excess.join("; ")}).`);
29
58
  if (!fs.existsSync(a.agentMd))
30
59
  err(`Agent "${a.name}": AGENT.md not found at ${a.agentMd}`);
31
60
  if (!a.memory.global && !a.memory.project)
@@ -39,6 +68,9 @@ export function validateProject(project) {
39
68
  }
40
69
  }
41
70
  }
71
+ if (Object.keys(project.modelTiers).length > 0 && !Object.values(project.agents).some((a) => a.name !== project.lead && a.tiers)) {
72
+ warn("model_tiers is set, but no member can run below its model (each member's model is the lowest entry of its ladder): tiers have no effect.");
73
+ }
42
74
  // Memory directories must not overlap between agents (or equal each other).
43
75
  const mem = [];
44
76
  for (const a of Object.values(project.agents))
package/dist/worktree.js CHANGED
@@ -4,6 +4,7 @@ import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { atomicWrite } from "./fs-util.js";
7
+ import { escapesParent } from "./paths.js";
7
8
  const IDENTITY = {
8
9
  GIT_AUTHOR_NAME: "agent-lyceum",
9
10
  GIT_AUTHOR_EMAIL: "agent-lyceum@localhost",
@@ -474,7 +475,7 @@ export function collectAgentChanges(workspace) {
474
475
  const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(p), target));
475
476
  if (path.posix.isAbsolute(target))
476
477
  violations.push(`${f.path}: symlink to an absolute path (${target})`);
477
- else if (resolved.startsWith(".."))
478
+ else if (escapesParent(resolved, "/"))
478
479
  violations.push(`${f.path}: symlink leaves the project directory (${target})`);
479
480
  }
480
481
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-lyceum",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "Config + dispatcher for teams of Claude Code / Codex agents with scoped context and file-based mailboxes",
5
5
  "keywords": [
6
6
  "ai",