agent-lyceum 0.8.4 → 0.9.1
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 +18 -20
- package/README.zh-TW.md +18 -20
- package/dist/cli.js +106 -136
- package/dist/doctor.js +1 -1
- package/dist/project-lock.d.ts +1 -1
- package/dist/project-lock.js +2 -2
- package/dist/prompt.js +1 -1
- package/dist/run-outcome.js +1 -1
- package/dist/status.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ The tool keeps **its own data out of your repo**: configuration, mailboxes, logs
|
|
|
11
11
|
```bash
|
|
12
12
|
npx agent-lyceum init # create the home + global agent library
|
|
13
13
|
npx agent-lyceum project add web --dir ~/code/web-app
|
|
14
|
-
npx agent-lyceum
|
|
14
|
+
npx agent-lyceum doctor --project web # check config + show enforcement levels
|
|
15
15
|
npx agent-lyceum run --task-file spec.md # inside the repo, the project is auto-detected
|
|
16
16
|
npx agent-lyceum status
|
|
17
17
|
```
|
|
@@ -22,23 +22,21 @@ Requires Node 20+, plus `claude` and/or `codex` on your `PATH` (already logged i
|
|
|
22
22
|
|
|
23
23
|
## Commands
|
|
24
24
|
|
|
25
|
-
| Command | What it does |
|
|
26
|
-
|
|
27
|
-
| `init [-y]` | Create the home and the global agent library. [details](docs/commands.md#init) |
|
|
28
|
-
| `project add <name> --dir <repo>` | Register a project. [details](docs/commands.md#project-add) |
|
|
29
|
-
| `project list` / `project remove <name> [--purge]` | List or unregister projects. [details](docs/commands.md#project-list-and-remove) |
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `resume [run-id] [-p name]` | Continue an interrupted or failed run. [details](docs/commands.md#resume) |
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
| `memory restore <timestamp> -p name --agent a` | Undo a tidy: move its archived files back. [details](docs/commands.md#memory-restore) |
|
|
41
|
-
| `unlock [-p name] --force` | Remove the project lock a crashed run left behind. [details](docs/commands.md#unlock) |
|
|
25
|
+
| Layer | Command | What it does |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| Project | `init [-y]` | Create the home and the global agent library. [details](docs/commands.md#init) |
|
|
28
|
+
| Project | `project add <name> --dir <repo>` | Register a project. [details](docs/commands.md#project-add) |
|
|
29
|
+
| Project | `project list` / `project remove <name> [--purge]` | List or unregister projects. [details](docs/commands.md#project-list-and-remove) |
|
|
30
|
+
| Diagnose | `doctor [-p name] [--task-file f] [--resolved] [--json]` | Check config (with enforcement levels), git, the lock and runtime CLIs without running agents; `--resolved` prints every effective setting and its source. [details](docs/commands.md#doctor) |
|
|
31
|
+
| Diagnose | `status [-p name] [--monitor]` | Show agents, mail, the current wake-up and past runs (`--json`, `--monitor`). [details](docs/commands.md#status) |
|
|
32
|
+
| Run | `run ["task"] [--task-file f] [--agent a] [-p name]` | Give the task to the lead and run until done; with `--agent`, to one member alone. [details](docs/commands.md#run) |
|
|
33
|
+
| Run | `resume [run-id] [--answer] [-p name]` | Continue an interrupted or failed run; `--answer` first answers the questions it is waiting on. [details](docs/commands.md#resume) |
|
|
34
|
+
| Run | `clear <run-id> [-p name] [--dry-run] [--keep-worktrees]` / `clear --lock --force` | Delete a run with its task memory and worktrees; or remove the lock a crashed run left behind. [details](docs/commands.md#clear) |
|
|
35
|
+
| Run | `call <agent> ["task"] [--task-file f] [--dir d]` | Call one global agent on its own, outside any project; prints its answer. [details](docs/commands.md#call) |
|
|
36
|
+
| Run | `memory tidy [-p name] [--agent a] [--layer project\|global] [--dry-run]` | Have agents tidy their own memory (manual only; archives, never deletes). [details](docs/commands.md#memory-tidy) |
|
|
37
|
+
| Run | `memory restore <timestamp> -p name --agent a` | Undo a tidy: move its archived files back. [details](docs/commands.md#memory-restore) |
|
|
38
|
+
|
|
39
|
+
`agent-lyceum --help` lists each command with a one-line summary; `agent-lyceum <command> --help` prints its full description and options.
|
|
42
40
|
|
|
43
41
|
Ctrl-C cancels a run cleanly and it can be continued with `resume` ([Stopping a run](docs/commands.md#stopping-a-run)). `run` and `resume` exit `0` only when the lead reported `completed`, `2` for partial or blocked, `1` for failed, `3` while waiting for your answers, `130` for cancelled ([Exit codes](docs/commands.md#exit-codes)). Without `-p`, the project is inferred from the current directory ([Choosing the project](docs/commands.md#choosing-the-project)).
|
|
44
42
|
|
|
@@ -111,7 +109,7 @@ agents:
|
|
|
111
109
|
|
|
112
110
|
Agent fields: `runtime` (`claude-code`|`codex`; optional when `model` is recognizable: `opus`/`sonnet`/`haiku`/`claude-*` → Claude Code, `gpt-*`/`o3`/`*codex*` → Codex; precedence: project runtime > project model > global runtime > global model), `model`, `effort` (Claude Code: `low`|`medium`|`high`|`xhigh`|`max` via `--effort`; Codex: `minimal`|`low`|`medium`|`high`|`xhigh` via `model_reasoning_effort`; unset = CLI default), `agent_md`, `memory.global` / `memory.project`, `resume`, `can_message` (`all` or list; default `[lead]`, lead default `all`), `can_edit_agent_md` (default only the lead), `can_ask_user` (may send `type: ask` mail to you and pause the run for an answer; default only the lead), `allow_web` (`none`|`search`|`fetch`; what the agent may use of the web, off unless set; see "Web access" below), `owns` (repo globs).
|
|
113
111
|
|
|
114
|
-
Rules checked by `
|
|
112
|
+
Rules checked by `doctor`: ≥ 2 agents (the lead and one member; the template has three), the lead is a listed agent, every agent has a runtime (explicit or inferred from `model`) and an existing `AGENT.md`, `effort` is valid for the agent's runtime (warning if `runtime` contradicts a recognizable `model`), `can_message` targets exist, memory dirs don't overlap, and with `max_parallel > 1` every non-lead agent needs non-overlapping `owns` and the repo must be a git repository.
|
|
115
113
|
|
|
116
114
|
## How a run works
|
|
117
115
|
|
|
@@ -175,6 +173,6 @@ Enforcement is layered because no single mechanism is complete:
|
|
|
175
173
|
- **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.
|
|
176
174
|
- **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.
|
|
177
175
|
|
|
178
|
-
`
|
|
176
|
+
`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` and other agents' context are OS-enforced.
|
|
179
177
|
|
|
180
178
|
Known gaps: Claude Code's built-in Edit/Write tools are not sandboxed (covered by the `Edit` rules); Codex MCP tools and hooks run outside its sandbox; `--dangerously-bypass-approvals-and-sandbox` / `danger-full-access` disable everything. With `can_edit_agent_md`, a Codex lead's writable roots widen to whole agent directories.
|
package/README.zh-TW.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
```bash
|
|
12
12
|
npx agent-lyceum init # 建立 home 與全域 agent 庫
|
|
13
13
|
npx agent-lyceum project add web --dir ~/code/web-app
|
|
14
|
-
npx agent-lyceum
|
|
14
|
+
npx agent-lyceum doctor --project web # 檢查設定並顯示各項防護等級
|
|
15
15
|
npx agent-lyceum run --task-file spec.md # 在 repo 內執行時會自動偵測專案
|
|
16
16
|
npx agent-lyceum status
|
|
17
17
|
```
|
|
@@ -22,23 +22,21 @@ npx agent-lyceum status
|
|
|
22
22
|
|
|
23
23
|
## 指令
|
|
24
24
|
|
|
25
|
-
| 指令 | 功能 |
|
|
26
|
-
|
|
27
|
-
| `init [-y]` | 建立 home 與全域 agent 庫。 [詳細](docs/commands.zh-TW.md#init) |
|
|
28
|
-
| `project add <name> --dir <repo>` | 註冊專案。 [詳細](docs/commands.zh-TW.md#project-add) |
|
|
29
|
-
| `project list` / `project remove <name> [--purge]` | 列出或取消註冊專案。 [詳細](docs/commands.zh-TW.md#project-list-and-remove) |
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `resume [run-id] [-p name]` | 接續被中斷或失敗的 run
|
|
34
|
-
| `
|
|
35
|
-
| `
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
| `memory restore <時間戳> -p name --agent a` | 還原一次整理:把封存的檔案搬回。 [詳細](docs/commands.zh-TW.md#memory-restore) |
|
|
41
|
-
| `unlock [-p name] --force` | 移除當機 run 留下的專案鎖。 [詳細](docs/commands.zh-TW.md#unlock) |
|
|
25
|
+
| 層 | 指令 | 功能 |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| 專案 | `init [-y]` | 建立 home 與全域 agent 庫。 [詳細](docs/commands.zh-TW.md#init) |
|
|
28
|
+
| 專案 | `project add <name> --dir <repo>` | 註冊專案。 [詳細](docs/commands.zh-TW.md#project-add) |
|
|
29
|
+
| 專案 | `project list` / `project remove <name> [--purge]` | 列出或取消註冊專案。 [詳細](docs/commands.zh-TW.md#project-list-and-remove) |
|
|
30
|
+
| 診斷 | `doctor [-p name] [--task-file f] [--resolved] [--json]` | 不執行 agent,檢查設定(含防護等級)、git、鎖與 runtime CLI;`--resolved` 印出每項生效設定與其來源。 [詳細](docs/commands.zh-TW.md#doctor) |
|
|
31
|
+
| 診斷 | `status [-p name] [--monitor]` | 顯示 agent、信件、目前執行與歷次 run(支援 `--json`、`--monitor`)。 [詳細](docs/commands.zh-TW.md#status) |
|
|
32
|
+
| 執行 | `run ["task"] [--task-file f] [--agent a] [-p name]` | 把任務交給 lead 並執行到結束;加 `--agent` 則只交給某個成員單獨做。 [詳細](docs/commands.zh-TW.md#run) |
|
|
33
|
+
| 執行 | `resume [run-id] [--answer] [-p name]` | 接續被中斷或失敗的 run;`--answer` 會先回答它等待中的問題。 [詳細](docs/commands.zh-TW.md#resume) |
|
|
34
|
+
| 執行 | `clear <run-id> [-p name] [--dry-run] [--keep-worktrees]` / `clear --lock --force` | 刪除 run 與其任務記憶、worktree;或移除當機 run 留下的專案鎖。 [詳細](docs/commands.zh-TW.md#clear) |
|
|
35
|
+
| 執行 | `call <agent> ["task"] [--task-file f] [--dir d]` | 單獨呼叫一個 global agent(不需專案),印出它的回答。 [詳細](docs/commands.zh-TW.md#call) |
|
|
36
|
+
| 執行 | `memory tidy [-p name] [--agent a] [--layer project\|global] [--dry-run]` | 讓 agent 整理自己的記憶(只能手動觸發;封存而非刪除)。 [詳細](docs/commands.zh-TW.md#memory-tidy) |
|
|
37
|
+
| 執行 | `memory restore <時間戳> -p name --agent a` | 還原一次整理:把封存的檔案搬回。 [詳細](docs/commands.zh-TW.md#memory-restore) |
|
|
38
|
+
|
|
39
|
+
`agent-lyceum --help` 只列出每個指令的一行簡述;`agent-lyceum <指令> --help` 會印出該指令的完整說明與選項。
|
|
42
40
|
|
|
43
41
|
Ctrl-C 會乾淨地取消 run,之後可用 `resume` 接續([中止 run](docs/commands.zh-TW.md#中止-run))。`run`/`resume` 只有在 lead 回報 `completed` 時才 exit `0`;`partial`/`blocked` 為 `2`,`failed` 為 `1`,等待你回答時為 `3`,取消為 `130`([退出碼](docs/commands.zh-TW.md#退出碼))。未指定 `-p` 時,專案由目前目錄推斷([選擇專案](docs/commands.zh-TW.md#選擇專案))。
|
|
44
42
|
|
|
@@ -112,7 +110,7 @@ agents:
|
|
|
112
110
|
|
|
113
111
|
Agent 欄位:`runtime`(`claude-code`|`codex`;若 `model` 可辨識則可省略:`opus`/`sonnet`/`haiku`/`claude-*` → Claude Code,`gpt-*`/`o3`/`*codex*` → Codex;優先順序:專案 runtime > 專案 model > 全域 runtime > 全域 model)、`model`、`effort`(Claude Code:`low`|`medium`|`high`|`xhigh`|`max`,經 `--effort`;Codex:`minimal`|`low`|`medium`|`high`|`xhigh`,經 `model_reasoning_effort`;未設則用 CLI 預設)、`agent_md`、`memory.global` / `memory.project`、`resume`、`can_message`(`all` 或清單;預設 `[lead]`,lead 預設 `all`)、`can_edit_agent_md`(預設只有 lead)、`can_ask_user`(可用 `type: ask` 信件向你提問並暫停 run 等待回答;預設只有 lead)、`allow_web`(`none`|`search`|`fetch`:agent 可以使用多少網路,不設就是不開;見下方「網路存取」)、`owns`(repo glob)。
|
|
114
112
|
|
|
115
|
-
`
|
|
113
|
+
`doctor` 檢查的規則:至少 2 個 agent(lead 加一位成員;範本預設是三位)、lead 在 agent 清單內、每個 agent 都有 runtime(明設或由 `model` 推斷)且 `AGENT.md` 存在、`effort` 對該 runtime 合法(若 `runtime` 與可辨識的 `model` 矛盾則警告)、`can_message` 的目標存在、記憶資料夾互不重疊,且當 `max_parallel > 1` 時,每個非 lead 的 agent 都必須有互不重疊的 `owns`,repo 也必須是 git repository。
|
|
116
114
|
|
|
117
115
|
## 一次執行如何運作
|
|
118
116
|
|
|
@@ -176,6 +174,6 @@ agents:
|
|
|
176
174
|
- **Codex**:`-s workspace-write` 搭配 `-c sandbox_workspace_write.writable_roots=[...]`(由 OS 強制)。Codex 無法禁止 repo 內的單一檔案,所以 repo 的 `CLAUDE.md`/`AGENTS.md` 只能靠**事後偵測**保護。
|
|
177
175
|
- **所有 runtime**:執行前會對受保護檔案做雜湊;每次喚醒後,未經授權的變更會被還原、記錄,並通報 lead;被寫入的內容會先留存在 `runs/<run-id>/violations/`。
|
|
178
176
|
|
|
179
|
-
`
|
|
177
|
+
`doctor` 與 `run` 會依類別(`os`、`tool-rules`、`post-hoc`、`prompt-only`)印出每個 agent 的防護等級,以及它能寫入 repo 之外的哪些路徑。設定 `dispatcher.strict: true` 時,除非記憶、`AGENT.md` 與其他 agent 的 context 都由 OS 強制保護,否則拒絕執行。
|
|
180
178
|
|
|
181
179
|
已知缺口:Claude Code 內建的 Edit/Write 工具不在沙箱內(由 `Edit` 規則涵蓋);Codex 的 MCP 工具與 hooks 在其沙箱之外執行;`--dangerously-bypass-approvals-and-sandbox` / `danger-full-access` 會停用所有防護。啟用 `can_edit_agent_md` 時,Codex lead 的可寫根目錄會擴大到整個 agent 目錄。
|
package/dist/cli.js
CHANGED
|
@@ -79,7 +79,7 @@ project
|
|
|
79
79
|
.action((name, opts) => {
|
|
80
80
|
try {
|
|
81
81
|
const r = addProject(home(), name, opts.dir);
|
|
82
|
-
console.log(`Project "${name}" created: ${r.config}\nEdit it, then run: agent-lyceum
|
|
82
|
+
console.log(`Project "${name}" created: ${r.config}\nEdit it, then run: agent-lyceum doctor --project ${name}`);
|
|
83
83
|
}
|
|
84
84
|
catch (e) {
|
|
85
85
|
fail(e.message);
|
|
@@ -108,37 +108,6 @@ project
|
|
|
108
108
|
fail(e.message);
|
|
109
109
|
}
|
|
110
110
|
});
|
|
111
|
-
program
|
|
112
|
-
.command("validate")
|
|
113
|
-
.description("Validate the merged configuration and report enforcement levels")
|
|
114
|
-
.option("-p, --project <name>")
|
|
115
|
-
.option("--task-file <path>", "also check a task file")
|
|
116
|
-
.action((opts) => {
|
|
117
|
-
try {
|
|
118
|
-
const pr = loadProject(opts.project);
|
|
119
|
-
const res = validateProject(pr);
|
|
120
|
-
console.log(`Project: ${pr.name} (repo: ${pr.dir})\n`);
|
|
121
|
-
console.log(formatEnforcement(res.enforcement));
|
|
122
|
-
console.log("");
|
|
123
|
-
for (const i of res.issues)
|
|
124
|
-
console.log(`${i.level === "error" ? "ERROR" : "warn "} ${i.message}`);
|
|
125
|
-
if (opts.taskFile) {
|
|
126
|
-
try {
|
|
127
|
-
const t = readTaskFile(absPath(opts.taskFile, process.cwd()));
|
|
128
|
-
console.log(`task file OK (${Buffer.byteLength(t)} bytes)`);
|
|
129
|
-
}
|
|
130
|
-
catch (e) {
|
|
131
|
-
console.log(`ERROR ${e.message}`);
|
|
132
|
-
res.ok = false;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
console.log(res.ok ? "\nValid." : "\nInvalid.");
|
|
136
|
-
process.exit(res.ok ? 0 : 1);
|
|
137
|
-
}
|
|
138
|
-
catch (e) {
|
|
139
|
-
fail(e instanceof ConfigError ? e.message : e.stack ?? String(e));
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
111
|
/** Ask the runtime CLIs what they support: refuse to start when something the team needs is explicitly missing, warn when unknown. */
|
|
143
112
|
async function preflight(pr) {
|
|
144
113
|
const r = await preflightRuntimes(pr);
|
|
@@ -217,7 +186,7 @@ function printWaiting(pr, runId, file, problems) {
|
|
|
217
186
|
console.log(`\nRun ${runId} is waiting for your answers.\n File: ${file}`);
|
|
218
187
|
for (const p of problems)
|
|
219
188
|
console.log(` - ${p}`);
|
|
220
|
-
console.log(`Answer with: agent-lyceum
|
|
189
|
+
console.log(`Answer with: agent-lyceum resume ${runId} --answer -p ${pr.name} (or edit the file, then: agent-lyceum resume ${runId} -p ${pr.name})`);
|
|
221
190
|
}
|
|
222
191
|
/**
|
|
223
192
|
* Keep going while the run stops to ask and the questions can be settled right now: by --assume-defaults, or (in a terminal)
|
|
@@ -253,7 +222,8 @@ async function driveRun(pr, runDir, first, assumeDefaults) {
|
|
|
253
222
|
}
|
|
254
223
|
program
|
|
255
224
|
.command("run [task]")
|
|
256
|
-
.
|
|
225
|
+
.summary("Start the dispatcher with a task for the lead")
|
|
226
|
+
.description('Start the dispatcher: give the task to the lead.\n\nGive the task either way:\n - "<task>": as text\n - --task-file <path>: from a file\n\nWho gets it:\n - by default, the lead\n - --agent <name>: that one member alone')
|
|
257
227
|
.option("-p, --project <name>")
|
|
258
228
|
.option("--task-file <path>", "read the task from a file")
|
|
259
229
|
.option("--agent <name>", "run only this member of the project, alone: it cannot mail teammates or ask you, and its own `done` ends the run")
|
|
@@ -265,7 +235,7 @@ program
|
|
|
265
235
|
for (const i of res.issues)
|
|
266
236
|
console.error(`${i.level === "error" ? "ERROR" : "warn "} ${i.message}`);
|
|
267
237
|
if (!res.ok)
|
|
268
|
-
fail("Configuration is invalid; fix the errors above (see `agent-lyceum
|
|
238
|
+
fail("Configuration is invalid; fix the errors above (see `agent-lyceum doctor`).");
|
|
269
239
|
let checked = pr;
|
|
270
240
|
if (opts.agent) {
|
|
271
241
|
assertName("agent", opts.agent);
|
|
@@ -299,8 +269,11 @@ program
|
|
|
299
269
|
});
|
|
300
270
|
program
|
|
301
271
|
.command("resume [run-id]")
|
|
302
|
-
.
|
|
272
|
+
.summary("Continue an interrupted (or failed) run")
|
|
273
|
+
.description("Continue an interrupted (or failed) run: same run dir, sessions and round count; the task is not re-sent.\n\nWhich run:\n - without an id: the newest run that is not done or running\n - with an id: that run")
|
|
303
274
|
.option("-p, --project <name>")
|
|
275
|
+
.option("--answer", "the run is waiting for answers: open ask-reply.md in $EDITOR, check the answers, then continue")
|
|
276
|
+
.option("--no-edit", "with --answer: do not open the editor; check the file as it is")
|
|
304
277
|
.option("--assume-defaults", "if the run waits for answers, use the suggested answer for every unanswered question that has one")
|
|
305
278
|
.action(async (runId, opts) => {
|
|
306
279
|
try {
|
|
@@ -309,7 +282,7 @@ program
|
|
|
309
282
|
for (const i of res.issues)
|
|
310
283
|
console.error(`${i.level === "error" ? "ERROR" : "warn "} ${i.message}`);
|
|
311
284
|
if (!res.ok)
|
|
312
|
-
fail("Configuration is invalid; fix the errors above (see `agent-lyceum
|
|
285
|
+
fail("Configuration is invalid; fix the errors above (see `agent-lyceum doctor`).");
|
|
313
286
|
let found;
|
|
314
287
|
if (runId) {
|
|
315
288
|
assertName("run", runId);
|
|
@@ -338,7 +311,11 @@ program
|
|
|
338
311
|
}
|
|
339
312
|
if (state.rounds >= pr.dispatcher.max_rounds)
|
|
340
313
|
fail(`Run ${state.run_id} used ${state.rounds}/${pr.dispatcher.max_rounds} rounds; raise dispatcher.max_rounds in project.yaml first.`);
|
|
314
|
+
if (opts.answer && state.end_reason !== "waiting")
|
|
315
|
+
fail(`Run ${state.run_id} is not waiting for answers (${state.end_reason ?? "not finished"}).`);
|
|
341
316
|
if (state.end_reason === "waiting") {
|
|
317
|
+
if (opts.answer && opts.edit)
|
|
318
|
+
openInEditor(prepareAnswers(dir, {}).path);
|
|
342
319
|
const prep = prepareAnswers(dir, { assumeDefaults: !!opts.assumeDefaults });
|
|
343
320
|
if (!prep.complete) {
|
|
344
321
|
printWaiting(pr, state.run_id, prep.path, prep.problems);
|
|
@@ -352,52 +329,49 @@ program
|
|
|
352
329
|
fail(e instanceof ConfigError ? e.message : e.message);
|
|
353
330
|
}
|
|
354
331
|
});
|
|
355
|
-
program
|
|
356
|
-
.command("answer <run-id>")
|
|
357
|
-
.description("Answer the questions a waiting run asked: opens ask-reply.md in $EDITOR, checks the answers, then continues the run. With --no-edit it only checks a file you already edited.")
|
|
358
|
-
.option("-p, --project <name>")
|
|
359
|
-
.option("--no-edit", "do not open the editor; check the file as it is")
|
|
360
|
-
.option("--assume-defaults", "use the suggested answer for every unanswered question that has one")
|
|
361
|
-
.action(async (runId, opts) => {
|
|
362
|
-
try {
|
|
363
|
-
assertName("run", runId);
|
|
364
|
-
const pr = loadProject(opts.project);
|
|
365
|
-
const dir = path.join(pr.paths.runs, runId);
|
|
366
|
-
if (!fs.existsSync(path.join(dir, "state.json")))
|
|
367
|
-
fail(`Run "${runId}" not found in ${pr.paths.runs}.`);
|
|
368
|
-
const state = loadRunState(dir);
|
|
369
|
-
if (state.end_reason !== "waiting")
|
|
370
|
-
fail(`Run ${runId} is not waiting for answers (${state.end_reason ?? "not finished"}).`);
|
|
371
|
-
if (opts.edit)
|
|
372
|
-
openInEditor(prepareAnswers(dir, {}).path);
|
|
373
|
-
const prep = prepareAnswers(dir, { assumeDefaults: !!opts.assumeDefaults });
|
|
374
|
-
if (!prep.complete) {
|
|
375
|
-
printWaiting(pr, runId, prep.path, prep.problems);
|
|
376
|
-
process.exit(3);
|
|
377
|
-
}
|
|
378
|
-
const res = validateProject(pr);
|
|
379
|
-
for (const i of res.issues)
|
|
380
|
-
console.error(`${i.level === "error" ? "ERROR" : "warn "} ${i.message}`);
|
|
381
|
-
if (!res.ok)
|
|
382
|
-
fail("Configuration is invalid; fix the errors above (see `agent-lyceum validate`).");
|
|
383
|
-
await preflight(pr);
|
|
384
|
-
const summary = await continueRun(pr, dir, runId);
|
|
385
|
-
await driveRun(pr, dir, summary, !!opts.assumeDefaults);
|
|
386
|
-
}
|
|
387
|
-
catch (e) {
|
|
388
|
-
fail(e instanceof ConfigError ? e.message : e.message);
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
332
|
/** Color when writing to a terminal, unless NO_COLOR is set; FORCE_COLOR overrides. */
|
|
333
|
+
/** `clear --lock`: show who holds the project lock; remove it only with --force. */
|
|
334
|
+
function unlockProject(pr, force) {
|
|
335
|
+
const info = inspectProjectLock(pr.paths.root);
|
|
336
|
+
if (!info && !fs.existsSync(path.join(pr.paths.root, "lock.json"))) {
|
|
337
|
+
console.log(`Project ${pr.name} has no run lock.`);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (info) {
|
|
341
|
+
console.log(`Lock held for run ${info.run_id} by pid ${info.pid} on ${info.hostname}, last heartbeat ${info.heartbeat_at}${lockHolderAlive(info) ? " (that process is still alive)" : " (that process is gone)"}.`);
|
|
342
|
+
}
|
|
343
|
+
else
|
|
344
|
+
console.log("The lock file exists but cannot be read.");
|
|
345
|
+
if (!force)
|
|
346
|
+
fail("Not removed. Check that no run is active, then repeat with --force.");
|
|
347
|
+
forceUnlock(pr.paths.root);
|
|
348
|
+
console.log("Lock removed. Resume the run with: agent-lyceum resume");
|
|
349
|
+
}
|
|
392
350
|
program
|
|
393
|
-
.command("clear
|
|
394
|
-
.
|
|
395
|
-
"
|
|
351
|
+
.command("clear [run-id]")
|
|
352
|
+
.summary("Delete a run by id, or remove a stale run lock")
|
|
353
|
+
.description("Delete a task (run) by id: its run directory (state, log, result, mailboxes), its task memory and its agent worktrees, branches and snapshots.\n\n" +
|
|
354
|
+
"Long-term (global and project) memory is never touched.\n" +
|
|
355
|
+
"Refuses while the run is running, or while a worktree holds work that is not in the repo.\n\n" +
|
|
356
|
+
"With --lock (no run id) it does something else:\n" +
|
|
357
|
+
" - removes the run lock a crashed run left behind\n" +
|
|
358
|
+
" - needs --force to actually remove it")
|
|
396
359
|
.option("-p, --project <name>")
|
|
397
360
|
.option("--dry-run", "only list what would be deleted and what would be kept")
|
|
361
|
+
.option("--lock", "remove the project's run lock left behind by a crashed run (takes no run id; needs --force)")
|
|
362
|
+
.option("--force", "with --lock: remove the lock even though its owner cannot be confirmed dead")
|
|
398
363
|
.option("--keep-worktrees", "keep the agents' worktrees and branches (and their snapshots), and still clear the run and its task memory")
|
|
399
364
|
.action((runId, opts) => {
|
|
400
365
|
try {
|
|
366
|
+
if (opts.lock) {
|
|
367
|
+
if (runId)
|
|
368
|
+
fail("--lock takes no run id.");
|
|
369
|
+
return unlockProject(loadProject(opts.project), !!opts.force);
|
|
370
|
+
}
|
|
371
|
+
if (opts.force)
|
|
372
|
+
fail("--force only goes with --lock.");
|
|
373
|
+
if (!runId)
|
|
374
|
+
fail("Give the run id to clear (see `agent-lyceum status --task-list`), or use --lock.");
|
|
401
375
|
assertName("run", runId);
|
|
402
376
|
const pr = loadProject(opts.project);
|
|
403
377
|
const showPlan = (plan) => {
|
|
@@ -442,80 +416,65 @@ program
|
|
|
442
416
|
fail(e.message);
|
|
443
417
|
}
|
|
444
418
|
});
|
|
445
|
-
const configCmd = program.command("config").description("Inspect configuration");
|
|
446
|
-
configCmd
|
|
447
|
-
.command("show")
|
|
448
|
-
.description("Show the effective configuration of a project and where each value comes from")
|
|
449
|
-
.requiredOption("--resolved", "merge global library, project file and defaults (currently the only view)")
|
|
450
|
-
.option("-p, --project <name>")
|
|
451
|
-
.option("--json", "print as JSON")
|
|
452
|
-
.action((opts) => {
|
|
453
|
-
try {
|
|
454
|
-
const h = home();
|
|
455
|
-
const pr = loadProject(opts.project);
|
|
456
|
-
const { project, sources } = resolveProjectWithSources(h, pr.name);
|
|
457
|
-
const values = flattenResolved(project);
|
|
458
|
-
if (opts.json) {
|
|
459
|
-
console.log(JSON.stringify({ schema_version: 1, project: project.name, values, sources }, null, 2));
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
const rel = (f) => path.relative(h, f) || f;
|
|
463
|
-
const where = (s) => ("default" in s ? "default" : "inferred_from" in s ? `inferred from ${s.inferred_from.key} (${rel(s.inferred_from.file)})` : rel(s.file));
|
|
464
|
-
const show = (v) => (typeof v === "string" ? v : JSON.stringify(v));
|
|
465
|
-
const keys = Object.keys(sources).filter((k) => values[k] !== undefined);
|
|
466
|
-
const w = Math.max(...keys.map((k) => `${k} = ${show(values[k])}`.length));
|
|
467
|
-
for (const k of keys)
|
|
468
|
-
console.log(`${`${k} = ${show(values[k])}`.padEnd(w)} ${where(must(sources[k], `source of ${k}`))}`);
|
|
469
|
-
}
|
|
470
|
-
catch (e) {
|
|
471
|
-
fail(e instanceof ConfigError ? e.message : e.message);
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
419
|
program
|
|
475
420
|
.command("doctor")
|
|
476
|
-
.
|
|
421
|
+
.summary("Check the setup without running any agent")
|
|
422
|
+
.description("Check the setup without running any agent. It checks:\n - configuration and enforcement levels\n - git\n - the project lock\n - what each runtime CLI supports (read from its --version and --help)\n\nWith --resolved it checks nothing and instead prints:\n - every effective setting\n - where each one came from")
|
|
477
423
|
.option("-p, --project <name>")
|
|
424
|
+
.option("--task-file <path>", "also check a task file")
|
|
425
|
+
.option("--resolved", "print the merged settings (global library, project file, defaults) and their sources; runs no other check")
|
|
478
426
|
.option("--json", "print the report as JSON")
|
|
479
427
|
.action(async (opts) => {
|
|
480
428
|
try {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
process.exit(report.ok ? 0 : 1);
|
|
484
|
-
}
|
|
485
|
-
catch (e) {
|
|
486
|
-
fail(e instanceof ConfigError ? e.message : e.message);
|
|
487
|
-
}
|
|
488
|
-
});
|
|
489
|
-
program
|
|
490
|
-
.command("unlock")
|
|
491
|
-
.description("Remove a project's run lock left behind by a crashed run. Shows the lock first; needs --force to remove it.")
|
|
492
|
-
.option("-p, --project <name>")
|
|
493
|
-
.option("--force", "remove the lock even though its owner cannot be confirmed dead")
|
|
494
|
-
.action((opts) => {
|
|
495
|
-
try {
|
|
496
|
-
const pr = loadProject(opts.project);
|
|
497
|
-
const info = inspectProjectLock(pr.paths.root);
|
|
498
|
-
if (!info && !fs.existsSync(path.join(pr.paths.root, "lock.json"))) {
|
|
499
|
-
console.log(`Project ${pr.name} has no run lock.`);
|
|
429
|
+
if (opts.resolved) {
|
|
430
|
+
printResolved(opts.project, !!opts.json);
|
|
500
431
|
return;
|
|
501
432
|
}
|
|
502
|
-
|
|
503
|
-
|
|
433
|
+
const pr = loadProject(opts.project);
|
|
434
|
+
const report = await diagnoseProject(pr);
|
|
435
|
+
let taskError;
|
|
436
|
+
if (opts.taskFile) {
|
|
437
|
+
try {
|
|
438
|
+
readTaskFile(absPath(opts.taskFile, process.cwd()));
|
|
439
|
+
}
|
|
440
|
+
catch (e) {
|
|
441
|
+
taskError = e.message;
|
|
442
|
+
report.checks.push({ level: "error", subject: "task", message: taskError });
|
|
443
|
+
report.ok = false;
|
|
444
|
+
}
|
|
504
445
|
}
|
|
446
|
+
if (opts.json)
|
|
447
|
+
console.log(JSON.stringify({ schema_version: 1, ...report }, null, 2));
|
|
505
448
|
else
|
|
506
|
-
console.log(
|
|
507
|
-
|
|
508
|
-
fail("Not removed. Check that no run is active, then repeat with --force.");
|
|
509
|
-
forceUnlock(pr.paths.root);
|
|
510
|
-
console.log("Lock removed. Resume the run with: agent-lyceum resume");
|
|
449
|
+
console.log(`${formatEnforcement(validateProject(pr).enforcement)}\n\n${formatDoctor(report)}`);
|
|
450
|
+
process.exit(report.ok ? 0 : 1);
|
|
511
451
|
}
|
|
512
452
|
catch (e) {
|
|
513
|
-
fail(e.message);
|
|
453
|
+
fail(e instanceof ConfigError ? e.message : e.message);
|
|
514
454
|
}
|
|
515
455
|
});
|
|
456
|
+
/** `doctor --resolved`: every effective setting with the file and key it came from. */
|
|
457
|
+
function printResolved(projectName, json) {
|
|
458
|
+
const h = home();
|
|
459
|
+
const pr = loadProject(projectName);
|
|
460
|
+
const { project, sources } = resolveProjectWithSources(h, pr.name);
|
|
461
|
+
const values = flattenResolved(project);
|
|
462
|
+
if (json) {
|
|
463
|
+
console.log(JSON.stringify({ schema_version: 1, project: project.name, values, sources }, null, 2));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const rel = (f) => path.relative(h, f) || f;
|
|
467
|
+
const where = (s) => ("default" in s ? "default" : "inferred_from" in s ? `inferred from ${s.inferred_from.key} (${rel(s.inferred_from.file)})` : rel(s.file));
|
|
468
|
+
const show = (v) => (typeof v === "string" ? v : JSON.stringify(v));
|
|
469
|
+
const keys = Object.keys(sources).filter((k) => values[k] !== undefined);
|
|
470
|
+
const w = Math.max(...keys.map((k) => `${k} = ${show(values[k])}`.length));
|
|
471
|
+
for (const k of keys)
|
|
472
|
+
console.log(`${`${k} = ${show(values[k])}`.padEnd(w)} ${where(must(sources[k], `source of ${k}`))}`);
|
|
473
|
+
}
|
|
516
474
|
program
|
|
517
475
|
.command("call <agent> [task]")
|
|
518
|
-
.
|
|
476
|
+
.summary("Call one global-library agent on its own, outside any project")
|
|
477
|
+
.description("Call one agent of the global library on its own, outside any project.\n\n - it works in --dir (default: the current directory)\n - its final answer is printed\n - it cannot mail anyone or ask you")
|
|
519
478
|
.option("--task-file <path>", "read the task from a file")
|
|
520
479
|
.option("--dir <path>", "the directory the agent works in (default: the current directory)")
|
|
521
480
|
.action(async (agentName, task, opts) => {
|
|
@@ -558,7 +517,8 @@ function layerOpt(v) {
|
|
|
558
517
|
}
|
|
559
518
|
memoryCmd
|
|
560
519
|
.command("tidy")
|
|
561
|
-
.
|
|
520
|
+
.summary("Have agents tidy their own memory")
|
|
521
|
+
.description("Have agents tidy their own memory: merge duplicates, fix stale entries, archive what is obsolete (never deletes).\n\nManual only; nothing triggers it automatically.")
|
|
562
522
|
.option("-p, --project <name>")
|
|
563
523
|
.option("--agent <name>", "tidy only this agent (default: every agent)")
|
|
564
524
|
.option("--layer <layer>", "project (default) or global; global memory is shared by every project, so it is never tidied unless you ask")
|
|
@@ -577,7 +537,7 @@ memoryCmd
|
|
|
577
537
|
for (const i of res.issues)
|
|
578
538
|
console.error(`${i.level === "error" ? "ERROR" : "warn "} ${i.message}`);
|
|
579
539
|
if (!res.ok)
|
|
580
|
-
fail("Configuration is invalid; fix the errors above (see `agent-lyceum
|
|
540
|
+
fail("Configuration is invalid; fix the errors above (see `agent-lyceum doctor`).");
|
|
581
541
|
await preflight(pr);
|
|
582
542
|
const lease = takeLock(pr, `tidy-${tidyStamp()}`);
|
|
583
543
|
const cancel = cancelOnSignals();
|
|
@@ -702,4 +662,14 @@ program
|
|
|
702
662
|
fail(e instanceof ConfigError ? e.message : e.message);
|
|
703
663
|
}
|
|
704
664
|
});
|
|
665
|
+
// `--help` lists the commands in three layers.
|
|
666
|
+
const HELP_LAYERS = {
|
|
667
|
+
"Project:": ["init", "project"],
|
|
668
|
+
"Diagnose:": ["doctor", "status"],
|
|
669
|
+
"Run:": ["run", "resume", "clear", "call", "memory"],
|
|
670
|
+
};
|
|
671
|
+
for (const [heading, names] of Object.entries(HELP_LAYERS))
|
|
672
|
+
for (const c of program.commands)
|
|
673
|
+
if (names.includes(c.name()))
|
|
674
|
+
c.helpGroup(heading);
|
|
705
675
|
program.parseAsync().catch((e) => fail(e.message));
|
package/dist/doctor.js
CHANGED
|
@@ -130,7 +130,7 @@ export async function diagnoseProject(project, opts = {}) {
|
|
|
130
130
|
checks.push({
|
|
131
131
|
level: "warn",
|
|
132
132
|
subject: "lock",
|
|
133
|
-
message: alive ? `A run is active: ${lock.run_id} (pid ${lock.pid}).` : `A stale run lock is left by ${lock.run_id} (pid ${lock.pid} is gone); \`agent-lyceum
|
|
133
|
+
message: alive ? `A run is active: ${lock.run_id} (pid ${lock.pid}).` : `A stale run lock is left by ${lock.run_id} (pid ${lock.pid} is gone); \`agent-lyceum clear --lock --force\` clears it.`,
|
|
134
134
|
});
|
|
135
135
|
}
|
|
136
136
|
for (const a of tidyAdvice(project))
|
package/dist/project-lock.d.ts
CHANGED
|
@@ -35,5 +35,5 @@ export declare function inspectProjectLock(root: string): LockInfo | undefined;
|
|
|
35
35
|
*/
|
|
36
36
|
export declare function lockHolderAlive(info: LockInfo): boolean;
|
|
37
37
|
export declare function acquireProjectLock(root: string, runId: string, opts?: LockOptions): ProjectLease;
|
|
38
|
-
/** Remove the lock regardless of owner (the `
|
|
38
|
+
/** Remove the lock regardless of owner (the `clear --lock --force` escape hatch). Returns what was there, if readable. */
|
|
39
39
|
export declare function forceUnlock(root: string): LockInfo | undefined;
|
package/dist/project-lock.js
CHANGED
|
@@ -95,7 +95,7 @@ function heldError(info) {
|
|
|
95
95
|
`Only one run per project at a time; wait for it to end, or stop it first.`);
|
|
96
96
|
}
|
|
97
97
|
function unreadableError(root) {
|
|
98
|
-
return new Error(`The project lock ${path.join(root, LOCK_FILE)} cannot be read or has no known owner. Check that no run is active, then run \`agent-lyceum
|
|
98
|
+
return new Error(`The project lock ${path.join(root, LOCK_FILE)} cannot be read or has no known owner. Check that no run is active, then run \`agent-lyceum clear --lock --force\`.`);
|
|
99
99
|
}
|
|
100
100
|
/** Create the lock file only if absent: write a private temp file, then hard-link it into place (atomic, never half-written). */
|
|
101
101
|
function tryCreate(root, info) {
|
|
@@ -212,7 +212,7 @@ export function acquireProjectLock(root, runId, opts = {}) {
|
|
|
212
212
|
release,
|
|
213
213
|
};
|
|
214
214
|
}
|
|
215
|
-
/** Remove the lock regardless of owner (the `
|
|
215
|
+
/** Remove the lock regardless of owner (the `clear --lock --force` escape hatch). Returns what was there, if readable. */
|
|
216
216
|
export function forceUnlock(root) {
|
|
217
217
|
const info = inspectProjectLock(root);
|
|
218
218
|
fs.rmSync(path.join(root, LOCK_FILE), { force: true });
|
package/dist/prompt.js
CHANGED
|
@@ -114,7 +114,7 @@ export function buildSystemPrompt(project, agent) {
|
|
|
114
114
|
lines.push(`- ${label}: \`${m}\``);
|
|
115
115
|
}
|
|
116
116
|
if (mems.length) {
|
|
117
|
-
lines.push("Keep a `MEMORY.md` index in each memory directory (one line per entry, pointing at a file). Its contents are shown in your prompt", "on every wake-up; read other memory files on demand; you can write only inside these directories.", "Put anything specific to the current task (decisions, progress, findings, branch names) in the **task** memory: it is private to this task and starts empty for every new task.", "Use the global/project memory only for durable lessons that stay true across tasks; never record task-specific details there.");
|
|
117
|
+
lines.push("Keep a `MEMORY.md` index in each memory directory (one line per entry, pointing at a file). Its contents are shown in your prompt", "on every wake-up; read other memory files on demand; you can write only inside these directories.", "Put anything specific to the current task (decisions, progress, findings, branch names) in the **task** memory: it is private to this task and starts empty for every new task.", "Also record approaches you considered but rejected, with the reason, so they can be revisited or reversed later.", "Use the global/project memory only for durable lessons that stay true across tasks; never record task-specific details there.");
|
|
118
118
|
}
|
|
119
119
|
lines.push("", "## Rules");
|
|
120
120
|
lines.push("- Never edit any `AGENT.md`, `COMMON.md`, or the repository's own `CLAUDE.md`/`AGENTS.md`" +
|
package/dist/run-outcome.js
CHANGED
|
@@ -17,7 +17,7 @@ export function endOutcome(endReason, done) {
|
|
|
17
17
|
case "lead_failed":
|
|
18
18
|
return { outcome: "failed", note: "The lead's wake-up failed." };
|
|
19
19
|
case "waiting":
|
|
20
|
-
return { outcome: "waiting", note: "Waiting for your answers. Fill in ask-reply.md or run `agent-lyceum
|
|
20
|
+
return { outcome: "waiting", note: "Waiting for your answers. Fill in ask-reply.md or run `agent-lyceum resume <run-id> --answer`, then continue with `agent-lyceum resume`." };
|
|
21
21
|
case "cancelled":
|
|
22
22
|
return { outcome: "cancelled", note: "Cancelled before the lead finished. Unread mail was kept; continue with `agent-lyceum resume`." };
|
|
23
23
|
case "idle":
|
package/dist/status.js
CHANGED
|
@@ -301,7 +301,7 @@ function summaryBlock(run, now, c) {
|
|
|
301
301
|
if (lastWake)
|
|
302
302
|
lines.push(`${L("上一次:")}#${lastWake.round} ${c.cyan(lastWake.agent)} ${okText(c, lastWake.ok)} → ${sentText(lastWake.sent)}`);
|
|
303
303
|
if (s.ask)
|
|
304
|
-
lines.push(`${L("等你回答:")}${s.ask.unanswered}/${s.ask.total} 題未答 → ${s.ask.path}`, c.dim(` 填完後執行 agent-lyceum
|
|
304
|
+
lines.push(`${L("等你回答:")}${s.ask.unanswered}/${s.ask.total} 題未答 → ${s.ask.path}`, c.dim(` 填完後執行 agent-lyceum resume ${s.run_id} --answer`));
|
|
305
305
|
lines.push(`${L("流程:")}${flowLine(s, c)}`);
|
|
306
306
|
return lines;
|
|
307
307
|
}
|