agent-lyceum 0.14.0 → 0.15.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 +5 -2
- package/README.zh-TW.md +5 -3
- package/dist/adapters/claude.js +1 -1
- package/dist/fs-util.d.ts +8 -0
- package/dist/fs-util.js +36 -0
- package/dist/guard.js +4 -1
- package/dist/mailbox.js +4 -1
- package/dist/policy.d.ts +9 -0
- package/dist/policy.js +34 -3
- package/dist/prompt.d.ts +2 -1
- package/dist/prompt.js +25 -11
- package/dist/run-session.d.ts +2 -0
- package/dist/run-session.js +25 -4
- package/dist/validate.js +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,7 +90,8 @@ Changes take effect on the next session start. If the pane shows `無法取得
|
|
|
90
90
|
├── agents/<agent>/{AGENT.md?, memory/} # project-level persona (optional) + project memory
|
|
91
91
|
├── shared/common/COMMON.md
|
|
92
92
|
├── locks/<run-id>.json # held while that run is active (up to dispatcher.max_concurrent_runs at once)
|
|
93
|
-
|
|
93
|
+
├── task-memory/<run-id>/{<agent>/, _shared/} # per-run memory, never shared between runs; _shared = the lead's handoff notes for follow-ups
|
|
94
|
+
└── runs/<run-id>/{task.md, log.jsonl, state.json, result.md, done-<n>.md, followups/<n>.md, snapshots/, agents/, mail/{inbox,outbox}/<agent>/}
|
|
94
95
|
```
|
|
95
96
|
|
|
96
97
|
`team.yaml` defines reusable agents; `project.yaml` picks the team and overrides fields. Objects merge field by field, arrays (`can_message`, `owns`) are replaced. Relative paths resolve against the folder of the file they are written in; `~` is allowed.
|
|
@@ -148,6 +149,7 @@ A finished (`done`) run can be reopened with a follow-up request: `agent-lyceum
|
|
|
148
149
|
- **What is kept:** the follow-up is saved as `runs/<run-id>/followups/<n>.md`. Each `done` report is kept as `done-<n>.md`, and `result.md` is always the latest. `state.json` gains `name`, `reopened` and `round_floor` (all optional, so older runs still load). Unresolved blocked integrations stay; the done-contract reminders start afresh for each reopened segment.
|
|
149
150
|
- **Round limit:** `max_rounds` counts from the reopen: `rounds` keeps accumulating, and the limit is the rounds at reopen plus `max_rounds`.
|
|
150
151
|
- **The lead's branch** (runs with a lead worktree): if the branch was already merged into your `HEAD` and the lead worktree is clean and untouched since the run ended, the worktree and the branch (same name, `agent-lyceum/<run-id>/<lead>`) are re-cut from your current `HEAD`, and a line says so. Otherwise the same worktree and branch continue. If a pull request was already recorded (`pr_url`) and the branch was not re-cut, a new `open_pr: true` only pushes ("pull request updated") while that pull request is still open; if it was merged (a squash merge included) or closed, a new pull request is opened instead. If the branch was re-cut, the recorded `pr_url` is cleared (the old URL stays in the run's notes) and a new pull request is opened when asked. agent-lyceum still never merges.
|
|
152
|
+
- **Handoff notes:** before it sends `done`, the lead writes what a follow-up will need into `task-memory/<run-id>/_shared/` in the agent-lyceum home (`MEMORY.md` is the index): decisions and why, interfaces agreed, current state, what is left and the next steps, pitfalls met. Members can read this folder but not write it (a run started with `run --agent` has one agent, which is its own lead and writes it; `call` has none). Once a run is reopened, every wake-up of every agent gets `MEMORY.md` in its prompt (the first 8 KB; a longer index is cut and flagged), and the reopened `task` mail's `Upstream` names it; the first segment's prompts do not contain it. If the lead sends `done` without having changed `MEMORY.md` in that segment, the run is not blocked: a note `shared-not-updated` is added to the run's notes. `clear` deletes the folder with the task memory; `tidy` does not touch it. It holds notes about this task only; lessons that outlive it belong in project or global memory.
|
|
151
153
|
|
|
152
154
|
### Opening a pull request
|
|
153
155
|
|
|
@@ -201,6 +203,7 @@ Default rules (everything else in the repo is unrestricted):
|
|
|
201
203
|
|---|---|
|
|
202
204
|
| own memory dirs, own `outbox/` | writable |
|
|
203
205
|
| any `AGENT.md`, `COMMON.md`, repo `CLAUDE.md`/`AGENTS.md` | read-only; the lead (`can_edit_agent_md`) may edit them |
|
|
206
|
+
| the run's handoff notes `task-memory/<run-id>/_shared/` | the lead writes; every other agent is read-only |
|
|
204
207
|
| other agents' memory/inbox/outbox, `runs/`, config files | not writable |
|
|
205
208
|
|
|
206
209
|
Enforcement is layered because no single mechanism is complete:
|
|
@@ -209,7 +212,7 @@ Enforcement is layered because no single mechanism is complete:
|
|
|
209
212
|
- **Codex**: `-s workspace-write` with `-c sandbox_workspace_write.writable_roots=[...]` (OS-enforced). Codex cannot forbid a single file inside the repo, so repo `CLAUDE.md`/`AGENTS.md` are protected by **post-hoc detection** only.
|
|
210
213
|
- **All runtimes**: protected files are hashed before the run; after each wake-up unauthorized changes are reverted, logged, and reported to the lead; what was written is kept in `runs/<run-id>/violations/` first.
|
|
211
214
|
|
|
212
|
-
`doctor` and `run` print every agent's level per category (`os`, `tool-rules`, `post-hoc`, `prompt-only`) and the paths it can write outside the repo. With `dispatcher.strict: true`, a run is refused unless memory, `AGENT.md
|
|
215
|
+
`doctor` and `run` print every agent's level per category (`os`, `tool-rules`, `post-hoc`, `prompt-only`) and the paths it can write outside the repo. With `dispatcher.strict: true`, a run is refused unless memory, `AGENT.md`, other agents' context and (for non-lead agents) the handoff notes are OS-enforced.
|
|
213
216
|
|
|
214
217
|
Git in agent worktrees: an agent can write its worktree's `.git` pointer file and its private git dir, which could make git read a config or hooks the agent planted. So every git call the dispatcher makes into an agent's worktree pins the git dir, common dir and work tree recorded when the worktree was made, turns off hooks, fsmonitor, external diff and textconv, and refuses a worktree whose git files were redirected or linked elsewhere, whose HEAD is not its own branch, or that contains a nested repository. Claude additionally may not write those pointer files (`denyWrite`); Codex cannot forbid single files and relies on the pinning alone. Residual risks are listed in [spec 15](docs/specs/15-worktree-git-isolation.md#156-殘留風險). This does not claim the dispatcher can never run code an agent planted. A repository using the `reftable` ref backend is refused in worktree modes.
|
|
215
218
|
|
package/README.zh-TW.md
CHANGED
|
@@ -90,8 +90,8 @@ claude --plugin-dir /path/to/agent-lyceum/plugins/status-monitor
|
|
|
90
90
|
├── agents/<agent>/{AGENT.md?, memory/} # 專案層人設(選用)+ 專案記憶
|
|
91
91
|
├── shared/common/COMMON.md
|
|
92
92
|
├── locks/<run-id>.json # 該 run 執行期間持有(同時最多 dispatcher.max_concurrent_runs 個)
|
|
93
|
-
├── task-memory/<run-id
|
|
94
|
-
└── runs/<run-id>/{task.md, log.jsonl, state.json, snapshots/, agents/, mail/{inbox,outbox}/<agent>/}
|
|
93
|
+
├── task-memory/<run-id>/{<agent>/, _shared/} # 任務記憶:每次 run 獨立,不與其他任務共用;_shared 是 lead 為追加要求寫的交接筆記
|
|
94
|
+
└── runs/<run-id>/{task.md, log.jsonl, state.json, result.md, done-<n>.md, followups/<n>.md, snapshots/, agents/, mail/{inbox,outbox}/<agent>/}
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
`team.yaml` 定義可重複使用的 agent;`project.yaml` 選擇團隊並覆寫欄位。物件會逐欄位合併,陣列(`can_message`、`owns`)則整個取代。相對路徑以寫入該路徑的檔案所在資料夾為基準;允許使用 `~`。
|
|
@@ -149,6 +149,7 @@ Agent 欄位:`runtime`(`claude-code`|`codex`;若 `model` 可辨識則可
|
|
|
149
149
|
- **保留什麼:** 追加要求存成 `runs/<run-id>/followups/<n>.md`。每份 `done` 報告保留為 `done-<n>.md`,`result.md` 永遠是最新一份。`state.json` 新增 `name`、`reopened`、`round_floor`(都是選填,舊的 run 照樣能讀)。未解決的 blocked integration 會保留;每段重開的 done 規範提醒重新計算。
|
|
150
150
|
- **輪數上限:** `max_rounds` 從重開時起算:`rounds` 持續累加,上限是重開當下的輪數加 `max_rounds`。
|
|
151
151
|
- **lead 的 branch**(lead 有 worktree 的 run):若該 branch 已合併進你的 `HEAD`,且 lead 的 worktree 乾淨、自 run 結束後沒被動過,worktree 與 branch(同名,`agent-lyceum/<run-id>/<lead>`)會從你目前的 `HEAD` 重新切出,並印一行說明。否則沿用原本的 worktree 與 branch。若已記錄 PR(`pr_url`)且 branch 沒有重切,新的 `open_pr: true` 只會 push(「pull request updated」),而且只在該 PR 仍開啟時;若它已被合併(含 squash merge)或關閉,就改開一個新的 PR;若 branch 重切了,已記錄的 `pr_url` 會被清掉(舊網址留在 run 的 notes),要求時再開一個新的 PR。agent-lyceum 仍然從不合併。
|
|
152
|
+
- **交接筆記:** lead 在送 `done` 之前,把追加要求會用到的內容寫進 agent-lyceum home 裡的 `task-memory/<run-id>/_shared/`(`MEMORY.md` 為索引):做過的決定與理由、介面約定、目前狀態、剩下的事與下一步、踩過的坑。成員可讀不可寫(用 `run --agent` 啟動的 run 只有一個 agent,它就是自己的 lead,所以可寫;`call` 沒有這個資料夾)。run 重開後,每個 agent 的每次喚醒都在 prompt 裡拿到 `MEMORY.md`(前 8 KB,較長的索引會被截斷並標示),重開的 `task` 信的 `Upstream` 也會指向它;第一段的 prompt 不含它。若 lead 在該段沒有更動 `MEMORY.md` 就送出 `done`,run 不會被擋,只會在 run 的 notes 加一筆 `shared-not-updated`。`clear` 會連同任務記憶一併刪除;`tidy` 不會動它。只放這個 task 的筆記,跨 task 的教訓仍放 project 或 global memory。
|
|
152
153
|
|
|
153
154
|
### 開 PR
|
|
154
155
|
|
|
@@ -202,6 +203,7 @@ agents:
|
|
|
202
203
|
|---|---|
|
|
203
204
|
| 自己的記憶資料夾、自己的 `outbox/` | 可寫入 |
|
|
204
205
|
| 任何 `AGENT.md`、`COMMON.md`、repo 的 `CLAUDE.md`/`AGENTS.md` | 唯讀;lead(`can_edit_agent_md`)可編輯 |
|
|
206
|
+
| run 的交接筆記 `task-memory/<run-id>/_shared/` | lead 可寫;其他 agent 唯讀 |
|
|
205
207
|
| 其他 agent 的記憶/inbox/outbox、`runs/`、設定檔 | 不可寫入 |
|
|
206
208
|
|
|
207
209
|
由於沒有單一機制是完整的,防護採分層設計:
|
|
@@ -210,7 +212,7 @@ agents:
|
|
|
210
212
|
- **Codex**:`-s workspace-write` 搭配 `-c sandbox_workspace_write.writable_roots=[...]`(由 OS 強制)。Codex 無法禁止 repo 內的單一檔案,所以 repo 的 `CLAUDE.md`/`AGENTS.md` 只能靠**事後偵測**保護。
|
|
211
213
|
- **所有 runtime**:執行前會對受保護檔案做雜湊;每次喚醒後,未經授權的變更會被還原、記錄,並通報 lead;被寫入的內容會先留存在 `runs/<run-id>/violations/`。
|
|
212
214
|
|
|
213
|
-
`doctor` 與 `run` 會依類別(`os`、`tool-rules`、`post-hoc`、`prompt-only`)印出每個 agent 的防護等級,以及它能寫入 repo 之外的哪些路徑。設定 `dispatcher.strict: true` 時,除非記憶、`AGENT.md
|
|
215
|
+
`doctor` 與 `run` 會依類別(`os`、`tool-rules`、`post-hoc`、`prompt-only`)印出每個 agent 的防護等級,以及它能寫入 repo 之外的哪些路徑。設定 `dispatcher.strict: true` 時,除非記憶、`AGENT.md`、其他 agent 的 context 與(非 lead 的 agent 的)交接筆記都由 OS 強制保護,否則拒絕執行。
|
|
214
216
|
|
|
215
217
|
agent worktree 內的 git:agent 能寫它 worktree 的 `.git` 指標檔與私有 git 目錄,可能讓 git 去讀 agent 埋的設定或 hook。因此 dispatcher 對 agent worktree 的每一次 git 呼叫,都釘住 worktree 建立當下記錄的 git dir、common dir 與 work tree,關閉 hook、fsmonitor、external diff 與 textconv,並拒絕使用 git 檔案被改向或連到別處、HEAD 不是它自己的分支、或內含巢狀 repository 的 worktree。Claude 另外不得寫那幾個指標檔(`denyWrite`);Codex 無法禁止單一檔案,只靠釘住。殘留風險列在 [spec 15](docs/specs/15-worktree-git-isolation.md#156-殘留風險)。這**不**表示 dispatcher 絕不會執行 agent 埋下的程式碼。使用 `reftable` ref 後端的 repository 在 worktree 模式下會被拒絕。
|
|
216
218
|
|
package/dist/adapters/claude.js
CHANGED
|
@@ -62,7 +62,7 @@ export function buildClaudeInvocation(input) {
|
|
|
62
62
|
fs.writeFileSync(systemFile, input.systemPrompt);
|
|
63
63
|
const pol = writePolicy(project, agent);
|
|
64
64
|
const repo = repoDirFor(project, agent.name);
|
|
65
|
-
const addDirs = [...pol.allowDirs, ...pol.allowFiles.map((f) => path.dirname(f))].filter((d) => !(d === repo || d.startsWith(repo + path.sep)));
|
|
65
|
+
const addDirs = [...pol.allowDirs, ...pol.readDirs, ...pol.allowFiles.map((f) => path.dirname(f))].filter((d) => !(d === repo || d.startsWith(repo + path.sep)));
|
|
66
66
|
const args = [
|
|
67
67
|
"-p",
|
|
68
68
|
"--output-format",
|
package/dist/fs-util.d.ts
CHANGED
|
@@ -2,3 +2,11 @@
|
|
|
2
2
|
export declare function atomicWrite(file: string, content: string): void;
|
|
3
3
|
/** Is `cmd` an executable file in one of the PATH directories? (No `which`: it may be missing in slim containers.) */
|
|
4
4
|
export declare function findOnPath(cmd: string, pathVar?: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Up to `limit` bytes of a regular file (symlinks to one are followed), and whether there was more.
|
|
7
|
+
* undefined for anything else (missing, a directory, a FIFO, a device) or on any error: never throws, never reads a huge file whole.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readFileCapped(file: string, limit: number): {
|
|
10
|
+
text: string;
|
|
11
|
+
truncated: boolean;
|
|
12
|
+
} | undefined;
|
package/dist/fs-util.js
CHANGED
|
@@ -22,3 +22,39 @@ export function findOnPath(cmd, pathVar = process.env.PATH ?? "") {
|
|
|
22
22
|
}
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Up to `limit` bytes of a regular file (symlinks to one are followed), and whether there was more.
|
|
27
|
+
* undefined for anything else (missing, a directory, a FIFO, a device) or on any error: never throws, never reads a huge file whole.
|
|
28
|
+
*/
|
|
29
|
+
export function readFileCapped(file, limit) {
|
|
30
|
+
let fd;
|
|
31
|
+
try {
|
|
32
|
+
if (!fs.statSync(file).isFile())
|
|
33
|
+
return undefined;
|
|
34
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK ?? 0));
|
|
35
|
+
if (!fs.fstatSync(fd).isFile())
|
|
36
|
+
return undefined;
|
|
37
|
+
const buf = Buffer.alloc(limit + 1);
|
|
38
|
+
let n = 0;
|
|
39
|
+
while (n < buf.length) {
|
|
40
|
+
const r = fs.readSync(fd, buf, n, buf.length - n, n);
|
|
41
|
+
if (r === 0)
|
|
42
|
+
break;
|
|
43
|
+
n += r;
|
|
44
|
+
}
|
|
45
|
+
if (n <= limit)
|
|
46
|
+
return { text: buf.subarray(0, n).toString("utf8"), truncated: false };
|
|
47
|
+
return { text: buf.subarray(0, limit).toString("utf8"), truncated: true };
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
if (fd !== undefined) {
|
|
54
|
+
try {
|
|
55
|
+
fs.closeSync(fd);
|
|
56
|
+
}
|
|
57
|
+
catch { }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/guard.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { commonFile, mayEditProtected, repoInstructionFiles } from "./policy.js";
|
|
4
|
+
import { commonFile, mayEditProtected, repoInstructionFiles, sharedIndex } from "./policy.js";
|
|
5
5
|
export const MAX_KEEP_BYTES = 1024 * 1024;
|
|
6
6
|
function safeName(file) {
|
|
7
7
|
return file.replace(/[\\/:]/g, "_").replace(/^_+/, "");
|
|
@@ -32,6 +32,9 @@ export class ProtectedGuard {
|
|
|
32
32
|
files.add(commonFile(project));
|
|
33
33
|
for (const f of repoInstructionFiles(project))
|
|
34
34
|
files.add(f);
|
|
35
|
+
const handoff = sharedIndex(project);
|
|
36
|
+
if (handoff)
|
|
37
|
+
files.add(handoff);
|
|
35
38
|
return [...files];
|
|
36
39
|
}
|
|
37
40
|
/** Copy the current protected files into `dir` for the audit trail. */
|
package/dist/mailbox.js
CHANGED
|
@@ -5,7 +5,7 @@ import YAML from "yaml";
|
|
|
5
5
|
import { formatWarning, missingSections, parseSteps } from "./format.js";
|
|
6
6
|
import { atomicWrite } from "./fs-util.js";
|
|
7
7
|
import { deterministicId, fault, RouteJournal, sourceIdOf } from "./message-store.js";
|
|
8
|
-
import { inboxDir, outboxDir } from "./policy.js";
|
|
8
|
+
import { inboxDir, outboxDir, sharedDir } from "./policy.js";
|
|
9
9
|
import { must } from "./assert.js";
|
|
10
10
|
export { atomicWrite };
|
|
11
11
|
export const MESSAGE_TYPES = ["task", "reply", "failure", "done", "ask"];
|
|
@@ -33,6 +33,9 @@ export function parseRaw(text) {
|
|
|
33
33
|
}
|
|
34
34
|
export function ensureProjectDirs(project) {
|
|
35
35
|
fs.mkdirSync(path.join(project.paths.shared, "common"), { recursive: true });
|
|
36
|
+
const handoff = sharedDir(project);
|
|
37
|
+
if (handoff)
|
|
38
|
+
fs.mkdirSync(handoff, { recursive: true });
|
|
36
39
|
for (const name of Object.keys(project.agents)) {
|
|
37
40
|
fs.mkdirSync(path.join(inboxDir(project, name), "read"), { recursive: true });
|
|
38
41
|
fs.mkdirSync(path.join(outboxDir(project, name), "rejected"), { recursive: true });
|
package/dist/policy.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface AgentEnforcement {
|
|
|
6
6
|
memory: Level;
|
|
7
7
|
agentMd: Level;
|
|
8
8
|
otherContext: Level;
|
|
9
|
+
shared: Level;
|
|
9
10
|
repoInstructions: Level;
|
|
10
11
|
owns: Level;
|
|
11
12
|
/** Absolute paths outside the repo this agent may write. */
|
|
@@ -15,6 +16,8 @@ export interface AgentEnforcement {
|
|
|
15
16
|
export interface WritePolicy {
|
|
16
17
|
/** Directories the agent may write to (besides the repo). */
|
|
17
18
|
allowDirs: string[];
|
|
19
|
+
/** Directories the agent may read besides the repo and its writable ones, but must not write (the run's handoff notes, for everyone but the lead). */
|
|
20
|
+
readDirs: string[];
|
|
18
21
|
/** Individual files the agent may write (outside allowDirs). */
|
|
19
22
|
allowFiles: string[];
|
|
20
23
|
/** Files/dirs that must stay unwritable (checked even inside the repo). */
|
|
@@ -29,6 +32,12 @@ export declare function inboxDir(project: ResolvedProject, agent: string): strin
|
|
|
29
32
|
/** The repo's own CLAUDE.md/AGENTS.md: in the lead's worktree when it has one (concurrent runs), else in the repo. */
|
|
30
33
|
export declare function repoInstructionFiles(project: ResolvedProject): string[];
|
|
31
34
|
export declare function memoryDirs(a: ResolvedAgent): string[];
|
|
35
|
+
/** Folder name of a run's handoff notes inside `task-memory/<run-id>/`. Agent names start with a letter or digit, so it cannot be an agent's own folder. */
|
|
36
|
+
export declare const SHARED_DIR = "_shared";
|
|
37
|
+
/** The run's handoff folder: read by every agent, written only by the lead. Undefined outside a run and for `call`. */
|
|
38
|
+
export declare function sharedDir(project: ResolvedProject): string | undefined;
|
|
39
|
+
/** The index of the handoff notes: the one file shown in prompts, and the one the guard watches. */
|
|
40
|
+
export declare function sharedIndex(project: ResolvedProject): string | undefined;
|
|
32
41
|
/** The repo directory this agent works in: its own worktree, or the main repo. */
|
|
33
42
|
export declare function repoDirFor(project: ResolvedProject, agent: string): string;
|
|
34
43
|
export declare function writePolicy(project: ResolvedProject, agent: ResolvedAgent): WritePolicy;
|
package/dist/policy.js
CHANGED
|
@@ -19,6 +19,19 @@ export function repoInstructionFiles(project) {
|
|
|
19
19
|
export function memoryDirs(a) {
|
|
20
20
|
return [a.memory.global, a.memory.project, a.memory.task].filter((x) => !!x);
|
|
21
21
|
}
|
|
22
|
+
/** Folder name of a run's handoff notes inside `task-memory/<run-id>/`. Agent names start with a letter or digit, so it cannot be an agent's own folder. */
|
|
23
|
+
export const SHARED_DIR = "_shared";
|
|
24
|
+
/** The run's handoff folder: read by every agent, written only by the lead. Undefined outside a run and for `call`. */
|
|
25
|
+
export function sharedDir(project) {
|
|
26
|
+
if (!project.run || project.solo === "call")
|
|
27
|
+
return undefined;
|
|
28
|
+
return path.join(project.paths.taskMemory, project.run.id, SHARED_DIR);
|
|
29
|
+
}
|
|
30
|
+
/** The index of the handoff notes: the one file shown in prompts, and the one the guard watches. */
|
|
31
|
+
export function sharedIndex(project) {
|
|
32
|
+
const dir = sharedDir(project);
|
|
33
|
+
return dir ? path.join(dir, "MEMORY.md") : undefined;
|
|
34
|
+
}
|
|
22
35
|
const RUN_ENTRIES = ["state.json", "log.jsonl", "log.1.jsonl", "result.md", "task.md", "snapshots", "violations", "agents", "mail"];
|
|
23
36
|
const MAIL_ENTRIES = ["inbox", "claims", "attempts", "journal.jsonl"];
|
|
24
37
|
function entries(dir) {
|
|
@@ -54,10 +67,20 @@ export function writePolicy(project, agent) {
|
|
|
54
67
|
const isLead = agent.name === project.lead && !project.solo;
|
|
55
68
|
const ws = project.workspaces?.[agent.name];
|
|
56
69
|
// A `call` has no mailboxes at all; a solo member of a run keeps its outbox to send `done`.
|
|
57
|
-
const
|
|
70
|
+
const shared = sharedDir(project);
|
|
71
|
+
const sharedIdx = sharedIndex(project);
|
|
72
|
+
const mayWriteShared = !!shared && agent.name === project.lead;
|
|
73
|
+
const allowDirs = [
|
|
74
|
+
...memoryDirs(agent),
|
|
75
|
+
...(project.solo === "call" ? [] : [outboxDir(project, agent.name)]),
|
|
76
|
+
...(ws ? [ws.gitDir] : []),
|
|
77
|
+
...(shared && mayWriteShared ? [shared] : []),
|
|
78
|
+
];
|
|
58
79
|
const allowFiles = [];
|
|
59
80
|
if (isLead)
|
|
60
81
|
allowFiles.push(commonFile(project));
|
|
82
|
+
if (mayWriteShared && sharedIdx)
|
|
83
|
+
allowFiles.push(sharedIdx);
|
|
61
84
|
if (agent.canEditAgentMd) {
|
|
62
85
|
for (const a of Object.values(project.agents))
|
|
63
86
|
allowFiles.push(a.agentMd);
|
|
@@ -81,6 +104,8 @@ export function writePolicy(project, agent) {
|
|
|
81
104
|
addDeny(commonFile(project));
|
|
82
105
|
for (const f of repoInstructionFiles(project))
|
|
83
106
|
addDeny(f);
|
|
107
|
+
if (shared && !mayWriteShared)
|
|
108
|
+
deny.push(shared);
|
|
84
109
|
deny.push(...runDenyPaths(project), project.paths.config, path.join(project.home, "team.yaml"));
|
|
85
110
|
// In a worktree: other agents' checkouts are off limits, and so are this checkout's own copies of the protected files.
|
|
86
111
|
for (const [name, other] of Object.entries(project.workspaces ?? {}))
|
|
@@ -95,6 +120,7 @@ export function writePolicy(project, agent) {
|
|
|
95
120
|
deny.push(path.join(ws.root, ".git"), ...["commondir", "config.worktree", "gitdir"].map((f) => path.join(ws.gitDir, f)));
|
|
96
121
|
return {
|
|
97
122
|
allowDirs,
|
|
123
|
+
readDirs: shared && !mayWriteShared ? [shared] : [],
|
|
98
124
|
allowFiles,
|
|
99
125
|
deny: [...new Set(deny)],
|
|
100
126
|
owns: agent.owns,
|
|
@@ -118,17 +144,22 @@ export function enforcementFor(project, agent) {
|
|
|
118
144
|
const os = osSandboxAvailable();
|
|
119
145
|
const notes = [];
|
|
120
146
|
const parallel = project.dispatcher.max_parallel > 1;
|
|
147
|
+
const isLeadAgent = agent.name === project.lead;
|
|
121
148
|
let e;
|
|
122
149
|
if (agent.runtime === "claude-code") {
|
|
123
150
|
const lvl = os ? "os" : "tool-rules";
|
|
124
|
-
e = { memory: lvl, agentMd: lvl, otherContext: lvl, repoInstructions: lvl, owns: parallel ? "tool-rules" : "n/a" };
|
|
151
|
+
e = { memory: lvl, agentMd: lvl, otherContext: lvl, shared: isLeadAgent ? "n/a" : lvl, repoInstructions: lvl, owns: parallel ? "tool-rules" : "n/a" };
|
|
125
152
|
if (!os)
|
|
126
153
|
notes.push("OS sandbox unavailable (Windows, or Linux without bubblewrap): Bash can bypass Edit rules.");
|
|
154
|
+
if (!isLeadAgent && !os)
|
|
155
|
+
notes.push("Without the OS sandbox Bash can write _shared; only the handoff index (_shared/MEMORY.md) is checked after each wake-up.");
|
|
127
156
|
}
|
|
128
157
|
else {
|
|
129
158
|
const lvl = os ? "os" : "prompt-only";
|
|
130
|
-
e = { memory: lvl, agentMd: lvl, otherContext: lvl, repoInstructions: "post-hoc", owns: parallel ? "prompt-only" : "n/a" };
|
|
159
|
+
e = { memory: lvl, agentMd: lvl, otherContext: lvl, shared: isLeadAgent ? "n/a" : os ? "os" : "post-hoc", repoInstructions: "post-hoc", owns: parallel ? "prompt-only" : "n/a" };
|
|
131
160
|
notes.push("Codex cannot forbid single files inside the repo; CLAUDE.md/AGENTS.md changes are detected and reverted after each wake-up.");
|
|
161
|
+
if (!isLeadAgent && !os)
|
|
162
|
+
notes.push("Without the OS sandbox only the handoff index (_shared/MEMORY.md) is checked after each wake-up; other files in _shared are not protected.");
|
|
132
163
|
if (agent.canEditAgentMd)
|
|
133
164
|
notes.push("can_edit_agent_md widens Codex writable roots to whole agent directories.");
|
|
134
165
|
}
|
package/dist/prompt.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Message } from "./mailbox.js";
|
|
|
3
3
|
import type { Layer, MemoryFile, MissingPath, ProjectChanges } from "./memory-tidy.js";
|
|
4
4
|
export declare const COMMON_LIMIT: number;
|
|
5
5
|
export declare const MEMORY_INDEX_LIMIT: number;
|
|
6
|
+
export declare const HANDOFF_LIMIT: number;
|
|
6
7
|
export declare const LEAD_BATCH_LIMIT = 5;
|
|
7
8
|
export declare function readPersona(agent: ResolvedAgent): string;
|
|
8
9
|
/** The protocol of an agent working alone: no teammates, no ask. `run --agent` still ends with a `done` mail; `call` just answers. */
|
|
@@ -13,7 +14,7 @@ export declare function buildCallUserPrompt(agent: ResolvedAgent, task: string):
|
|
|
13
14
|
export declare function buildSystemPrompt(project: ResolvedProject, agent: ResolvedAgent): string;
|
|
14
15
|
/** Messages one wake-up handles: the oldest one, or for the lead a run of `reply`/`failure` mail (up to LEAD_BATCH_LIMIT) so it decides once on all of it. */
|
|
15
16
|
export declare function pickMessages(project: ResolvedProject, agent: ResolvedAgent, queue: Message[]): Message[];
|
|
16
|
-
export declare function buildUserPrompt(project: ResolvedProject, agent: ResolvedAgent, unread: Message[], handleCount?: number): string;
|
|
17
|
+
export declare function buildUserPrompt(project: ResolvedProject, agent: ResolvedAgent, unread: Message[], handleCount?: number, handoff?: boolean): string;
|
|
17
18
|
export interface TidyContext {
|
|
18
19
|
layer: Layer;
|
|
19
20
|
/** The memory directory being tidied. */
|
package/dist/prompt.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { readFileCapped } from "./fs-util.js";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { REQUIRED_SECTIONS } from "./format.js";
|
|
4
|
-
import { commonFile, memoryDirs, outboxDir, repoDirFor } from "./policy.js";
|
|
5
|
+
import { commonFile, memoryDirs, outboxDir, repoDirFor, sharedDir, sharedIndex } from "./policy.js";
|
|
5
6
|
function memoryLabel(agent, dir) {
|
|
6
7
|
if (dir === agent.memory.task)
|
|
7
8
|
return "task (this task only, isolated)";
|
|
@@ -9,16 +10,10 @@ function memoryLabel(agent, dir) {
|
|
|
9
10
|
}
|
|
10
11
|
export const COMMON_LIMIT = 8 * 1024;
|
|
11
12
|
export const MEMORY_INDEX_LIMIT = 4 * 1024;
|
|
13
|
+
export const HANDOFF_LIMIT = COMMON_LIMIT;
|
|
12
14
|
export const LEAD_BATCH_LIMIT = 5;
|
|
13
15
|
const BATCHABLE = new Set(["reply", "failure"]);
|
|
14
|
-
|
|
15
|
-
if (!fs.existsSync(file))
|
|
16
|
-
return undefined;
|
|
17
|
-
const buf = fs.readFileSync(file);
|
|
18
|
-
if (buf.length <= limit)
|
|
19
|
-
return { text: buf.toString("utf8"), truncated: false };
|
|
20
|
-
return { text: buf.subarray(0, limit).toString("utf8"), truncated: true };
|
|
21
|
-
}
|
|
16
|
+
const readCapped = readFileCapped;
|
|
22
17
|
/** A PR only needs to say agent-lyceum opened it; no agent or role names. */
|
|
23
18
|
const PR_ATTRIBUTION_RULE = "- If you open a pull request, end its description with a line saying it was opened by agent-lyceum (e.g. `🤖 Opened by agent-lyceum`). Do not name any agent or role in it.";
|
|
24
19
|
/** A tool call that never returns (a server or daemon started in the foreground) burns the whole wake-up timeout and the finished work is thrown away with it. */
|
|
@@ -28,6 +23,14 @@ const OPEN_PR_RULE = "Add the frontmatter line `open_pr: true` to `done` only wh
|
|
|
28
23
|
export function readPersona(agent) {
|
|
29
24
|
return fs.readFileSync(agent.agentMd, "utf8").trim();
|
|
30
25
|
}
|
|
26
|
+
/** What the lead (or a solo agent) is told about the handoff notes: they are what a reopened task starts from. */
|
|
27
|
+
function handoffRule(dir) {
|
|
28
|
+
return [
|
|
29
|
+
"",
|
|
30
|
+
"## Handoff for follow-ups",
|
|
31
|
+
`This task may be reopened later with a follow-up request, and the agents woken then start with fresh sessions. Before you send \`done\`, write what they need to carry on into \`${dir}\`: keep a \`MEMORY.md\` index there (one line per entry, pointing at a file) and record the decisions made and why, the interfaces agreed, the current state, what is left and what to do next, and the pitfalls met. Your teammates can read this folder but cannot write it; if a report from a teammate holds something worth keeping, add it yourself. Only the index is shown in prompts, and only the first ${HANDOFF_LIMIT / 1024} KB of it, so keep it short. These notes are about this task only; lessons that outlive it belong in your project or global memory.`,
|
|
32
|
+
];
|
|
33
|
+
}
|
|
31
34
|
/** The protocol of an agent working alone: no teammates, no ask. `run --agent` still ends with a `done` mail; `call` just answers. */
|
|
32
35
|
export function buildSoloSystemPrompt(project, agent) {
|
|
33
36
|
const isCall = project.solo === "call";
|
|
@@ -38,6 +41,9 @@ export function buildSoloSystemPrompt(project, agent) {
|
|
|
38
41
|
if (!isCall) {
|
|
39
42
|
lines.push("## Finishing", `When the job is finished, create a Markdown file (any name ending in \`.md\`) directly inside \`${outboxDir(project, agent.name)}\` with this exact shape:`, "", "```", "---", "type: done", "subject: <short subject>", "outcome: completed|partial|blocked|failed", "---", "", "## Result", "## Files", "## Verification", "## Not done", "```", "", "That ends the run. `## Result` is the actual deliverable or conclusion (not just \"done\"), `## Files` the paths you created or changed, `## Verification` what you ran or checked and what it showed, `## Not done` anything skipped or unverified (or `None`). Only `completed` means success and it needs all four headings; if work remains or is blocked say `partial`/`blocked`/`failed` (those need only `## Result` and `## Not done`). A summary in your reply text is never delivered: without a `done` mail the run ends as idle with no report. " + OPEN_PR_RULE);
|
|
40
43
|
}
|
|
44
|
+
const shared = sharedDir(project);
|
|
45
|
+
if (!isCall && shared)
|
|
46
|
+
lines.push(...handoffRule(shared));
|
|
41
47
|
lines.push("", "## Memory");
|
|
42
48
|
const mems = memoryDirs(agent);
|
|
43
49
|
if (mems.length === 0)
|
|
@@ -111,7 +117,7 @@ export function buildSystemPrompt(project, agent) {
|
|
|
111
117
|
"Your final `done` mail must also include the full `## Steps` checklist. Tick only steps that were actually done and verified; leave undone ones",
|
|
112
118
|
"(e.g. build, app start, commit) unticked and say so in the mail instead of reporting them as finished.",
|
|
113
119
|
]
|
|
114
|
-
: []), "", "## Memory");
|
|
120
|
+
: []), ...(isLead && sharedDir(project) ? handoffRule(sharedDir(project)) : []), "", "## Memory");
|
|
115
121
|
const mems = memoryDirs(agent);
|
|
116
122
|
if (mems.length === 0)
|
|
117
123
|
lines.push("You have no long-term memory directory.");
|
|
@@ -143,7 +149,7 @@ export function pickMessages(project, agent, queue) {
|
|
|
143
149
|
}
|
|
144
150
|
return run;
|
|
145
151
|
}
|
|
146
|
-
export function buildUserPrompt(project, agent, unread, handleCount = 1) {
|
|
152
|
+
export function buildUserPrompt(project, agent, unread, handleCount = 1, handoff = false) {
|
|
147
153
|
const parts = [];
|
|
148
154
|
const common = readCapped(commonFile(project), COMMON_LIMIT);
|
|
149
155
|
parts.push("# Shared team context (COMMON.md)");
|
|
@@ -155,6 +161,14 @@ export function buildUserPrompt(project, agent, unread, handleCount = 1) {
|
|
|
155
161
|
parts.push(`\n[COMMON.md was truncated at ${COMMON_LIMIT} bytes — the lead should condense it.]`);
|
|
156
162
|
}
|
|
157
163
|
}
|
|
164
|
+
const handoffFile = handoff ? sharedIndex(project) : undefined;
|
|
165
|
+
const notes = handoffFile ? readCapped(handoffFile, HANDOFF_LIMIT) : undefined;
|
|
166
|
+
if (handoffFile && notes?.text.trim()) {
|
|
167
|
+
parts.push("", `# Handoff notes from earlier segments of this task (${handoffFile})`, notes.text.trim());
|
|
168
|
+
if (notes.truncated)
|
|
169
|
+
parts.push(`\n[handoff index truncated at ${HANDOFF_LIMIT} bytes — the lead should condense it.]`);
|
|
170
|
+
parts.push("The other files in that folder are there to read on demand.");
|
|
171
|
+
}
|
|
158
172
|
for (const dir of memoryDirs(agent)) {
|
|
159
173
|
const idx = readCapped(path.join(dir, "MEMORY.md"), MEMORY_INDEX_LIMIT);
|
|
160
174
|
const label = memoryLabel(agent, dir).split(" ")[0];
|
package/dist/run-session.d.ts
CHANGED
|
@@ -59,6 +59,8 @@ export interface RunOptions {
|
|
|
59
59
|
export declare class RunSession {
|
|
60
60
|
private readonly opts;
|
|
61
61
|
private readonly resume;
|
|
62
|
+
/** The handoff index as this segment started; the lead is noted when it is the same at `done`. */
|
|
63
|
+
private readonly sharedAtStart;
|
|
62
64
|
private readonly invoke;
|
|
63
65
|
private readonly say;
|
|
64
66
|
private readonly cfg;
|
package/dist/run-session.js
CHANGED
|
@@ -9,9 +9,9 @@ import { abandonClaim, attemptLogDir, sourceIdOf, beginAttempt, claimMessages, c
|
|
|
9
9
|
import { isInside } from "./paths.js";
|
|
10
10
|
import { createRunLog } from "./run-log.js";
|
|
11
11
|
import { endOutcome, resolveDoneOutcome } from "./run-outcome.js";
|
|
12
|
-
import { outboxDir, ownsDirs } from "./policy.js";
|
|
12
|
+
import { outboxDir, ownsDirs, sharedIndex } from "./policy.js";
|
|
13
13
|
import { buildSystemPrompt, buildUserPrompt, pickMessages } from "./prompt.js";
|
|
14
|
-
import { atomicWrite } from "./fs-util.js";
|
|
14
|
+
import { atomicWrite, readFileCapped } from "./fs-util.js";
|
|
15
15
|
import { assertWorktreeRunnable, collectAgentChanges, dirtyPaths, existingAgentWorkspace, finishLeadBranch, integrateAgentChanges, prepareAgentWorkspace, recutLeadWorkspace, refusedIntegration, resolveWorkspaceMode, snapshotBase, snapshotHead, toplevel } from "./worktree.js";
|
|
16
16
|
import { bindRunProject, newRunId, newRunState, roundLimit, saveRunState } from "./run-store.js";
|
|
17
17
|
import { soloProject } from "./solo.js";
|
|
@@ -24,6 +24,14 @@ export const RESULT_FILE = "result.md";
|
|
|
24
24
|
export const MAX_DONE_REJECTIONS = 2;
|
|
25
25
|
/** How many times a malformed `ask` goes back to its sender before the lead is told instead. */
|
|
26
26
|
export const MAX_ASK_REJECTIONS = 2;
|
|
27
|
+
/** How much of the handoff index is compared; the prompts show far less. */
|
|
28
|
+
const INDEX_COMPARE_LIMIT = 1024 * 1024;
|
|
29
|
+
/** The text of a file, "" when it is missing, undefined when there is no such file for this run (no handoff folder). */
|
|
30
|
+
function readIndex(file) {
|
|
31
|
+
if (!file)
|
|
32
|
+
return undefined;
|
|
33
|
+
return readFileCapped(file, INDEX_COMPARE_LIMIT)?.text ?? "";
|
|
34
|
+
}
|
|
27
35
|
/** Prompt for a repeat attempt: the earlier one failed and may have changed things already. */
|
|
28
36
|
function retryPrompt(prompt, error) {
|
|
29
37
|
return `${prompt}\n\n# Retry notice\nYour previous attempt at this wake-up failed${error ? ` (${error})` : ""} after possibly changing files already. Its mail was discarded. Check the current state of the repository before redoing the work, and send the mail again.`;
|
|
@@ -31,6 +39,8 @@ function retryPrompt(prompt, error) {
|
|
|
31
39
|
export class RunSession {
|
|
32
40
|
opts;
|
|
33
41
|
resume;
|
|
42
|
+
/** The handoff index as this segment started; the lead is noted when it is the same at `done`. */
|
|
43
|
+
sharedAtStart;
|
|
34
44
|
invoke;
|
|
35
45
|
say;
|
|
36
46
|
cfg;
|
|
@@ -176,12 +186,14 @@ export class RunSession {
|
|
|
176
186
|
const legacy = path.join(runDir, RESULT_FILE);
|
|
177
187
|
if (!fs.existsSync(prev) && fs.existsSync(legacy))
|
|
178
188
|
fs.copyFileSync(legacy, prev);
|
|
189
|
+
const handoffIdx = sharedIndex(project);
|
|
190
|
+
const handoffLine = handoffIdx && fs.existsSync(handoffIdx) ? ` The handoff notes written for follow-ups are in \`${handoffIdx}\`.` : "";
|
|
179
191
|
deliver(project, {
|
|
180
192
|
from: "user",
|
|
181
193
|
to: project.lead,
|
|
182
194
|
type: "task",
|
|
183
195
|
subject: opts.followup.text.split("\n").find((l) => l.trim())?.slice(0, 80) ?? "Follow-up",
|
|
184
|
-
body: `## Goal\n${opts.followup.text.trimEnd()}\n\n## Acceptance criteria\nAs stated in the goal; the earlier result still holds unless the goal changes it.\n\n## Scope\nThis run's earlier work and files.\n\n## Upstream\nThe previous final report is in \`${path.join(runDir, `done-${n}.md`)}\`; the original task is in \`${path.join(runDir, "task.md")}
|
|
196
|
+
body: `## Goal\n${opts.followup.text.trimEnd()}\n\n## Acceptance criteria\nAs stated in the goal; the earlier result still holds unless the goal changes it.\n\n## Scope\nThis run's earlier work and files.\n\n## Upstream\nThe previous final report is in \`${path.join(runDir, `done-${n}.md`)}\`; the original task is in \`${path.join(runDir, "task.md")}\`.${handoffLine}\n`,
|
|
185
197
|
});
|
|
186
198
|
log("reopen", { n });
|
|
187
199
|
}
|
|
@@ -207,6 +219,8 @@ export class RunSession {
|
|
|
207
219
|
this.log = log;
|
|
208
220
|
this.guard = guard;
|
|
209
221
|
this.state = state;
|
|
222
|
+
// Only a segment that starts here has a baseline: a fresh run or a follow-up reopen. A plain resume continues one begun by an earlier process.
|
|
223
|
+
this.sharedAtStart = !resume || opts.followup ? readIndex(sharedIndex(project)) : undefined;
|
|
210
224
|
}
|
|
211
225
|
/** A waiting run only continues once every unanswered question has a valid answer. */
|
|
212
226
|
static assertAnswered(runDir, runId) {
|
|
@@ -283,7 +297,7 @@ export class RunSession {
|
|
|
283
297
|
workDir,
|
|
284
298
|
workspace: spaces[agent.name],
|
|
285
299
|
systemPrompt: buildSystemPrompt(aproject, agent),
|
|
286
|
-
userPrompt: buildUserPrompt(project, agent, queue, unread.length),
|
|
300
|
+
userPrompt: buildUserPrompt(project, agent, queue, unread.length, (state.reopened?.length ?? 0) > 0),
|
|
287
301
|
timeoutSec: cfg.wake_timeout_sec,
|
|
288
302
|
};
|
|
289
303
|
let result;
|
|
@@ -508,6 +522,13 @@ export class RunSession {
|
|
|
508
522
|
log("rejected", r);
|
|
509
523
|
}
|
|
510
524
|
if (route.done) {
|
|
525
|
+
const handoffIdx = sharedIndex(project);
|
|
526
|
+
const nowShared = handoffIdx && this.sharedAtStart !== undefined ? readIndex(handoffIdx) : undefined;
|
|
527
|
+
if (nowShared !== undefined && nowShared === this.sharedAtStart) {
|
|
528
|
+
this.note(nowShared.trim()
|
|
529
|
+
? `shared-not-updated: the lead did not change the handoff notes (${handoffIdx}) in this segment; a follow-up will see them as they are.`
|
|
530
|
+
: `shared-not-updated: the lead did not write handoff notes (${handoffIdx}) in this segment, so a follow-up will start without handoff notes.`);
|
|
531
|
+
}
|
|
511
532
|
this.doneMessage = { subject: route.done.subject, body: route.done.body };
|
|
512
533
|
this.openPrRequested = route.done.meta.open_pr === true;
|
|
513
534
|
const resolved = resolveDoneOutcome(must(contract, "completion contract of the done report"), state.done_rejections ?? 0, state.blocked_integrations ?? []);
|
package/dist/validate.js
CHANGED
|
@@ -100,8 +100,8 @@ export function validateProject(project) {
|
|
|
100
100
|
const enforcement = Object.values(project.agents).map((a) => enforcementFor(project, a));
|
|
101
101
|
if (project.dispatcher.strict) {
|
|
102
102
|
for (const e of enforcement) {
|
|
103
|
-
for (const k of ["memory", "agentMd", "otherContext"]) {
|
|
104
|
-
if (e[k] !== "os")
|
|
103
|
+
for (const k of ["memory", "agentMd", "otherContext", "shared"]) {
|
|
104
|
+
if (e[k] !== "os" && !(k === "shared" && e[k] === "n/a"))
|
|
105
105
|
err(`strict: agent "${e.agent}" ${k} protection is "${e[k]}", not OS-enforced.`);
|
|
106
106
|
}
|
|
107
107
|
}
|
|
@@ -109,10 +109,10 @@ export function validateProject(project) {
|
|
|
109
109
|
return { issues, enforcement, ok: !issues.some((i) => i.level === "error") };
|
|
110
110
|
}
|
|
111
111
|
export function formatEnforcement(list) {
|
|
112
|
-
const header = ["agent", "runtime", "memory", "AGENT.md", "others' ctx", "repo CLAUDE/AGENTS", "owns"];
|
|
112
|
+
const header = ["agent", "runtime", "memory", "AGENT.md", "others' ctx", "handoff", "repo CLAUDE/AGENTS", "owns"];
|
|
113
113
|
const rows = [header];
|
|
114
114
|
for (const e of list)
|
|
115
|
-
rows.push([e.agent, e.runtime, e.memory, e.agentMd, e.otherContext, e.repoInstructions, e.owns]);
|
|
115
|
+
rows.push([e.agent, e.runtime, e.memory, e.agentMd, e.otherContext, e.shared, e.repoInstructions, e.owns]);
|
|
116
116
|
const w = header.map((_, i) => Math.max(...rows.map((r) => (r[i] ?? "").length)));
|
|
117
117
|
const lines = rows.map((r) => r.map((c, i) => c.padEnd(w[i] ?? 0)).join(" ").trimEnd());
|
|
118
118
|
lines.splice(1, 0, w.map((n) => "-".repeat(n)).join(" "));
|