agent-lyceum 0.25.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,7 +40,7 @@ Latest unreleased code: `npx github:gogogohuang/agent-lyceum <command>`.
40
40
  3. Agents reply by writing mail to their own `outbox/`; the dispatcher checks `can_message` and delivers it.
41
41
  4. The run ends when the lead sends `done`.
42
42
 
43
- Its own data (config, mailboxes, logs, memory, worktrees) lives in `~/agent-lyceum-config/`, not in your repo. Concurrency needs no configuration. In a git repo with a commit, every run works in its own git worktree and ends on its own branch (`agent-lyceum/<run>/…`), members with pending mail wake in parallel, and any number of runs can be active at once; the result is not written into your working tree, so use `git merge <branch>` or `open_pr`, and note that a run starts from `HEAD` and does not see your uncommitted edits. In a non-git folder, a repo with no commit, or a repo using reftable, the run works directly in the folder, one agent at a time (an info line says why), and only one such run can be active. agent-lyceum never merges. Upgrading from a version with `owns`, `max_parallel`, `max_concurrent_runs` or `workspace_mode`: see [Upgrading](docs/upgrading-run-v2.md). More: [How a run works](docs/how-it-works.md), [Configuration](docs/configuration.md).
43
+ Its own data (config, mailboxes, logs, memory, worktrees) lives in `~/agent-lyceum-config/`, not in your repo. Concurrency needs no configuration. In a git repo with a commit, every run works in its own git worktree and ends on its own branch (`agent-lyceum/<run>/…`), members with pending mail wake in parallel, and any number of runs can be active at once; the result is not written into your working tree, so use `git merge <branch>` or `open_pr`, and note that a run starts from `HEAD` and does not see your uncommitted edits. In a non-git folder, a repo with no commit, or a repo using reftable, the run works directly in the folder, one agent at a time (an info line says why), and only one such run can be active. A lead can mark independent tasks `parallel: true` so one member works on several at once, each in its own worktree (worktree mode only, the lead decides, at most `dispatcher.max_instances` per member, default 4); see [How a run works](docs/how-it-works.md#parallel-instances-of-one-member). agent-lyceum never merges. Upgrading from a version with `owns`, `max_parallel`, `max_concurrent_runs` or `workspace_mode`: see [Upgrading](docs/upgrading-run-v2.md). More: [How a run works](docs/how-it-works.md), [Configuration](docs/configuration.md).
44
44
 
45
45
  ## Commands
46
46
 
@@ -82,6 +82,6 @@ Folder layout, every agent field and the rules `doctor` checks: [Configuration](
82
82
  | Run lifecycle, parallel agents, several runs at once, follow-ups, opening a PR | [How a run works](docs/how-it-works.md) |
83
83
  | What agents may write, sandbox levels, known gaps | [Write scope](docs/write-scope.md) |
84
84
  | Letting an agent search or fetch the web | [Web access](docs/web-access.md) |
85
- | Live run status inside Claude Code | [Monitor mod](docs/monitor.md) |
85
+ | Live run status inside Claude Code (a framed, state-coloured tree of agents; pane or full-width band) | [Monitor mod](docs/monitor.md) |
86
86
  | All commands in full | [Commands](docs/commands.md) |
87
87
  | Coming from an older version | [Upgrading to run v2](docs/upgrading-run-v2.md) |
package/README.zh-TW.md CHANGED
@@ -40,7 +40,7 @@ npx agent-lyceum status
40
40
  3. agent 把回信寫進自己的 `outbox/`,dispatcher 檢查 `can_message` 後投遞。
41
41
  4. lead 寄出 `done` 時,run 結束。
42
42
 
43
- 工具自己的資料(設定、信箱、log、記憶、worktree)放在 `~/agent-lyceum-config/`,不放進你的 repo。並行不需要任何設定。在有 commit 的 git repo 裡,每個 run 都在自己的 git worktree 工作,並結束在自己的 branch(`agent-lyceum/<run>/…`);有待處理信件的成員會平行喚醒,同時也可有任意多個 run;結果不會寫進你的工作目錄,請用 `git merge <branch>` 或 `open_pr`,且 run 從 `HEAD` 起跑,看不到你未提交的修改。在非 git 資料夾、沒有 commit 的 repo 或使用 reftable 的 repo,run 直接在該資料夾工作、一次只跑一個 agent(會有一行說明原因),且同時只能有一個這樣的 run。agent-lyceum 不會幫你合併。從有 `owns`、`max_parallel`、`max_concurrent_runs` 或 `workspace_mode` 的版本升級:見[升級](docs/upgrading-run-v2.md)。詳見[一次執行如何運作](docs/how-it-works.zh-TW.md)、[設定](docs/configuration.zh-TW.md)。
43
+ 工具自己的資料(設定、信箱、log、記憶、worktree)放在 `~/agent-lyceum-config/`,不放進你的 repo。並行不需要任何設定。在有 commit 的 git repo 裡,每個 run 都在自己的 git worktree 工作,並結束在自己的 branch(`agent-lyceum/<run>/…`);有待處理信件的成員會平行喚醒,同時也可有任意多個 run;結果不會寫進你的工作目錄,請用 `git merge <branch>` 或 `open_pr`,且 run 從 `HEAD` 起跑,看不到你未提交的修改。在非 git 資料夾、沒有 commit 的 repo 或使用 reftable 的 repo,run 直接在該資料夾工作、一次只跑一個 agent(會有一行說明原因),且同時只能有一個這樣的 run。lead 可以把互不相依的 task 標成 `parallel: true`,讓一位成員同時處理多件,各在自己的 worktree(僅 worktree 模式、由 lead 決定、每位成員最多 `dispatcher.max_instances` 個,預設 4);見[一次執行如何運作](docs/how-it-works.zh-TW.md#同一成員的平行實例)。agent-lyceum 不會幫你合併。從有 `owns`、`max_parallel`、`max_concurrent_runs` 或 `workspace_mode` 的版本升級:見[升級](docs/upgrading-run-v2.md)。詳見[一次執行如何運作](docs/how-it-works.zh-TW.md)、[設定](docs/configuration.zh-TW.md)。
44
44
 
45
45
  ## 指令
46
46
 
@@ -82,6 +82,6 @@ agents:
82
82
  | run 的生命週期、平行 agent、同時多個 run、追加需求、開 PR | [一次執行如何運作](docs/how-it-works.zh-TW.md) |
83
83
  | agent 能寫什麼、沙箱等級、已知缺口 | [寫入範圍](docs/write-scope.zh-TW.md) |
84
84
  | 讓 agent 搜尋或讀取網頁 | [網路存取](docs/web-access.zh-TW.md) |
85
- | 在 Claude Code 內看即時 run 狀態 | [監控 mod](docs/monitor.zh-TW.md) |
85
+ | 在 Claude Code 內看即時 run 狀態(每個 agent 一個依狀態上色的外框,樹狀排列;面板或整寬橫幅) | [監控 mod](docs/monitor.zh-TW.md) |
86
86
  | 所有指令的完整說明 | [指令](docs/commands.zh-TW.md) |
87
87
  | 從舊版升級 | [升級到 run v2](docs/upgrading-run-v2.md) |
package/dist/config.d.ts CHANGED
@@ -33,6 +33,16 @@ export interface ResolvedAgent {
33
33
  }[];
34
34
  /** Where each field's value came from (for `validate` output). */
35
35
  sources: Record<string, Source>;
36
+ /** Set when the run bound this agent as an instance of a role that has several (spec 23): which role, and the instance number. */
37
+ instance?: {
38
+ role: string;
39
+ k: number;
40
+ };
41
+ /** Instances 2 and up: the role's global/project memory, which only instance 1 may write. */
42
+ sharedMemory?: {
43
+ global?: string;
44
+ project?: string;
45
+ };
36
46
  }
37
47
  export interface ResolvedProject {
38
48
  home: string;
@@ -52,7 +62,10 @@ export interface ResolvedProject {
52
62
  run?: {
53
63
  id: string;
54
64
  dir: string;
55
- layout: "run" | "legacy";
65
+ layout: "run" | "legacy"; /** Set by the run when same-role instances are possible: the lead's prompt then explains `parallel: true`. */
66
+ instances?: {
67
+ max: number;
68
+ };
56
69
  };
57
70
  /** Agents working in their own git worktree instead of `dir` (set by the dispatcher for the wake-ups in progress). */
58
71
  workspaces?: Record<string, AgentWorkspace>;
@@ -102,6 +115,7 @@ export declare function loadProjectRaw(home: string, name: string): {
102
115
  effort?: "high" | "low" | "max" | "medium" | "minimal" | "xhigh" | undefined;
103
116
  }[] | undefined;
104
117
  fallback_cooldown_sec?: number | undefined;
118
+ max_instances?: number | undefined;
105
119
  };
106
120
  agents: Record<string, {
107
121
  runtime?: "claude-code" | "codex" | undefined;
package/dist/config.js CHANGED
@@ -276,6 +276,8 @@ export function resolveProjectWithSources(home, name) {
276
276
  continue;
277
277
  sources[`dispatcher.${k}`] = raw.dispatcher[k] !== undefined ? { file: projectFile, key: `dispatcher.${k}` } : { default: true };
278
278
  }
279
+ if (raw.dispatcher.max_instances !== undefined)
280
+ sources["dispatcher.max_instances"] = { file: projectFile, key: "dispatcher.max_instances" };
279
281
  for (const a of Object.values(project.agents)) {
280
282
  const at = (src, field) => {
281
283
  const key = `agents.${a.name}.${field}`;
@@ -0,0 +1,4 @@
1
+ import type { DoctorCheck } from "./doctor.js";
2
+ import type { ResolvedProject } from "./config.js";
3
+ /** Same-role instances (spec 23.13): what doctor says about them. Reads configuration and paths only. */
4
+ export declare function diagnoseInstances(project: ResolvedProject, blocker: string | undefined, platform?: NodeJS.Platform): DoctorCheck[];
@@ -0,0 +1,63 @@
1
+ import path from "node:path";
2
+ import { DEFAULT_MAX_INSTANCES, instanceCap } from "./instances.js";
3
+ import { NAME_RE, canonicalPath, isInside } from "./paths.js";
4
+ import { osSandboxAvailable } from "./policy.js";
5
+ import { git } from "./worktree.js";
6
+ /** Same-role instances (spec 23.13): what doctor says about them. Reads configuration and paths only. */
7
+ export function diagnoseInstances(project, blocker, platform = process.platform) {
8
+ const checks = [];
9
+ const written = project.dispatcher.max_instances;
10
+ const max = written ?? DEFAULT_MAX_INSTANCES;
11
+ const add = (level, message) => checks.push({ level, subject: "instances", message });
12
+ if (written !== undefined && written > 1 && blocker) {
13
+ add("warn", `dispatcher.max_instances is ${written}, but runs here take turns in the checkout (${blocker}): parallel tasks run one at a time.`);
14
+ }
15
+ if (blocker || max <= 1 || project.solo)
16
+ return checks;
17
+ const asRun = { ...project, run: { id: "doctor", dir: "", layout: "run" } };
18
+ const members = Object.keys(project.agents).filter((n) => n !== project.lead);
19
+ const usable = [];
20
+ for (const role of members) {
21
+ if (!NAME_RE.test(role)) {
22
+ add("info", `"${role}" cannot run as several instances (name is not a valid branch name).`);
23
+ continue;
24
+ }
25
+ const cap = instanceCap(asRun, "worktree", role);
26
+ if (cap.n < max && cap.reason)
27
+ add("warn", cap.reason);
28
+ if (cap.n > 1)
29
+ usable.push(role);
30
+ }
31
+ if (!osSandboxAvailable()) {
32
+ for (const role of usable) {
33
+ if (project.agents[role]?.runtime !== "codex")
34
+ continue;
35
+ add(written !== undefined ? "warn" : "info", `only "${role}" writes its memory; its other instances are only told not to (Codex without an OS sandbox).`);
36
+ }
37
+ }
38
+ // The shared memory must not contain, or lie inside, a folder an extra instance writes: that is under the run data roots.
39
+ const roots = [project.paths.taskMemory, project.paths.runs, path.join(project.paths.root, "worktrees")];
40
+ try {
41
+ roots.push(git(project.dir, ["rev-parse", "--path-format=absolute", "--git-common-dir"]));
42
+ }
43
+ catch {
44
+ /* not readable: the other roots still count */
45
+ }
46
+ const ci = platform === "darwin";
47
+ const norm = (p) => (ci ? canonicalPath(p).toLowerCase() : canonicalPath(p));
48
+ for (const role of usable) {
49
+ const a = project.agents[role];
50
+ for (const s of [a?.memory.global, a?.memory.project]) {
51
+ if (!s)
52
+ continue;
53
+ const cs = norm(s);
54
+ const contains = roots.find((r) => isInside(cs, norm(r)));
55
+ const inside = roots.find((r) => isInside(norm(r), cs));
56
+ if (contains)
57
+ add("warn", `"${role}" runs one task at a time: its memory ${s} contains ${contains}, where its other instances would write. Move the memory elsewhere to allow parallel tasks.`);
58
+ else if (inside)
59
+ add("warn", `the memory of "${role}" (${s}) is inside ${inside}, where instances write; this may prevent extra instances for some runs. Move the memory elsewhere to be sure parallel tasks are possible.`);
60
+ }
61
+ }
62
+ return checks;
63
+ }
package/dist/doctor.d.ts CHANGED
@@ -44,3 +44,5 @@ export declare function preflightRuntimes(project: ResolvedProject, opts?: Probe
44
44
  /** Everything `agent-lyceum doctor` reports. Reads configuration and asks the CLIs for `--version`/`--help`; never runs an agent. */
45
45
  export declare function diagnoseProject(project: ResolvedProject, opts?: ProbeOptions): Promise<DoctorReport>;
46
46
  export declare function formatDoctor(r: DoctorReport): string;
47
+ /** One-line summary counted from the checks; `info` and `ok` never count. */
48
+ export declare function doctorFooter(r: DoctorReport): string;
package/dist/doctor.js CHANGED
@@ -7,6 +7,7 @@ import { listRecords, RECORD_KINDS } from "./records.js";
7
7
  import { listRunLocks, listUnreadableRunLocks, lockHolderAlive } from "./project-lock.js";
8
8
  import { runInvocation } from "./process-runner.js";
9
9
  import { validateProject } from "./validate.js";
10
+ import { diagnoseInstances } from "./doctor-instances.js";
10
11
  import { dirtyPaths, git, isolationBlocker } from "./worktree.js";
11
12
  const BINARY = { "claude-code": "claude", codex: "codex" };
12
13
  /** Run a read-only CLI query (`--version`, `--help`): bounded in time and output, never an agent task. */
@@ -138,6 +139,8 @@ export async function diagnoseProject(project, opts = {}) {
138
139
  : { level: "ok", subject: "git", message: "git repo with a commit: runs and agents work in their own worktrees." });
139
140
  }
140
141
  }
142
+ if (fs.existsSync(project.dir))
143
+ checks.push(...diagnoseInstances(project, blocker));
141
144
  if (fs.existsSync(project.dir) && !blocker) {
142
145
  const missing = [];
143
146
  if (!findOnPath("gh", opts.env?.PATH ?? process.env.PATH))
@@ -195,6 +198,19 @@ export function formatDoctor(r) {
195
198
  for (const c of r.runtimes) {
196
199
  lines.push(` ${c.runtime}: ${c.binary === "missing" ? "not installed" : `${c.version ?? "version unknown"}; json ${c.json}, resume ${c.resume}, sandbox ${c.sandbox}, effort ${c.effort}`}`);
197
200
  }
198
- lines.push("", r.ok ? "No problems found." : "Problems found.");
201
+ lines.push("", doctorFooter(r));
199
202
  return lines.join("\n");
200
203
  }
204
+ /** One-line summary counted from the checks; `info` and `ok` never count. */
205
+ export function doctorFooter(r) {
206
+ const errors = r.checks.filter((c) => c.level === "error").length;
207
+ const warns = r.checks.filter((c) => c.level === "warn").length;
208
+ const plural = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
209
+ if (errors > 0) {
210
+ const parts = [plural(errors, "error"), ...(warns > 0 ? [plural(warns, "warning")] : [])];
211
+ return `Problems found: ${parts.join(", ")} (marked ERROR${warns > 0 ? " / warn" : ""} above).`;
212
+ }
213
+ if (warns > 0)
214
+ return `No errors; ${plural(warns, "warning")} to review (marked warn above).`;
215
+ return "No problems found.";
216
+ }
@@ -0,0 +1,15 @@
1
+ import type { ResolvedProject } from "./config.js";
2
+ import { type MailLayout } from "./run-store.js";
3
+ import { type AgentWorkspace } from "./worktree.js";
4
+ export interface InstanceContainment {
5
+ /** Why instance `k` of `role` must not exist (it could write the role's shared memory), judged on the paths it will get; undefined when fine. */
6
+ check: (role: string, k: number) => string | undefined;
7
+ /** The same judgment with the worktree it actually got (root and git dir). */
8
+ checkWorkspace: (role: string, k: number, ws: Pick<AgentWorkspace, "root" | "gitDir">) => string | undefined;
9
+ }
10
+ /**
11
+ * D-17 as one object per run: reads the file system only through `canonicalPath` (lstat/realpath), writes nothing. The shared memory of a role
12
+ * is its `memory.global`/`memory.project`; an instance k >= 2 may write its task memory, outbox, worktree and git dir, none of which may
13
+ * contain or lie inside the shared memory (the OS sandbox and the Codex writable roots are built from the same paths).
14
+ */
15
+ export declare function instanceContainment(base: ResolvedProject, runDir: string, layout: MailLayout, platform?: NodeJS.Platform): InstanceContainment;
@@ -0,0 +1,36 @@
1
+ import { containmentConflict, slotName, slotWritableRoots } from "./instances.js";
2
+ import { canonicalPath } from "./paths.js";
3
+ import { writePolicy } from "./policy.js";
4
+ import { bindRunProject } from "./run-store.js";
5
+ import { recordedGitDir, workspaceRoot } from "./worktree.js";
6
+ /**
7
+ * D-17 as one object per run: reads the file system only through `canonicalPath` (lstat/realpath), writes nothing. The shared memory of a role
8
+ * is its `memory.global`/`memory.project`; an instance k >= 2 may write its task memory, outbox, worktree and git dir, none of which may
9
+ * contain or lie inside the shared memory (the OS sandbox and the Codex writable roots are built from the same paths).
10
+ */
11
+ export function instanceContainment(base, runDir, layout, platform = process.platform) {
12
+ const runId = runDir.split(/[\\/]/).filter(Boolean).pop() ?? "";
13
+ const judge = (role, k, work) => {
14
+ const agent = base.agents[role];
15
+ const shared = [agent?.memory.global, agent?.memory.project].filter((x) => !!x);
16
+ if (!shared.length || k < 2)
17
+ return undefined;
18
+ const slots = Array.from({ length: k }, (_, i) => slotName(role, i + 1));
19
+ const bound = bindRunProject(base, runDir, layout, { roles: { [role]: slots } });
20
+ const inst = bound.agents[slotName(role, k)];
21
+ if (!inst)
22
+ return undefined;
23
+ const roots = slotWritableRoots(writePolicy(bound, inst), work);
24
+ const c = containmentConflict(shared, roots, canonicalPath, platform === "darwin");
25
+ return c ? `could write the shared memory ${c.shared} of "${role}": its writable path ${c.root} contains it or lies inside it` : undefined;
26
+ };
27
+ return {
28
+ check: (role, k) => {
29
+ // An instance that already has a worktree is judged on the git dir recorded for it (read-only), not only on the predicted root.
30
+ const slot = slotName(role, k);
31
+ const gitDir = recordedGitDir(base, runId, slot);
32
+ return judge(role, k, [workspaceRoot(base, runId, slot), ...(gitDir ? [gitDir] : [])]);
33
+ },
34
+ checkWorkspace: (role, k, ws) => judge(role, k, [ws.root, ws.gitDir]),
35
+ };
36
+ }
@@ -0,0 +1,183 @@
1
+ import type { ResolvedAgent, ResolvedProject } from "./config.js";
2
+ import type { WorkspaceMode } from "./worktree.js";
3
+ /** Instances one role may have at the same time when `dispatcher.max_instances` is not written. */
4
+ export declare const DEFAULT_MAX_INSTANCES = 4;
5
+ /** Instance 1 is the role itself; the others are `<role>--<k>`. */
6
+ export declare const slotName: (role: string, k: number) => string;
7
+ /** `<role>--<k>` with k >= 2 (no leading zero) → its parts. A name like `fe--x` is just an agent, never an instance. */
8
+ export declare function parseSlotName(name: string): {
9
+ role: string;
10
+ k: number;
11
+ } | undefined;
12
+ export interface InstanceCap {
13
+ /** How many instances the role may have at once (1 = no parallel). */
14
+ n: number;
15
+ /** Why `n` is 1 (or lower than configured); set whenever the limit is not simply the configured one. */
16
+ reason?: string;
17
+ }
18
+ type CapProject = Pick<ResolvedProject, "lead" | "solo" | "run" | "dispatcher"> & {
19
+ agents: Record<string, unknown>;
20
+ };
21
+ /**
22
+ * The most instances `role` may have in this run. `check(role, k)` is the caller's extra refusal for slot k (a path
23
+ * overlap, spec D-17): it returns a reason, and the cap stops before that slot. `opened` are instance names already in the
24
+ * run's slot table; they are not name clashes even though a bound project lists them among its agents.
25
+ */
26
+ export declare function instanceCap(project: CapProject, mode: WorkspaceMode, role: string, check?: (role: string, k: number) => string | undefined, opened?: readonly string[]): InstanceCap;
27
+ export type ParallelFlag = "absent" | "invalid" | "on" | {
28
+ ignored: string;
29
+ };
30
+ /**
31
+ * What a mail's `parallel` field means (spec 23.3). `senderIsLead` is the router's own knowledge (the outbox owner), never the
32
+ * mail's `from`. `invalid` is a lead's mistake (the mail is sent back); `ignored` mail is delivered as an ordinary one.
33
+ */
34
+ export declare function parallelFlag(input: {
35
+ value: unknown;
36
+ senderIsLead: boolean;
37
+ type: string;
38
+ replyTo?: unknown;
39
+ thread?: unknown;
40
+ cap: InstanceCap;
41
+ }): ParallelFlag;
42
+ /** What the route journal holds about one message id; every field is written by the dispatcher, never read from mail text. */
43
+ export interface OwnerRecord {
44
+ /** Slot the assigner placed it in. */
45
+ assign?: string;
46
+ /** Instance that sent it. */
47
+ from?: string;
48
+ /** Instance a dispatcher notice is about. */
49
+ about?: string;
50
+ /** Its delivery (intent) line: order in the journal, recipient, inbox file name, route source key. */
51
+ intent?: {
52
+ seq: number;
53
+ to: string;
54
+ file: string;
55
+ source: string;
56
+ msg_id: string;
57
+ };
58
+ }
59
+ export interface OwnersCtx {
60
+ /** `state.instances.roles[role]`: the role's open instances, `[0]` is the role itself. */
61
+ opened: readonly string[];
62
+ rec: (id: string) => OwnerRecord | undefined;
63
+ /** Journal position of the role's first held intent (its activation); undefined while it is not in the journal yet. */
64
+ mark: number | undefined;
65
+ /** `agent` of `mail/claims/<id>.json`; undefined when there is no readable claim. */
66
+ claimAgent: (claimId: string) => string | undefined;
67
+ }
68
+ /**
69
+ * The instance of `role` that owns message `id`, so a revision goes back to it (spec 23.7.2 rule 1). Only instances the role has
70
+ * open count, then assign > from > about. Work from before the role was activated that involves it by name belongs to instance 1.
71
+ */
72
+ export declare function ownersOf(id: string, role: string, ctx: OwnersCtx): string | undefined;
73
+ /**
74
+ * Whether a delivery already in the journal concerns `role` by its own name: sent to it, sent by it, or a dispatcher notice about
75
+ * it (a failed integration or a refused worktree). Uses only what the dispatcher wrote: journal fields and claim records.
76
+ */
77
+ export declare function involves(intent: {
78
+ to: string;
79
+ file: string;
80
+ source: string;
81
+ msg_id: string;
82
+ }, role: string, claimAgent: (claimId: string) => string | undefined): boolean;
83
+ /** A message waiting in `mail/inbox/<role>/pending/`, as the journal describes it. */
84
+ export interface HeldMsg {
85
+ file: string;
86
+ id: string;
87
+ /** The recipient name on the mail: the role or one of its instances. */
88
+ to: string;
89
+ replyTo?: string;
90
+ thread?: string;
91
+ /** The journal says the lead marked it `parallel: true`. */
92
+ parallel: boolean;
93
+ }
94
+ export type Acceptance = true | "blocked" | "refused" | "cap";
95
+ export interface PlanInput {
96
+ role: string;
97
+ /** Waiting mail in delivery (file name) order. */
98
+ held: readonly HeldMsg[];
99
+ /** The role's open instances, `[0]` is the role itself. */
100
+ opened: readonly string[];
101
+ /** Effective instance limit. */
102
+ cap: number;
103
+ /** `ownersOf(id, role)` with everything but the id bound. */
104
+ owners: (id: string) => string | undefined;
105
+ /** The slot the journal already recorded for a message. */
106
+ recorded: (id: string) => string | undefined;
107
+ /** Whether a slot may take a new thread (not refused, not blocked, within the cap). */
108
+ acceptsNew: (slot: string) => Acceptance;
109
+ /** The slot's inbox has unread mail. */
110
+ busy: (slot: string) => boolean;
111
+ /** The role's primary model is cooling down after a rate limit. */
112
+ coolingDown: boolean;
113
+ /** Whether a new instance of that name may be opened. */
114
+ mayOpen: (name: string) => true | "collision" | "containment";
115
+ }
116
+ export interface Assignment {
117
+ file: string;
118
+ slot: string;
119
+ rule: "recorded" | "affinity" | "addressed" | "parallel" | "default";
120
+ /** Set when the slot has to be opened first. */
121
+ open?: string;
122
+ /** Set when an affinity placed a message addressed to another instance. */
123
+ addressed?: string;
124
+ }
125
+ export interface Plan {
126
+ assign: Assignment[];
127
+ waiting: {
128
+ file: string;
129
+ reason: string;
130
+ }[];
131
+ }
132
+ /**
133
+ * Where each waiting message of one role goes (spec 23.7.2). A decision never depends on a later message; a message that has to
134
+ * wait only holds back the new threads behind it for the same destination.
135
+ */
136
+ export declare function planAssignments(input: PlanInput): Plan;
137
+ /** A run's slot table: `roles[<role>]` lists the role's open instances, `[0]` is the role itself. */
138
+ export interface SlotTable {
139
+ roles: Record<string, string[]>;
140
+ }
141
+ /**
142
+ * The agents of a run with its opened instances added (spec 23.6): each `<role>--<k>` follows its role, copies the role's settings, keeps only
143
+ * its own task memory (the role's global/project memory become read-only `sharedMemory`) and cannot edit AGENT.md. Roles of the table that
144
+ * are not agents any more are skipped (a resume refuses those before it binds, `assertInstancesResumable`).
145
+ */
146
+ export declare function synthesizeSlots(agents: Record<string, ResolvedAgent>, table: SlotTable, taskMemory: (slot: string) => string): Record<string, ResolvedAgent>;
147
+ /** The role an agent name stands for: the role itself, or the role of a bound instance. */
148
+ export declare const roleOfName: (agents: Record<string, {
149
+ instance?: {
150
+ role: string;
151
+ };
152
+ } | undefined>, name: string) => string;
153
+ /** How the lead is told who a mail came from (decision 8): a bound instance of a role with several shows its role and number. */
154
+ export declare function senderLabel(name: string, agents: Record<string, {
155
+ instance?: {
156
+ role: string;
157
+ k: number;
158
+ };
159
+ } | undefined>): string;
160
+ /** Key of the one failure sent to the lead about this set of waiting mails (spec D-16). */
161
+ export declare const heldStallKey: (ids: readonly string[]) => string;
162
+ /**
163
+ * Refuse, before anything is written, to resume a run whose slot table no longer fits the configuration (spec 23.10): a role that
164
+ * is gone or is now the lead, an instance name that is now a configured agent, or an opened instance `check` finds a conflict for.
165
+ */
166
+ export declare function assertInstancesResumable(base: {
167
+ lead: string;
168
+ agents: Record<string, unknown>;
169
+ }, table: SlotTable, runId: string, projectName: string, check?: (role: string, k: number) => string | undefined): void;
170
+ /** Every directory an instance can write: what its policy allows, the folders of the files it may edit, and its working directory. */
171
+ export declare const slotWritableRoots: (pol: {
172
+ allowDirs: readonly string[];
173
+ allowFiles: readonly string[];
174
+ }, work: readonly string[]) => string[];
175
+ /**
176
+ * The first pair of (shared memory path, writable root) where one contains the other, or undefined. `canon` resolves symlinks (the caller injects
177
+ * it); `caseInsensitive` compares in lower case (macOS volumes), which can only report a conflict that is not one, never miss one.
178
+ */
179
+ export declare function containmentConflict(shared: readonly string[], roots: readonly string[], canon: (p: string) => string, caseInsensitive: boolean): {
180
+ shared: string;
181
+ root: string;
182
+ } | undefined;
183
+ export {};