agent-lyceum 0.25.1 → 0.27.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 +6 -2
- package/README.zh-TW.md +6 -2
- package/dist/call.js +2 -0
- package/dist/config.d.ts +29 -1
- package/dist/config.js +34 -3
- package/dist/doctor-instances.d.ts +4 -0
- package/dist/doctor-instances.js +63 -0
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +17 -1
- package/dist/duplicate-notices.d.ts +9 -0
- package/dist/duplicate-notices.js +34 -0
- package/dist/fallback.d.ts +4 -2
- package/dist/fallback.js +11 -4
- package/dist/guard.d.ts +17 -1
- package/dist/guard.js +37 -6
- package/dist/instance-containment.d.ts +15 -0
- package/dist/instance-containment.js +36 -0
- package/dist/instances.d.ts +183 -0
- package/dist/instances.js +268 -0
- package/dist/mailbox.d.ts +46 -2
- package/dist/mailbox.js +121 -36
- package/dist/message-store.d.ts +47 -1
- package/dist/message-store.js +75 -7
- package/dist/paths.d.ts +7 -0
- package/dist/paths.js +27 -2
- package/dist/policy.d.ts +1 -0
- package/dist/policy.js +2 -2
- package/dist/prompt.d.ts +11 -0
- package/dist/prompt.js +66 -10
- package/dist/run-session.d.ts +35 -2
- package/dist/run-session.js +337 -20
- package/dist/run-store.d.ts +17 -3
- package/dist/run-store.js +17 -7
- package/dist/scaffold.d.ts +3 -0
- package/dist/scaffold.js +10 -1
- package/dist/schema.d.ts +19 -0
- package/dist/schema.js +12 -0
- package/dist/status.d.ts +7 -0
- package/dist/status.js +74 -15
- package/dist/tiers.d.ts +27 -0
- package/dist/tiers.js +49 -0
- package/dist/validate.js +28 -1
- package/dist/worktree.d.ts +6 -0
- package/dist/worktree.js +16 -2
- package/package.json +1 -1
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
|
|
|
@@ -75,6 +75,10 @@ agents:
|
|
|
75
75
|
|
|
76
76
|
Folder layout, every agent field and the rules `doctor` checks: [Configuration](docs/configuration.md).
|
|
77
77
|
|
|
78
|
+
## Downgrading
|
|
79
|
+
|
|
80
|
+
Older versions refuse a `team.yaml` or `project.yaml` that contains `model_tiers` (a load error, "Unrecognized key"). Before using an older binary, remove `model_tiers` from every `project.yaml` and from `team.yaml`. Other mail and run state are unaffected: older versions ignore a `tier` line in mail. See [Model tiers](docs/configuration.md#model-tiers). (Spec 24.3.2, 24.13)
|
|
81
|
+
|
|
78
82
|
## Topics
|
|
79
83
|
|
|
80
84
|
| Topic | Read |
|
|
@@ -82,6 +86,6 @@ Folder layout, every agent field and the rules `doctor` checks: [Configuration](
|
|
|
82
86
|
| Run lifecycle, parallel agents, several runs at once, follow-ups, opening a PR | [How a run works](docs/how-it-works.md) |
|
|
83
87
|
| What agents may write, sandbox levels, known gaps | [Write scope](docs/write-scope.md) |
|
|
84
88
|
| 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) |
|
|
89
|
+
| Live run status inside Claude Code (a framed, state-coloured tree of agents; pane or full-width band) | [Monitor mod](docs/monitor.md) |
|
|
86
90
|
| All commands in full | [Commands](docs/commands.md) |
|
|
87
91
|
| 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
|
|
|
@@ -75,6 +75,10 @@ agents:
|
|
|
75
75
|
|
|
76
76
|
資料夾結構、所有 agent 欄位與 `doctor` 檢查的規則:[設定](docs/configuration.zh-TW.md)。
|
|
77
77
|
|
|
78
|
+
## 降級(改用舊版)
|
|
79
|
+
|
|
80
|
+
舊版程式會拒絕載入含 `model_tiers` 的 `team.yaml` 或 `project.yaml`(載入錯誤,「Unrecognized key」)。改用舊版前,請先從每個 `project.yaml` 與 `team.yaml` 拿掉 `model_tiers`。其他信件與 run 狀態不受影響:舊版會忽略信件中的 `tier` 行。見[模型分級](docs/configuration.zh-TW.md#模型分級)。(spec 24.3.2、24.13)
|
|
81
|
+
|
|
78
82
|
## 主題
|
|
79
83
|
|
|
80
84
|
| 主題 | 閱讀 |
|
|
@@ -82,6 +86,6 @@ agents:
|
|
|
82
86
|
| run 的生命週期、平行 agent、同時多個 run、追加需求、開 PR | [一次執行如何運作](docs/how-it-works.zh-TW.md) |
|
|
83
87
|
| agent 能寫什麼、沙箱等級、已知缺口 | [寫入範圍](docs/write-scope.zh-TW.md) |
|
|
84
88
|
| 讓 agent 搜尋或讀取網頁 | [網路存取](docs/web-access.zh-TW.md) |
|
|
85
|
-
| 在 Claude Code 內看即時 run
|
|
89
|
+
| 在 Claude Code 內看即時 run 狀態(每個 agent 一個依狀態上色的外框,樹狀排列;面板或整寬橫幅) | [監控 mod](docs/monitor.zh-TW.md) |
|
|
86
90
|
| 所有指令的完整說明 | [指令](docs/commands.zh-TW.md) |
|
|
87
91
|
| 從舊版升級 | [升級到 run v2](docs/upgrading-run-v2.md) |
|
package/dist/call.js
CHANGED
package/dist/config.d.ts
CHANGED
|
@@ -33,6 +33,18 @@ export interface ResolvedAgent {
|
|
|
33
33
|
}[];
|
|
34
34
|
/** Where each field's value came from (for `validate` output). */
|
|
35
35
|
sources: Record<string, Source>;
|
|
36
|
+
/** Spec 24: the tiers this agent can run on (its runtime's ladder up to and including `model`); unset when nothing is below `model`. */
|
|
37
|
+
tiers?: string[];
|
|
38
|
+
/** Set when the run bound this agent as an instance of a role that has several (spec 23): which role, and the instance number. */
|
|
39
|
+
instance?: {
|
|
40
|
+
role: string;
|
|
41
|
+
k: number;
|
|
42
|
+
};
|
|
43
|
+
/** Instances 2 and up: the role's global/project memory, which only instance 1 may write. */
|
|
44
|
+
sharedMemory?: {
|
|
45
|
+
global?: string;
|
|
46
|
+
project?: string;
|
|
47
|
+
};
|
|
36
48
|
}
|
|
37
49
|
export interface ResolvedProject {
|
|
38
50
|
home: string;
|
|
@@ -42,6 +54,10 @@ export interface ResolvedProject {
|
|
|
42
54
|
lead: string;
|
|
43
55
|
dispatcher: DispatcherSettings;
|
|
44
56
|
agents: Record<string, ResolvedAgent>;
|
|
57
|
+
/** Spec 24: non-empty model ladders per runtime, lowest first (project.yaml replaces team.yaml's per runtime). */
|
|
58
|
+
modelTiers: Partial<Record<Runtime, string[]>>;
|
|
59
|
+
/** Which file each ladder in `modelTiers` came from. */
|
|
60
|
+
modelTiersFrom: Partial<Record<Runtime, "global" | "project">>;
|
|
45
61
|
paths: ProjectPaths;
|
|
46
62
|
/** Removed settings still present in config files (ignored); `validate` warns once per file. */
|
|
47
63
|
ignoredSettings?: {
|
|
@@ -52,7 +68,10 @@ export interface ResolvedProject {
|
|
|
52
68
|
run?: {
|
|
53
69
|
id: string;
|
|
54
70
|
dir: string;
|
|
55
|
-
layout: "run" | "legacy";
|
|
71
|
+
layout: "run" | "legacy"; /** Set by the run when same-role instances are possible: the lead's prompt then explains `parallel: true`. */
|
|
72
|
+
instances?: {
|
|
73
|
+
max: number;
|
|
74
|
+
};
|
|
56
75
|
};
|
|
57
76
|
/** Agents working in their own git worktree instead of `dir` (set by the dispatcher for the wake-ups in progress). */
|
|
58
77
|
workspaces?: Record<string, AgentWorkspace>;
|
|
@@ -60,6 +79,10 @@ export interface ResolvedProject {
|
|
|
60
79
|
solo?: "run" | "call";
|
|
61
80
|
}
|
|
62
81
|
export declare function loadGlobal(home: string): {
|
|
82
|
+
model_tiers?: {
|
|
83
|
+
"claude-code"?: string[] | undefined;
|
|
84
|
+
codex?: string[] | undefined;
|
|
85
|
+
} | undefined;
|
|
63
86
|
agents: Record<string, {
|
|
64
87
|
runtime?: "claude-code" | "codex" | undefined;
|
|
65
88
|
model?: string | undefined;
|
|
@@ -87,6 +110,10 @@ export declare function loadProjectRaw(home: string, name: string): {
|
|
|
87
110
|
team: {
|
|
88
111
|
lead: string;
|
|
89
112
|
};
|
|
113
|
+
model_tiers?: {
|
|
114
|
+
"claude-code"?: string[] | undefined;
|
|
115
|
+
codex?: string[] | undefined;
|
|
116
|
+
} | undefined;
|
|
90
117
|
dispatcher: {
|
|
91
118
|
max_rounds?: number | undefined;
|
|
92
119
|
max_parallel?: unknown;
|
|
@@ -102,6 +129,7 @@ export declare function loadProjectRaw(home: string, name: string): {
|
|
|
102
129
|
effort?: "high" | "low" | "max" | "medium" | "minimal" | "xhigh" | undefined;
|
|
103
130
|
}[] | undefined;
|
|
104
131
|
fallback_cooldown_sec?: number | undefined;
|
|
132
|
+
max_instances?: number | undefined;
|
|
105
133
|
};
|
|
106
134
|
agents: Record<string, {
|
|
107
135
|
runtime?: "claude-code" | "codex" | undefined;
|
package/dist/config.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import YAML from "yaml";
|
|
4
|
-
import { absPath, homePaths, projectPaths, } from "./paths.js";
|
|
5
|
-
import {
|
|
4
|
+
import { absPath, escapesParent, homePaths, projectPaths, } from "./paths.js";
|
|
5
|
+
import { availableTiers } from "./tiers.js";
|
|
6
|
+
import { DISPATCHER_DEFAULTS, GlobalConfig, REMOVED_AGENT_KEYS, REMOVED_DISPATCHER_KEYS, ProjectConfig, inferRuntime, RUNTIMES, } from "./schema.js";
|
|
6
7
|
export class ConfigError extends Error {
|
|
7
8
|
}
|
|
8
9
|
function readYaml(file, schema, label) {
|
|
@@ -60,6 +61,16 @@ export function resolveProjectFrom(home, name, g, p) {
|
|
|
60
61
|
const lead = p.team.lead;
|
|
61
62
|
const { max_parallel: _mp, max_concurrent_runs: _mc, workspace_mode: _wm, fallback: dispatcherFallback, ...liveDispatcher } = p.dispatcher;
|
|
62
63
|
const dispatcher = { ...DISPATCHER_DEFAULTS, ...stripUndef(liveDispatcher) };
|
|
64
|
+
const modelTiers = {};
|
|
65
|
+
const modelTiersFrom = {};
|
|
66
|
+
for (const rt of RUNTIMES) {
|
|
67
|
+
const fromProject = p.model_tiers?.[rt] !== undefined;
|
|
68
|
+
const ladder = fromProject ? p.model_tiers?.[rt] : g.model_tiers?.[rt];
|
|
69
|
+
if (ladder?.length) {
|
|
70
|
+
modelTiers[rt] = [...ladder];
|
|
71
|
+
modelTiersFrom[rt] = fromProject ? "project" : "global";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
63
74
|
const agents = {};
|
|
64
75
|
for (const agentName of Object.keys(p.agents)) {
|
|
65
76
|
const gl = resolveLayer(g.agents[agentName], home);
|
|
@@ -144,6 +155,7 @@ export function resolveProjectFrom(home, name, g, p) {
|
|
|
144
155
|
}
|
|
145
156
|
}
|
|
146
157
|
const { entries: fallback, invalid: fallbackInvalid } = resolveFallback(rawFallback, runtime);
|
|
158
|
+
const tiers = runtime ? availableTiers(modelTiers[runtime], model) : undefined;
|
|
147
159
|
const isLead = agentName === lead;
|
|
148
160
|
const resume = pick("resume");
|
|
149
161
|
const canMessage = pick("can_message");
|
|
@@ -175,6 +187,7 @@ export function resolveProjectFrom(home, name, g, p) {
|
|
|
175
187
|
fallbackShared,
|
|
176
188
|
fallbackInvalid,
|
|
177
189
|
sources,
|
|
190
|
+
...(tiers ? { tiers } : {}),
|
|
178
191
|
};
|
|
179
192
|
}
|
|
180
193
|
return {
|
|
@@ -184,6 +197,8 @@ export function resolveProjectFrom(home, name, g, p) {
|
|
|
184
197
|
lead,
|
|
185
198
|
dispatcher,
|
|
186
199
|
agents,
|
|
200
|
+
modelTiers,
|
|
201
|
+
modelTiersFrom,
|
|
187
202
|
paths,
|
|
188
203
|
ignoredSettings: collectIgnored(hp.globalConfig, g, paths.config, p),
|
|
189
204
|
};
|
|
@@ -252,7 +267,7 @@ export function findProjectForCwd(home, cwd) {
|
|
|
252
267
|
let best;
|
|
253
268
|
for (const pr of listProjects(home)) {
|
|
254
269
|
const rel = path.relative(pr.dir, cwd);
|
|
255
|
-
if (
|
|
270
|
+
if (!escapesParent(rel)) {
|
|
256
271
|
if (!best || pr.dir.length > best.len)
|
|
257
272
|
best = { name: pr.name, len: pr.dir.length };
|
|
258
273
|
}
|
|
@@ -276,6 +291,13 @@ export function resolveProjectWithSources(home, name) {
|
|
|
276
291
|
continue;
|
|
277
292
|
sources[`dispatcher.${k}`] = raw.dispatcher[k] !== undefined ? { file: projectFile, key: `dispatcher.${k}` } : { default: true };
|
|
278
293
|
}
|
|
294
|
+
if (raw.dispatcher.max_instances !== undefined)
|
|
295
|
+
sources["dispatcher.max_instances"] = { file: projectFile, key: "dispatcher.max_instances" };
|
|
296
|
+
for (const rt of RUNTIMES) {
|
|
297
|
+
const from = project.modelTiersFrom[rt];
|
|
298
|
+
if (from)
|
|
299
|
+
sources[`model_tiers.${rt}`] = { file: from === "project" ? projectFile : globalFile, key: `model_tiers.${rt}` };
|
|
300
|
+
}
|
|
279
301
|
for (const a of Object.values(project.agents)) {
|
|
280
302
|
const at = (src, field) => {
|
|
281
303
|
const key = `agents.${a.name}.${field}`;
|
|
@@ -305,6 +327,10 @@ export function resolveProjectWithSources(home, name) {
|
|
|
305
327
|
else
|
|
306
328
|
sources[`agents.${a.name}.${f}`] = at(src, f);
|
|
307
329
|
}
|
|
330
|
+
// The ladder is the runtime's, so its source is the ladder's own key (spec 24.9).
|
|
331
|
+
const ladderSrc = a.tiers ? sources[`model_tiers.${a.runtime}`] : undefined;
|
|
332
|
+
if (ladderSrc)
|
|
333
|
+
sources[`agents.${a.name}.model_tiers`] = ladderSrc;
|
|
308
334
|
}
|
|
309
335
|
return { project, sources };
|
|
310
336
|
}
|
|
@@ -312,6 +338,9 @@ export function resolveProjectWithSources(home, name) {
|
|
|
312
338
|
export function flattenResolved(project) {
|
|
313
339
|
const v = { dir: project.dir, "team.lead": project.lead };
|
|
314
340
|
const anyFallback = Object.values(project.agents).some((a) => a.fallback.length > 0);
|
|
341
|
+
for (const rt of RUNTIMES)
|
|
342
|
+
if (project.modelTiers[rt])
|
|
343
|
+
v[`model_tiers.${rt}`] = project.modelTiers[rt];
|
|
315
344
|
for (const [k, val] of Object.entries(project.dispatcher)) {
|
|
316
345
|
if (k === "fallback_cooldown_sec" && !anyFallback)
|
|
317
346
|
continue; // keep `doctor --resolved` unchanged without fallback
|
|
@@ -332,6 +361,8 @@ export function flattenResolved(project) {
|
|
|
332
361
|
v[`${p}.allow_web`] = a.allowWeb;
|
|
333
362
|
if (a.fallback.length)
|
|
334
363
|
v[`${p}.fallback`] = a.fallback;
|
|
364
|
+
if (a.tiers)
|
|
365
|
+
v[`${p}.model_tiers`] = a.tiers;
|
|
335
366
|
}
|
|
336
367
|
return v;
|
|
337
368
|
}
|
|
@@ -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
|
|
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,9 @@
|
|
|
1
|
+
/** How many distinct sent-again mails of one repeat are announced to the sender before further repeats are set aside silently. */
|
|
2
|
+
export declare const MAX_DUPLICATE_NOTICES = 2;
|
|
3
|
+
/** Key of one sender's repeat of earlier mail; JSON keeps it unambiguous whatever the names contain. */
|
|
4
|
+
export declare function duplicateNoticeKey(sender: string, repeats: Iterable<[string, string]>): string;
|
|
5
|
+
/**
|
|
6
|
+
* Decide whether the repeat found in outbox mail `source` is announced. Idempotent per source: the notified source ids
|
|
7
|
+
* are stored, so a replay after a crash neither counts twice nor loses a notice it already earned.
|
|
8
|
+
*/
|
|
9
|
+
export declare function shouldNotifyDuplicate(runDir: string, key: string, source: string): boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { atomicWrite } from "./fs-util.js";
|
|
4
|
+
/** How many distinct sent-again mails of one repeat are announced to the sender before further repeats are set aside silently. */
|
|
5
|
+
export const MAX_DUPLICATE_NOTICES = 2;
|
|
6
|
+
const FILE = "duplicate-notices.json";
|
|
7
|
+
/** Key of one sender's repeat of earlier mail; JSON keeps it unambiguous whatever the names contain. */
|
|
8
|
+
export function duplicateNoticeKey(sender, repeats) {
|
|
9
|
+
return JSON.stringify([sender, [...repeats].map(([to, id]) => [to, id]).sort()]);
|
|
10
|
+
}
|
|
11
|
+
function load(runDir) {
|
|
12
|
+
try {
|
|
13
|
+
const data = JSON.parse(fs.readFileSync(path.join(runDir, FILE), "utf8"));
|
|
14
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Decide whether the repeat found in outbox mail `source` is announced. Idempotent per source: the notified source ids
|
|
22
|
+
* are stored, so a replay after a crash neither counts twice nor loses a notice it already earned.
|
|
23
|
+
*/
|
|
24
|
+
export function shouldNotifyDuplicate(runDir, key, source) {
|
|
25
|
+
const all = load(runDir);
|
|
26
|
+
const seen = Array.isArray(all[key]) ? all[key] : [];
|
|
27
|
+
if (seen.includes(source))
|
|
28
|
+
return true;
|
|
29
|
+
if (seen.length >= MAX_DUPLICATE_NOTICES)
|
|
30
|
+
return false;
|
|
31
|
+
all[key] = [...seen, source];
|
|
32
|
+
atomicWrite(path.join(runDir, FILE), `${JSON.stringify(all, null, 2)}\n`);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
package/dist/fallback.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface Candidate {
|
|
|
7
7
|
model?: string;
|
|
8
8
|
effort?: Effort;
|
|
9
9
|
primary: boolean;
|
|
10
|
+
/** The cheaper model a lead's `tier` picked (spec 24.7): not the primary, and not a fallback either. */
|
|
11
|
+
tiered?: true;
|
|
10
12
|
/** `<runtime>/<model>`; the cooldown and session key. */
|
|
11
13
|
key: string;
|
|
12
14
|
}
|
|
@@ -14,7 +16,7 @@ export interface Candidate {
|
|
|
14
16
|
* The primary model followed by the agent's fallback chain, in order. An entry with the same key as the primary or as an
|
|
15
17
|
* earlier entry is dropped: a chain shared through `dispatcher.fallback` may list the agent's own primary (spec 20.12).
|
|
16
18
|
*/
|
|
17
|
-
export declare function candidatesFor(agent: ResolvedAgent): Candidate[];
|
|
19
|
+
export declare function candidatesFor(agent: ResolvedAgent, tierModel?: string): Candidate[];
|
|
18
20
|
/**
|
|
19
21
|
* The agent as it runs on candidate `c`: itself for the primary; for a backup, the same agent on the backup's runtime, model
|
|
20
22
|
* and effort, at the primary's effective web level (spec 20.12). A set `allow_web` carries over. Unset, claude-code has no web
|
|
@@ -39,7 +41,7 @@ export declare class Cooldowns {
|
|
|
39
41
|
}>): Cooldowns;
|
|
40
42
|
}
|
|
41
43
|
/** The candidates for one wake: the chain minus those cooling down, or the whole chain when every one is (something must be tried). */
|
|
42
|
-
export declare function planAttempts(
|
|
44
|
+
export declare function planAttempts(all: readonly Candidate[], cooldowns: Cooldowns, now: number): Candidate[];
|
|
43
45
|
export type Step = "retry-same" | "next-candidate" | "stop";
|
|
44
46
|
/**
|
|
45
47
|
* What to do after a failed call (spec 20.2 decisions 2 and 5). `attemptsOnCurrent` counts the calls made on the current
|
package/dist/fallback.js
CHANGED
|
@@ -4,12 +4,18 @@ const keyOf = (runtime, model) => `${runtime}/${(model ?? "").trim().toLowerCase
|
|
|
4
4
|
* The primary model followed by the agent's fallback chain, in order. An entry with the same key as the primary or as an
|
|
5
5
|
* earlier entry is dropped: a chain shared through `dispatcher.fallback` may list the agent's own primary (spec 20.12).
|
|
6
6
|
*/
|
|
7
|
-
export function candidatesFor(agent) {
|
|
7
|
+
export function candidatesFor(agent, tierModel) {
|
|
8
8
|
// `validate` refuses an agent without a runtime before anything is woken.
|
|
9
9
|
const runtime = agent.runtime;
|
|
10
10
|
const primary = { runtime, model: agent.model, effort: agent.effort, primary: true, key: keyOf(runtime, agent.model) };
|
|
11
11
|
const out = [primary];
|
|
12
12
|
const seen = new Set([primary.key]);
|
|
13
|
+
// A tier's cheaper model goes first (spec 24.7.1); one that is the ceiling itself adds nothing.
|
|
14
|
+
if (tierModel && keyOf(runtime, tierModel) !== primary.key) {
|
|
15
|
+
const key = keyOf(runtime, tierModel);
|
|
16
|
+
out.unshift({ runtime, model: tierModel, effort: agent.effort, primary: false, tiered: true, key });
|
|
17
|
+
seen.add(key);
|
|
18
|
+
}
|
|
13
19
|
for (const f of agent.fallback) {
|
|
14
20
|
const model = f.model || undefined;
|
|
15
21
|
const key = keyOf(f.runtime, model);
|
|
@@ -28,6 +34,8 @@ export function candidatesFor(agent) {
|
|
|
28
34
|
export function agentForCandidate(agent, c) {
|
|
29
35
|
if (c.primary)
|
|
30
36
|
return agent;
|
|
37
|
+
if (c.tiered)
|
|
38
|
+
return { ...agent, model: c.model };
|
|
31
39
|
const allowWeb = agent.allowWeb ?? (agent.runtime === "claude-code" && c.runtime === "codex" ? "none" : undefined);
|
|
32
40
|
return { ...agent, runtime: c.runtime, model: c.model, effort: c.effort, allowWeb };
|
|
33
41
|
}
|
|
@@ -63,10 +71,9 @@ export class Cooldowns {
|
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
73
|
/** The candidates for one wake: the chain minus those cooling down, or the whole chain when every one is (something must be tried). */
|
|
66
|
-
export function planAttempts(
|
|
67
|
-
const all = candidatesFor(agent);
|
|
74
|
+
export function planAttempts(all, cooldowns, now) {
|
|
68
75
|
const warm = all.filter((c) => cooldowns.active(c.key, now) === 0);
|
|
69
|
-
return warm.length ? warm : all;
|
|
76
|
+
return warm.length ? warm : [...all];
|
|
70
77
|
}
|
|
71
78
|
/**
|
|
72
79
|
* What to do after a failed call (spec 20.2 decisions 2 and 5). `attemptsOnCurrent` counts the calls made on the current
|
package/dist/guard.d.ts
CHANGED
|
@@ -9,6 +9,12 @@ export interface Violation {
|
|
|
9
9
|
/** The kept copy is cut to MAX_KEEP_BYTES. */
|
|
10
10
|
truncated?: boolean;
|
|
11
11
|
}
|
|
12
|
+
/** A change to a cross-run shared file that no agent of the batch may make, while another run of the project was live. */
|
|
13
|
+
export interface Unattributed {
|
|
14
|
+
file: string;
|
|
15
|
+
agents: string[];
|
|
16
|
+
otherRuns: string[];
|
|
17
|
+
}
|
|
12
18
|
export declare const MAX_KEEP_BYTES: number;
|
|
13
19
|
/**
|
|
14
20
|
* Detects (and reverts) changes to protected files made by agents that may not edit them.
|
|
@@ -16,10 +22,20 @@ export declare const MAX_KEEP_BYTES: number;
|
|
|
16
22
|
*/
|
|
17
23
|
export declare class ProtectedGuard {
|
|
18
24
|
private project;
|
|
25
|
+
private otherLiveRuns;
|
|
19
26
|
private baseline;
|
|
20
27
|
/** Regular files of the records tree (including `.archive/`); only the lead may change them. */
|
|
21
28
|
private records;
|
|
22
|
-
|
|
29
|
+
/** Changes kept (not reverted) because another live run may have made them; see `takeUnattributed`. */
|
|
30
|
+
private unattributed;
|
|
31
|
+
/** Other runs that were live at the last `rebase()` (or at `check()` time); computed once per batch. */
|
|
32
|
+
private liveAtBase;
|
|
33
|
+
/** `otherLiveRuns`: ids of the project's other runs that have a live lock holder right now. */
|
|
34
|
+
constructor(project: ResolvedProject, otherLiveRuns?: () => string[]);
|
|
35
|
+
/** The unattributed changes since the last call (they were kept and adopted as the baseline). */
|
|
36
|
+
takeUnattributed(): Unattributed[];
|
|
37
|
+
/** Files shared by every run of the project: only these can be changed by another run's lead. */
|
|
38
|
+
private crossRunShared;
|
|
23
39
|
/**
|
|
24
40
|
* Take the current files as the baseline. Called right before a batch is woken, so only what changes while it runs is
|
|
25
41
|
* judged: edits made in between (you, or another run of the project) are not the batch's doing.
|
package/dist/guard.js
CHANGED
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { readRegularFileNoFollow } from "./fs-util.js";
|
|
5
|
+
import { escapesParent } from "./paths.js";
|
|
5
6
|
import { mayEditProtected, recordsDir, repoInstructionFiles, sharedContextFiles, sharedIndex } from "./policy.js";
|
|
6
7
|
export const MAX_KEEP_BYTES = 1024 * 1024;
|
|
7
8
|
function safeName(file) {
|
|
@@ -13,18 +14,38 @@ function safeName(file) {
|
|
|
13
14
|
*/
|
|
14
15
|
export class ProtectedGuard {
|
|
15
16
|
project;
|
|
17
|
+
otherLiveRuns;
|
|
16
18
|
baseline = new Map();
|
|
17
19
|
/** Regular files of the records tree (including `.archive/`); only the lead may change them. */
|
|
18
20
|
records = new Map();
|
|
19
|
-
|
|
21
|
+
/** Changes kept (not reverted) because another live run may have made them; see `takeUnattributed`. */
|
|
22
|
+
unattributed = [];
|
|
23
|
+
/** Other runs that were live at the last `rebase()` (or at `check()` time); computed once per batch. */
|
|
24
|
+
liveAtBase = [];
|
|
25
|
+
/** `otherLiveRuns`: ids of the project's other runs that have a live lock holder right now. */
|
|
26
|
+
constructor(project, otherLiveRuns = () => []) {
|
|
20
27
|
this.project = project;
|
|
28
|
+
this.otherLiveRuns = otherLiveRuns;
|
|
21
29
|
this.rebase();
|
|
22
30
|
}
|
|
31
|
+
/** The unattributed changes since the last call (they were kept and adopted as the baseline). */
|
|
32
|
+
takeUnattributed() {
|
|
33
|
+
const out = this.unattributed;
|
|
34
|
+
this.unattributed = [];
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/** Files shared by every run of the project: only these can be changed by another run's lead. */
|
|
38
|
+
crossRunShared(file) {
|
|
39
|
+
if (sharedContextFiles(this.project).includes(file))
|
|
40
|
+
return true;
|
|
41
|
+
return Object.values(this.project.agents).some((a) => a.agentMd === file);
|
|
42
|
+
}
|
|
23
43
|
/**
|
|
24
44
|
* Take the current files as the baseline. Called right before a batch is woken, so only what changes while it runs is
|
|
25
45
|
* judged: edits made in between (you, or another run of the project) are not the batch's doing.
|
|
26
46
|
*/
|
|
27
47
|
rebase() {
|
|
48
|
+
this.liveAtBase = this.otherLiveRuns();
|
|
28
49
|
for (const f of ProtectedGuard.protectedFiles(this.project))
|
|
29
50
|
this.baseline.set(f, read(f));
|
|
30
51
|
this.records = readTree(recordsDir(this.project));
|
|
@@ -69,6 +90,7 @@ export class ProtectedGuard {
|
|
|
69
90
|
/** Compare against the baseline after `running` agents finished; revert unauthorized changes. */
|
|
70
91
|
check(running) {
|
|
71
92
|
const violations = [];
|
|
93
|
+
const other = [...new Set([...this.liveAtBase, ...this.otherLiveRuns()])];
|
|
72
94
|
for (const [file, before] of this.baseline) {
|
|
73
95
|
const now = read(file);
|
|
74
96
|
if (same(before, now))
|
|
@@ -78,6 +100,11 @@ export class ProtectedGuard {
|
|
|
78
100
|
this.baseline.set(file, now);
|
|
79
101
|
continue;
|
|
80
102
|
}
|
|
103
|
+
if (other.length && this.crossRunShared(file)) {
|
|
104
|
+
this.baseline.set(file, now);
|
|
105
|
+
this.unattributed.push({ file, agents: running.map((a) => a.name), otherRuns: other });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
81
108
|
if (before === null) {
|
|
82
109
|
const kept = this.keep(file, now);
|
|
83
110
|
fs.rmSync(file, { force: true });
|
|
@@ -90,10 +117,10 @@ export class ProtectedGuard {
|
|
|
90
117
|
violations.push({ file, action: "restored", suspects: running.map((a) => a.name), ...kept });
|
|
91
118
|
}
|
|
92
119
|
}
|
|
93
|
-
this.checkRecords(running, violations);
|
|
120
|
+
this.checkRecords(running, violations, other);
|
|
94
121
|
return violations;
|
|
95
122
|
}
|
|
96
|
-
checkRecords(running, violations) {
|
|
123
|
+
checkRecords(running, violations, other) {
|
|
97
124
|
const root = recordsDir(this.project);
|
|
98
125
|
// Never write through a symlinked parent of the records tree: skip its protection for this batch.
|
|
99
126
|
if (isLink(path.dirname(root)) || isLink(path.dirname(path.dirname(root))))
|
|
@@ -109,6 +136,10 @@ export class ProtectedGuard {
|
|
|
109
136
|
continue;
|
|
110
137
|
if (lead)
|
|
111
138
|
continue;
|
|
139
|
+
if (other.length) {
|
|
140
|
+
this.unattributed.push({ file, agents: suspects, otherRuns: other });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
112
143
|
const kept = this.keep(file, cur);
|
|
113
144
|
if (before === null) {
|
|
114
145
|
if (!insideRoot(root, path.dirname(file)))
|
|
@@ -124,7 +155,7 @@ export class ProtectedGuard {
|
|
|
124
155
|
violations.push({ file, action: "restored", suspects, ...kept });
|
|
125
156
|
}
|
|
126
157
|
}
|
|
127
|
-
if (lead)
|
|
158
|
+
if (lead || other.length)
|
|
128
159
|
this.records = now;
|
|
129
160
|
}
|
|
130
161
|
/**
|
|
@@ -133,7 +164,7 @@ export class ProtectedGuard {
|
|
|
133
164
|
*/
|
|
134
165
|
realDirs(root, dir, done, suspects, violations) {
|
|
135
166
|
const rel = path.relative(root, dir);
|
|
136
|
-
if (
|
|
167
|
+
if (escapesParent(rel))
|
|
137
168
|
return false;
|
|
138
169
|
let cur = root;
|
|
139
170
|
for (const part of ["", ...(rel ? rel.split(path.sep) : [])]) {
|
|
@@ -167,7 +198,7 @@ function isLink(p) {
|
|
|
167
198
|
function insideRoot(root, dir) {
|
|
168
199
|
try {
|
|
169
200
|
const r = path.relative(fs.realpathSync(root), fs.realpathSync(dir));
|
|
170
|
-
return !
|
|
201
|
+
return !escapesParent(r);
|
|
171
202
|
}
|
|
172
203
|
catch {
|
|
173
204
|
return false;
|
|
@@ -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;
|