agent-lyceum 0.4.14 → 0.6.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.
Files changed (47) hide show
  1. package/README.md +21 -21
  2. package/README.zh-TW.md +21 -21
  3. package/dist/adapters/claude.js +2 -0
  4. package/dist/ask-answer.d.ts +10 -0
  5. package/dist/ask-answer.js +21 -0
  6. package/dist/ask-reply.d.ts +65 -0
  7. package/dist/ask-reply.js +165 -0
  8. package/dist/assert.d.ts +2 -0
  9. package/dist/assert.js +6 -0
  10. package/dist/cli.js +202 -16
  11. package/dist/config.d.ts +21 -16
  12. package/dist/config.js +19 -7
  13. package/dist/dispatcher.d.ts +3 -34
  14. package/dist/dispatcher.js +4 -464
  15. package/dist/doctor.js +3 -0
  16. package/dist/format.js +7 -5
  17. package/dist/fs-util.d.ts +2 -0
  18. package/dist/fs-util.js +15 -0
  19. package/dist/guard.d.ts +8 -0
  20. package/dist/guard.js +24 -3
  21. package/dist/mailbox.d.ts +11 -1
  22. package/dist/mailbox.js +21 -8
  23. package/dist/memory-tidy.d.ts +115 -0
  24. package/dist/memory-tidy.js +351 -0
  25. package/dist/policy.js +3 -3
  26. package/dist/process-runner.js +1 -2
  27. package/dist/project-lock.d.ts +10 -1
  28. package/dist/project-lock.js +39 -4
  29. package/dist/prompt.d.ts +20 -0
  30. package/dist/prompt.js +76 -3
  31. package/dist/run-cleanup.js +4 -3
  32. package/dist/run-log.d.ts +5 -0
  33. package/dist/run-log.js +18 -0
  34. package/dist/run-outcome.d.ts +22 -0
  35. package/dist/run-outcome.js +28 -0
  36. package/dist/run-session.d.ts +92 -0
  37. package/dist/run-session.js +602 -0
  38. package/dist/run-store.d.ts +3 -1
  39. package/dist/run-store.js +7 -6
  40. package/dist/scaffold.js +1 -0
  41. package/dist/schema.d.ts +72 -239
  42. package/dist/schema.js +8 -3
  43. package/dist/status.d.ts +9 -1
  44. package/dist/status.js +51 -15
  45. package/dist/validate.js +12 -8
  46. package/dist/worktree.js +7 -6
  47. package/package.json +14 -8
package/README.md CHANGED
@@ -24,23 +24,22 @@ Requires Node 20+, plus `claude` and/or `codex` on your `PATH` (already logged i
24
24
 
25
25
  | Command | What it does |
26
26
  |---|---|
27
- | `init [-y]` | Create the home and a global agent library (`lead`, `fe-member`, `qa-member`). Asks before creating the default path. |
28
- | `project add <name> --dir <repo>` | Register a project: creates `projects/<name>/project.yaml`, shared folders and `COMMON.md`. |
29
- | `project list` / `project remove <name> [--purge]` | List projects / unregister (keeps context unless `--purge`). |
30
- | `validate [-p name] [--task-file f]` | Validate the merged config and print each agent's enforcement level. Exit 1 on errors. |
31
- | `run ["task"] [--task-file f] [-p name]` | Give the task to the lead and run the dispatcher until done. Give *either* text or `--task-file`. |
32
- | `resume [run-id] [-p name]` | Continue an interrupted or failed run (each `run` is its own task; without an id, the newest run that is neither done nor running): same run dir, sessions and round count; the task is not re-sent and unread mail is picked up again. Refuses if the run is still alive. For a run that is already done it continues nothing: it prints the recorded outcome and result and exits with the code for that outcome (`0` completed, `2` partial/blocked, `1` failed; a run from before outcomes were recorded counts as an unverified `partial`, exit `2`). A run that ended idle or at `max_rounds` has no unread mail left unless you add some, so resuming it ends idle again (exit `2`). |
33
- | `status [-p name] [--monitor]` | Agents, unread mail, what is running right now (agent, elapsed time, message being handled), last run (task source, rounds, end reason, output tokens). Without `--monitor` it also prints the full wake-by-wake record of the latest run; `--monitor` keeps a live page open showing only the latest three wakes of each run. `--task-id <id>` prints everything about one task (every wake, its result, log directory). `--task-list [project]` lists every run of the project with its id, state, rounds and task (feed the id to `resume`). `--json` prints the same report as JSON (`schema_version: 1`, no colour, nothing else on stdout; errors go to stderr with a non-zero exit); it works with `--task-list` and `--task-id`. Text and JSON are made from the same report. |
34
- | `clear <run-id> [-p name] [--dry-run] [--keep-worktrees]` | Delete a task (run) by id: its run directory (state, log, result, mailboxes), its **task memory**, and its agents' git worktrees, branches and snapshot refs. Global and project (long-term) memory is never touched. It refuses, and deletes nothing, while the run is still running or while a worktree holds work that is not in your repo (uncommitted, or committed but not brought in); it lists that work. `--keep-worktrees` keeps the worktrees and their branches and still clears the rest. `--dry-run` only prints what would be deleted and kept. Symlinks are never followed out of the project home. Progress is journaled in `projects/<name>/cleanup/`, so an interrupted `clear` is finished by repeating the same command. List ids with `status --task-list`. |
35
- | `config show --resolved [-p name] [--json]` | Print every effective setting (global library, project file and defaults merged) and where it came from: the file and key, `default`, or `inferred from <model key>` when the runtime was recognised from `model`. Project values replace global ones; lists (`can_message`, `owns`) are replaced, not merged; relative paths are resolved against the file they were written in. |
36
- | `doctor [-p name] [--json]` | Check the setup without running any agent: configuration, git (for parallel runs), the project lock, and what each runtime CLI supports, read from its `--version` and `--help` (JSON output, resume, sandbox settings, effort; each is `yes`, `no` or `unknown`). Exit 1 when something the team needs is explicitly unsupported. It does not read credentials: login status is reported as unknown. `run` and `resume` do the same capability check first: they refuse to start on an explicit `no` and warn on `unknown`. A real end-to-end check against the live CLIs (a tiny task) spends tokens and is deliberately manual. |
37
- | `unlock [-p name] --force` | Remove the project's run lock left behind by a crashed run (only one run per project at a time). Without `--force` it only shows who holds the lock. |
38
-
39
- **Stopping a run:** Ctrl-C (or SIGTERM) cancels it cleanly: running agents get SIGTERM, then SIGKILL after 5 s (their whole process tree), unread mail is kept, the lock is released and the exit code is `130`; continue later with `agent-lyceum resume`. A second Ctrl-C quits at once. A wake-up that exceeds `wake_timeout_sec` is stopped the same way and counts as a failed attempt. Each attempt's complete stdout/stderr is written to `runs/<run-id>/mail/attempts/<attempt>/log/`; only the last 64 KiB of each stream is kept in memory.
40
-
41
- **Exit codes of `run` / `resume`:** `0` only when the lead reported `outcome: completed`; `2` for `partial` or `blocked` (also when the run went idle or hit `max_rounds` without a done); `1` for `failed` (also when the lead itself failed); `130` for `cancelled`. *Upgrading:* before this version an `idle` run exited `0` and a failed lead exited `2`; scripts that treated `0` as "the run ended" must now check for `0` as "the work was completed". Runs that ended before outcomes were recorded show as unverified (`partial`) and are not reported as a success.
42
-
43
- Without `--project`, the project is the registered one whose `dir` is the longest prefix of the current directory; if none matches the command lists the registered projects and stops.
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
+ | `validate [-p name] [--task-file f]` | Validate the merged config and show each agent's enforcement level. [details](docs/commands.md#validate) |
31
+ | `run ["task"] [--task-file f] [-p name]` | Give the task to the lead and run until done. [details](docs/commands.md#run) |
32
+ | `resume [run-id] [-p name]` | Continue an interrupted or failed run. [details](docs/commands.md#resume) |
33
+ | `answer <run-id> [--no-edit] [-p name]` | Answer the questions a waiting run asked, then continue it. [details](docs/commands.md#answer) |
34
+ | `status [-p name] [--monitor]` | Show agents, mail, the current wake-up and past runs (`--json`, `--monitor`). [details](docs/commands.md#status) |
35
+ | `clear <run-id> [-p name] [--dry-run] [--keep-worktrees]` | Delete a run with its task memory and worktrees. [details](docs/commands.md#clear) |
36
+ | `config show --resolved [-p name] [--json]` | Print every effective setting and where it came from. [details](docs/commands.md#config-show) |
37
+ | `doctor [-p name] [--json]` | Check config, git, the lock and runtime CLIs without running agents. [details](docs/commands.md#doctor) |
38
+ | `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) |
39
+ | `memory restore <timestamp> -p name --agent a` | Undo a tidy: move its archived files back. [details](docs/commands.md#memory-restore) |
40
+ | `unlock [-p name] --force` | Remove the project lock a crashed run left behind. [details](docs/commands.md#unlock) |
41
+
42
+ 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
43
 
45
44
  ## Monitor mod (Claude Code)
46
45
 
@@ -57,7 +56,7 @@ claude --plugin-dir /path/to/agent-lyceum/plugins/status-monitor
57
56
  **Use it.**
58
57
  - **Status line:** always on once loaded (`team 讀取中…` until the first poll, `team 無法取得狀態:…` when the CLI cannot run); shows the run state, round `n/max`, output tokens, the current step and unread mail. With several projects it shows one overview line (`team 1/3 running · a ... | b ...`).
59
58
  - **Toasts:** a message pops up when a run finishes, is interrupted, or an agent's wake fails (prefixed `[project]` when several are watched). Nothing to do.
60
- - **`/status-monitor`:** the pane opens automatically at session start (it seats from 144 terminal columns; the status line works at any width); type the command to open it at any width. It shows, per project: the run id and state, task, progress and checklist, the agents working now and the queued mail, blocked integrations, notes and result summary, output tokens per runtime, per-agent wake statistics, the last 8 wakes. Each task (a project's current run and its last 3 past runs) is a tab; click a tab to switch. It refreshes with the polling interval while open. Click **關閉** (Close) to dismiss it (Esc does not close it).
59
+ - **`/status-monitor`:** type it to open (or, if already open, close) the pane. It is off at session start unless `autoOpen` is set. In the **fullscreen layout** (`"tui": "fullscreen"` in `~/.claude/settings.json`, restart the session to apply; `/tui fullscreen` does the same) from 110 terminal columns it docks on the right at half the terminal width, and tabs and **關閉** (Close) can be clicked. Otherwise it shows above the prompt; the layout is fixed when the session starts, so a session started on the main screen stays that way. Keyboard (works in both): `x` closes, `1`-`9` or Tab/Enter switch task, Esc returns to the prompt, running the command again also closes. It shows, per project: the run id and state, task, progress and checklist, the agents working now and the queued mail, blocked integrations, notes and result summary, output tokens per runtime, per-agent wake statistics, the last 8 wakes. Each running task is a tab, newest first; finished and interrupted runs are not listed. It refreshes with the polling interval while open.
61
60
 
62
61
  **Configure it.** In `~/.claude/settings.json` under `pluginConfigs."status-monitor@inline"` (`options`):
63
62
 
@@ -66,6 +65,7 @@ claude --plugin-dir /path/to/agent-lyceum/plugins/status-monitor
66
65
  | `command` | CLI to run, split on spaces, e.g. `node /path/to/agent-lyceum/dist/cli.js` | `agent-lyceum` |
67
66
  | `project` | Registered project name(s) (`-p`); comma-separate to watch several; empty infers from the session's directory | empty |
68
67
  | `intervalSeconds` | How often status is polled | `1` |
68
+ | `autoOpen` | Open the pane when a session starts (it seats only from 144 columns when opened unasked) | `false` |
69
69
 
70
70
  Changes take effect on the next session start. If the pane shows `無法取得狀態:...`, the `command` could not run (usually `dist/cli.js` was not built) or the project name is not registered.
71
71
 
@@ -89,14 +89,14 @@ Changes take effect on the next session start. If the pane shows `無法取得
89
89
  # project.yaml
90
90
  dir: ~/code/web-app
91
91
  team: { lead: lead }
92
- dispatcher: { max_rounds: 30, max_parallel: 1, wake_timeout_sec: 600, retry: 1, strict: false, workspace_mode: auto }
92
+ dispatcher: { max_rounds: 30, max_parallel: 1, wake_timeout_sec: 600, retry: 1, strict: false, workspace_mode: auto, log_max_bytes: 8388608 }
93
93
  agents:
94
94
  lead: { resume: true, can_message: all, can_edit_agent_md: true }
95
95
  fe-member: { runtime: codex, can_message: [lead], owns: ["src/web/**"] }
96
96
  qa-member: { can_message: [lead], owns: ["tests/**"] }
97
97
  ```
98
98
 
99
- 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), `owns` (repo globs).
99
+ 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), `owns` (repo globs).
100
100
 
101
101
  Rules checked by `validate`: ≥ 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.
102
102
 
@@ -125,7 +125,7 @@ Enforcement is layered because no single mechanism is complete:
125
125
 
126
126
  - **Claude Code**: per-run `--settings` with `Edit(...)` rules **plus** the OS sandbox (`allowWrite`/`denyWrite`, `allowUnsandboxedCommands: false`). Without the sandbox, `python -c` and similar bypass Edit rules, so the sandbox is required for the `os` level.
127
127
  - **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.
128
- - **All runtimes**: protected files are hashed before the run; after each wake-up unauthorized changes are reverted, logged, and reported to the lead.
128
+ - **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.
129
129
 
130
130
  `validate` 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.
131
131
 
package/README.zh-TW.md CHANGED
@@ -24,23 +24,22 @@ npx agent-lyceum status
24
24
 
25
25
  | 指令 | 功能 |
26
26
  |---|---|
27
- | `init [-y]` | 建立 home 與全域 agent 庫(`lead`、`fe-member`、`qa-member`)。建立預設路徑前會先詢問。 |
28
- | `project add <name> --dir <repo>` | 註冊專案:建立 `projects/<name>/project.yaml`、共用資料夾與 `COMMON.md`。 |
29
- | `project list` / `project remove <name> [--purge]` | 列出專案/取消註冊(除非加 `--purge`,否則保留 context)。 |
30
- | `validate [-p name] [--task-file f]` | 驗證合併後的設定,並印出每個 agent 的防護等級。有錯誤時 exit 1。 |
31
- | `run ["task"] [--task-file f] [-p name]` | 把任務交給 lead,並執行 dispatcher 直到完成。任務文字與 `--task-file` 擇一提供。 |
32
- | `resume [run-id] [-p name]` | 接續被中斷或失敗的 run(每次 `run` 都是獨立任務;不指定 id 時接續最新一個尚未結束且未在執行的 run,指定 id 則接續該任務):沿用同一個 run 目錄、session 與輪數,不會重送任務,未讀信件會重新處理。run 仍在執行時會拒絕。對已完成(done)的 run 不會繼續任何工作:只印出已記錄的結果狀態與內容,並以該結果對應的碼結束(`0` completed、`2` partial/blocked、`1` failed;在記錄結果狀態之前就結束的舊 run 視為未驗證的 `partial`,回傳 `2`)。因閒置或達 `max_rounds` 而結束的 run,除非補上新信件,否則已沒有未讀信件,接續後會再次以閒置結束(回傳 `2`)。 |
33
- | `status [-p name] [--monitor]` | 顯示 agent、未讀信件、目前正在執行的 agent(耗時、處理中的信件)、上次執行(任務來源、輪數、結束原因、output token 數)。不帶 `--monitor` 時會一併印出最新 run 的完整 wake 紀錄;`--monitor` 會常駐並持續更新,每個 run 只顯示最新三筆 wake。`--task-id <id>` 印出單一任務的完整內容(每次 wake、結果、log 目錄)。`--task-list [project]` 列出專案所有任務(run)的 id、狀態、輪數與任務內容,id 可直接給 `resume` 使用。加上 `--json` 會把同一份報告以 JSON 輸出(`schema_version: 1`、無顏色,stdout 只有 JSON;錯誤走 stderr 並以非零 exit code 結束),可搭配 `--task-list`、`--task-id`。文字與 JSON 由同一份報告產生。 |
34
- | `clear <run-id> [-p name] [--dry-run] [--keep-worktrees]` | 依 id 刪除一個任務(run):它的 run 目錄(狀態、log、結果、信箱)、**任務記憶**,以及各 agent 的 git worktree、branch 與快照 ref。全域與專案層(長期)記憶一律不會動。run 仍在執行,或有 worktree 內的成果不在你的 repo 裡(未提交,或已提交但尚未整合進來)時會拒絕且什麼都不刪,並列出那些成果。`--keep-worktrees` 會保留 worktree 與 branch,其餘照樣清除。`--dry-run` 只列出會刪除與會保留的項目。絕不會順著 symlink 刪到專案 home 之外。進度會記在 `projects/<name>/cleanup/` 的 journal,所以中斷後重跑同一個指令即可完成。id 可用 `status --task-list` 查。 |
35
- | `config show --resolved [-p name] [--json]` | 印出每一項生效的設定(全域 agent 庫、專案檔與預設值合併後的結果)與它的來源:檔案與 key、`default`,或在 runtime 是由 `model` 辨識出來時顯示 `inferred from <model 的 key>`。專案的值覆蓋全域;清單(`can_message`、`owns`)是整個取代而非合併;相對路徑以寫它的那個檔案為基準解析。 |
36
- | `doctor [-p name] [--json]` | 在不執行任何 agent 的前提下檢查環境:設定、git(平行 run 需要)、專案鎖,以及各 runtime CLI 支援什麼(從 `--version` 與 `--help` 讀取:JSON 輸出、resume、sandbox 設定、effort,每項都是 `yes`、`no` 或 `unknown`)。只要團隊需要的功能被明確判定不支援就 exit 1。它不讀取憑證,登入狀態會回報為 unknown。`run` 與 `resume` 啟動前會先做同樣的能力檢查:明確 `no` 就拒絕啟動,`unknown` 則警告。要對真實 CLI 做端到端驗證(跑一個很小的任務)會花 token,刻意保留為手動。 |
37
- | `unlock [-p name] --force` | 移除當機的 run 留下的專案鎖(同一專案同時只能有一個 run)。不加 `--force` 只會顯示鎖的持有者。 |
38
-
39
- **中止 run:** Ctrl-C(或 SIGTERM)會乾淨地取消 run:執行中的 agent 先收到 SIGTERM,5 秒後 SIGKILL(連同整個子程序樹),未讀信件保留、專案鎖釋放,exit code 為 `130`;之後可用 `agent-lyceum resume` 接續。再按一次 Ctrl-C 會立刻結束。超過 `wake_timeout_sec` 的喚醒也以同樣方式停止,並算作一次失敗的嘗試。每次嘗試完整的 stdout/stderr 都寫在 `runs/<run-id>/mail/attempts/<attempt>/log/`,記憶體中每個串流只保留最後 64 KiB。
40
-
41
- **`run`/`resume` 的 exit code:** `0` 只代表 lead 回報 `outcome: completed`;`2` 代表 `partial` 或 `blocked`(run 閒置或達到 `max_rounds` 而沒有 done 也算);`1` 代表 `failed`(lead 本身失敗也算);`130` 代表 `cancelled`。*升級注意:* 舊版 `idle` 結束會回傳 `0`、lead 失敗回傳 `2`;原本把 `0` 當成「run 結束了」的腳本,現在 `0` 的意思是「工作確實完成」。在記錄結果狀態之前就結束的舊 run 會顯示為「未驗證」(`partial`),不會被當成成功。
42
-
43
- 未指定 `--project` 時,會選用 `dir` 為目前目錄最長前綴的已註冊專案;若沒有符合的專案,指令會列出已註冊專案後停止。
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
+ | `validate [-p name] [--task-file f]` | 驗證設定並顯示每個 agent 的防護等級。 [詳細](docs/commands.zh-TW.md#validate) |
31
+ | `run ["task"] [--task-file f] [-p name]` | 把任務交給 lead 並執行到結束。 [詳細](docs/commands.zh-TW.md#run) |
32
+ | `resume [run-id] [-p name]` | 接續被中斷或失敗的 run。 [詳細](docs/commands.zh-TW.md#resume) |
33
+ | `answer <run-id> [--no-edit] [-p name]` | 回答等待中的 run 提出的問題,並接續執行。 [詳細](docs/commands.zh-TW.md#answer) |
34
+ | `status [-p name] [--monitor]` | 顯示 agent、信件、目前執行與歷次 run(支援 `--json`、`--monitor`)。 [詳細](docs/commands.zh-TW.md#status) |
35
+ | `clear <run-id> [-p name] [--dry-run] [--keep-worktrees]` | 刪除 run 與其任務記憶、worktree。 [詳細](docs/commands.zh-TW.md#clear) |
36
+ | `config show --resolved [-p name] [--json]` | 印出每項生效設定與其來源。 [詳細](docs/commands.zh-TW.md#config-show) |
37
+ | `doctor [-p name] [--json]` | 不執行 agent,檢查設定、git、鎖與 runtime CLI。 [詳細](docs/commands.zh-TW.md#doctor) |
38
+ | `memory tidy [-p name] [--agent a] [--layer project\|global] [--dry-run]` | 讓 agent 整理自己的記憶(只能手動觸發;封存而非刪除)。 [詳細](docs/commands.zh-TW.md#memory-tidy) |
39
+ | `memory restore <時間戳> -p name --agent a` | 還原一次整理:把封存的檔案搬回。 [詳細](docs/commands.zh-TW.md#memory-restore) |
40
+ | `unlock [-p name] --force` | 移除當機 run 留下的專案鎖。 [詳細](docs/commands.zh-TW.md#unlock) |
41
+
42
+ 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
43
 
45
44
  ## 監控 mod(Claude Code)
46
45
 
@@ -57,7 +56,7 @@ claude --plugin-dir /path/to/agent-lyceum/plugins/status-monitor
57
56
  **操作。**
58
57
  - **狀態列:** 載入後常駐(第一次輪詢前顯示 `team 讀取中…`,CLI 跑不起來時顯示 `team 無法取得狀態:…`),顯示執行狀態、輪數 `n/max`、output tokens、目前步驟與未讀信件。監看多個專案時顯示一行總覽(`team 1/3 執行中 · a ... | b ...`)。
59
58
  - **通知:** 執行完成、被中斷或某個 agent 喚醒失敗時會跳出訊息(監看多個專案時前面加 `[專案名]`),不需要任何操作。
60
- - **`/status-monitor`:** session 啟動時面板會自動開啟(終端寬度需 144 欄以上才會顯示,狀態列不受影響);也可輸入此指令在任何寬度開啟。每個專案顯示:執行 id 與狀態、任務、進度與清單、正在工作的 agent 與排隊中的信件、受阻的整合、備註與結果摘要、各 runtime 的 output tokens、各 agent 的喚醒統計、最近 8 次喚醒。每個任務(專案目前的執行與最近 3 次過往執行)是一個分頁,點分頁即可切換。面板開著時會依輪詢間隔自動更新,只能點 **關閉** 收起(Esc 不會關閉)。
59
+ - **`/status-monitor`:** 輸入即開啟面板(已開著則關閉)。session 啟動時預設不開,除非設定 `autoOpen`。在 **fullscreen 版面**(`~/.claude/settings.json` 設 `"tui": "fullscreen"` 後重開 session;`/tui fullscreen` 效果相同)且終端寬度 110 欄以上時,面板會靠右 dock 在終端寬度的一半,分頁與 **關閉** 都可以用滑鼠點。否則顯示在輸入框上方;版面在 session 啟動時就固定,從主畫面啟動的 session 不會變。鍵盤操作兩種版面都能用:`x` 關閉,`1`-`9` 或 Tab/Enter 切換任務,Esc 回到輸入框,再執行一次指令也會關閉。每個專案顯示:執行 id 與狀態、任務、進度與清單、正在工作的 agent 與排隊中的信件、受阻的整合、備註與結果摘要、各 runtime 的 output tokens、各 agent 的喚醒統計、最近 8 次喚醒。每個執行中的任務是一個分頁,最新的排最前面;已結束或已中斷的不列出。面板開著時會依輪詢間隔自動更新。
61
60
 
62
61
  **設定。** 在 `~/.claude/settings.json` 的 `pluginConfigs."status-monitor@inline"`(`options`):
63
62
 
@@ -66,6 +65,7 @@ claude --plugin-dir /path/to/agent-lyceum/plugins/status-monitor
66
65
  | `command` | 要執行的 CLI,以空白分隔,例如 `node /path/to/agent-lyceum/dist/cli.js` | `agent-lyceum` |
67
66
  | `project` | 已註冊的專案名稱(`-p`);用逗號分隔可同時監看多個;留空則依 session 所在目錄推斷 | 空 |
68
67
  | `intervalSeconds` | 輪詢狀態的間隔(秒) | `1` |
68
+ | `autoOpen` | session 啟動時自動開啟面板(未經操作開啟時,終端需 144 欄以上才會顯示) | `false` |
69
69
 
70
70
  設定變更要等下次啟動 session 才生效。面板若顯示 `無法取得狀態:...`,通常是 `command` 跑不起來(多半是還沒 build `dist/cli.js`),或專案名稱沒有註冊。
71
71
 
@@ -90,14 +90,14 @@ claude --plugin-dir /path/to/agent-lyceum/plugins/status-monitor
90
90
  # project.yaml
91
91
  dir: ~/code/web-app
92
92
  team: { lead: lead }
93
- dispatcher: { max_rounds: 30, max_parallel: 1, wake_timeout_sec: 600, retry: 1, strict: false, workspace_mode: auto }
93
+ dispatcher: { max_rounds: 30, max_parallel: 1, wake_timeout_sec: 600, retry: 1, strict: false, workspace_mode: auto, log_max_bytes: 8388608 }
94
94
  agents:
95
95
  lead: { resume: true, can_message: all, can_edit_agent_md: true }
96
96
  fe-member: { runtime: codex, can_message: [lead], owns: ["src/web/**"] }
97
97
  qa-member: { can_message: [lead], owns: ["tests/**"] }
98
98
  ```
99
99
 
100
- 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)、`owns`(repo glob)。
100
+ 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)、`owns`(repo glob)。
101
101
 
102
102
  `validate` 檢查的規則:至少 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。
103
103
 
@@ -126,7 +126,7 @@ Agent 欄位:`runtime`(`claude-code`|`codex`;若 `model` 可辨識則可
126
126
 
127
127
  - **Claude Code**:每次執行帶入 `--settings`,內含 `Edit(...)` 規則,**加上** OS 沙箱(`allowWrite`/`denyWrite`、`allowUnsandboxedCommands: false`)。沒有沙箱時,`python -c` 之類的做法能繞過 Edit 規則,因此 `os` 等級必須啟用沙箱。
128
128
  - **Codex**:`-s workspace-write` 搭配 `-c sandbox_workspace_write.writable_roots=[...]`(由 OS 強制)。Codex 無法禁止 repo 內的單一檔案,所以 repo 的 `CLAUDE.md`/`AGENTS.md` 只能靠**事後偵測**保護。
129
- - **所有 runtime**:執行前會對受保護檔案做雜湊;每次喚醒後,未經授權的變更會被還原、記錄,並通報 lead。
129
+ - **所有 runtime**:執行前會對受保護檔案做雜湊;每次喚醒後,未經授權的變更會被還原、記錄,並通報 lead;被寫入的內容會先留存在 `runs/<run-id>/violations/`。
130
130
 
131
131
  `validate` 與 `run` 會依類別(`os`、`tool-rules`、`post-hoc`、`prompt-only`)印出每個 agent 的防護等級,以及它能寫入 repo 之外的哪些路徑。設定 `dispatcher.strict: true` 時,除非記憶、`AGENT.md` 與其他 agent 的 context 都由 OS 強制保護,否則拒絕執行。
132
132
 
@@ -35,6 +35,8 @@ export function buildClaudeSettings(project, agent) {
35
35
  allow.push(rule("Edit", f));
36
36
  const deny = pol.deny.flatMap(denyRules);
37
37
  return {
38
+ // The user's own and plugin hooks (e.g. a hook that asks the user a question) would block every tool call of a headless wake-up.
39
+ disableAllHooks: true,
38
40
  permissions: { allow, deny },
39
41
  sandbox: {
40
42
  enabled: true,
@@ -0,0 +1,10 @@
1
+ export interface PrepareResult {
2
+ complete: boolean;
3
+ problems: string[];
4
+ path: string;
5
+ }
6
+ /** Read the ask-reply file of a waiting run. With `assumeDefaults`, unanswered questions that have a suggestion take it (and the file is saved). */
7
+ export declare function prepareAnswers(runDir: string, opts: {
8
+ assumeDefaults?: boolean;
9
+ }): PrepareResult;
10
+ export declare function openInEditor(file: string, env?: NodeJS.ProcessEnv): void;
@@ -0,0 +1,21 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { applyDefaults, askReplyPath, checkAnswers, describeCheck, isComplete, readAskReply, writeAskReply } from "./ask-reply.js";
3
+ /** Read the ask-reply file of a waiting run. With `assumeDefaults`, unanswered questions that have a suggestion take it (and the file is saved). */
4
+ export function prepareAnswers(runDir, opts) {
5
+ const file = askReplyPath(runDir);
6
+ let reply = readAskReply(runDir);
7
+ if (!reply)
8
+ return { complete: false, problems: ["ask-reply.md does not exist"], path: file };
9
+ if (opts.assumeDefaults) {
10
+ reply = applyDefaults(reply);
11
+ writeAskReply(runDir, reply);
12
+ }
13
+ const check = checkAnswers(reply);
14
+ return { complete: isComplete(check), problems: describeCheck(check), path: file };
15
+ }
16
+ export function openInEditor(file, env = process.env) {
17
+ const editor = env.VISUAL || env.EDITOR || "vi";
18
+ const r = spawnSync(`${editor} ${JSON.stringify(file)}`, { shell: true, stdio: "inherit", env });
19
+ if (r.error || (r.status ?? 0) !== 0)
20
+ throw new Error(`The editor "${editor}" failed${r.error ? `: ${r.error.message}` : ` (exit ${r.status})`}.`);
21
+ }
@@ -0,0 +1,65 @@
1
+ export declare const MAX_QUESTIONS_PER_ASK = 10;
2
+ export declare const ASK_REPLY_FILE = "ask-reply.md";
3
+ export type AnswerSource = "user" | "default";
4
+ export interface AskQuestion {
5
+ id: string;
6
+ text: string;
7
+ options?: string[];
8
+ suggested?: {
9
+ value: string;
10
+ reason?: string;
11
+ };
12
+ /** The agent that asked; the answer goes back to it. */
13
+ asker: string;
14
+ answer?: {
15
+ value: string;
16
+ by: AnswerSource;
17
+ };
18
+ /** The answer was already sent to the asker. */
19
+ delivered?: boolean;
20
+ }
21
+ export interface AskReply {
22
+ status: "pending" | "answered";
23
+ /** Who asked the latest batch. */
24
+ askedBy: string;
25
+ /** The round of the latest batch. */
26
+ round: number;
27
+ questions: AskQuestion[];
28
+ }
29
+ export type NewQuestion = Omit<AskQuestion, "asker" | "answer" | "delivered">;
30
+ export declare const askReplyPath: (runDir: string) => string;
31
+ export declare function parseAskBody(body: string): {
32
+ ok: true;
33
+ questions: NewQuestion[];
34
+ } | {
35
+ ok: false;
36
+ error: string;
37
+ };
38
+ export declare function parseAskReply(text: string): AskReply;
39
+ export declare function renderAskReply(r: AskReply): string;
40
+ export declare function readAskReply(runDir: string): AskReply | undefined;
41
+ export declare function writeAskReply(runDir: string, r: AskReply): void;
42
+ /** Add a batch of questions, keeping everything already in the file. The same id with the same text is skipped (a re-routed ask after a crash). */
43
+ export declare function addBatch(existing: AskReply | undefined, asker: string, round: number, qs: NewQuestion[]): {
44
+ ok: true;
45
+ reply: AskReply;
46
+ } | {
47
+ ok: false;
48
+ error: string;
49
+ };
50
+ export interface AnswerCheck {
51
+ missing: string[];
52
+ invalid: {
53
+ id: string;
54
+ reason: string;
55
+ }[];
56
+ }
57
+ export declare const isComplete: (c: AnswerCheck) => boolean;
58
+ /** Only questions whose answer was not sent yet count. */
59
+ export declare function checkAnswers(r: AskReply): AnswerCheck;
60
+ export declare function describeCheck(c: AnswerCheck): string[];
61
+ export declare function applyDefaults(r: AskReply): AskReply;
62
+ export declare function askersWithAnswers(r: AskReply): string[];
63
+ /** The body of the `reply` mail that carries the answers back to `asker`. */
64
+ export declare function renderAnswers(r: AskReply, asker: string): string;
65
+ export declare function markDelivered(r: AskReply): AskReply;
@@ -0,0 +1,165 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { atomicWrite } from "./fs-util.js";
4
+ export const MAX_QUESTIONS_PER_ASK = 10;
5
+ export const ASK_REPLY_FILE = "ask-reply.md";
6
+ export const askReplyPath = (runDir) => path.join(runDir, "mail", ASK_REPLY_FILE);
7
+ // ---- a tiny reader for the few tags used here (no XML dependency) ----
8
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
9
+ const unesc = (s) => s.replace(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
10
+ function tags(xml, name) {
11
+ const re = new RegExp(`<${name}((?:\\s+[\\w-]+="[^"]*")*)\\s*(?:/>|>([\\s\\S]*?)</${name}>)`, "g");
12
+ return [...xml.matchAll(re)].map((m) => {
13
+ const attrs = {};
14
+ for (const a of (m[1] ?? "").matchAll(/([\w-]+)="([^"]*)"/g))
15
+ attrs[a[1]] = unesc(a[2]);
16
+ return { attrs, inner: m[2] ?? "" };
17
+ });
18
+ }
19
+ const first = (xml, name) => tags(xml, name)[0];
20
+ const textOf = (t) => unesc((t?.inner ?? "").trim());
21
+ // ---- the body of an `ask` mail ----
22
+ export function parseAskBody(body) {
23
+ const block = first(body, "ask");
24
+ if (!block)
25
+ return { ok: false, error: "the body has no <ask>…</ask> block" };
26
+ const found = tags(block.inner, "question");
27
+ if (found.length === 0)
28
+ return { ok: false, error: "the <ask> block needs at least one <question>" };
29
+ if (found.length > MAX_QUESTIONS_PER_ASK)
30
+ return { ok: false, error: `at most ${MAX_QUESTIONS_PER_ASK} questions per ask (got ${found.length}); ask the most important ones first` };
31
+ const seen = new Set();
32
+ const questions = [];
33
+ for (const [i, q] of found.entries()) {
34
+ const id = q.attrs.id?.trim();
35
+ if (!id || !/^[\w.-]+$/.test(id))
36
+ return { ok: false, error: `question ${i + 1} needs an id attribute made of letters, digits, "_", "-" or "."` };
37
+ if (seen.has(id))
38
+ return { ok: false, error: `duplicate question id "${id}"` };
39
+ seen.add(id);
40
+ const text = textOf(first(q.inner, "text"));
41
+ if (!text)
42
+ return { ok: false, error: `question "${id}" needs a non-empty <text>` };
43
+ const options = tags(first(q.inner, "options")?.inner ?? "", "option").map((o) => textOf(o)).filter(Boolean);
44
+ const sug = first(q.inner, "suggested");
45
+ const suggested = sug && textOf(sug) ? { value: textOf(sug), ...(sug.attrs.reason ? { reason: sug.attrs.reason } : {}) } : undefined;
46
+ if (suggested && options.length && !options.includes(suggested.value))
47
+ return { ok: false, error: `the suggested value of question "${id}" must be one of its options` };
48
+ questions.push({ id, text, ...(options.length ? { options } : {}), ...(suggested ? { suggested } : {}) });
49
+ }
50
+ return { ok: true, questions };
51
+ }
52
+ // ---- the ask-reply file ----
53
+ export function parseAskReply(text) {
54
+ const root = first(text, "ask-reply");
55
+ if (!root)
56
+ throw new Error("ask-reply.md has no <ask-reply> root tag");
57
+ const askedBy = root.attrs.asked_by ?? "";
58
+ const questions = tags(root.inner, "question").map((q) => {
59
+ const ans = first(q.inner, "answer");
60
+ const value = textOf(ans);
61
+ const options = tags(first(q.inner, "options")?.inner ?? "", "option").map((o) => textOf(o));
62
+ const sug = first(q.inner, "suggested");
63
+ return {
64
+ id: q.attrs.id ?? "",
65
+ text: textOf(first(q.inner, "text")),
66
+ ...(options.length ? { options } : {}),
67
+ ...(sug && textOf(sug) ? { suggested: { value: textOf(sug), ...(sug.attrs.reason ? { reason: sug.attrs.reason } : {}) } } : {}),
68
+ asker: q.attrs.asker ?? askedBy,
69
+ ...(value ? { answer: { value, by: ans?.attrs.by === "default" ? "default" : "user" } } : {}),
70
+ ...(q.attrs.delivered === "true" ? { delivered: true } : {}),
71
+ };
72
+ });
73
+ return { status: root.attrs.status === "answered" ? "answered" : "pending", askedBy, round: Number(root.attrs.round) || 0, questions };
74
+ }
75
+ export function renderAskReply(r) {
76
+ const out = [`<ask-reply status="${r.status}" asked_by="${esc(r.askedBy)}" round="${r.round}">`];
77
+ for (const q of r.questions) {
78
+ out.push(` <question id="${esc(q.id)}" asker="${esc(q.asker)}"${q.delivered ? ` delivered="true"` : ""}>`);
79
+ out.push(` <text>${esc(q.text)}</text>`);
80
+ if (q.options?.length)
81
+ out.push(` <options>`, ...q.options.map((o) => ` <option>${esc(o)}</option>`), ` </options>`);
82
+ if (q.suggested)
83
+ out.push(` <suggested${q.suggested.reason ? ` reason="${esc(q.suggested.reason)}"` : ""}>${esc(q.suggested.value)}</suggested>`);
84
+ out.push(q.answer ? ` <answer by="${q.answer.by}">${esc(q.answer.value)}</answer>` : ` <answer></answer>`);
85
+ out.push(` </question>`);
86
+ }
87
+ out.push(`</ask-reply>`);
88
+ return out.join("\n") + "\n";
89
+ }
90
+ export function readAskReply(runDir) {
91
+ const file = askReplyPath(runDir);
92
+ if (!fs.existsSync(file))
93
+ return undefined;
94
+ return parseAskReply(fs.readFileSync(file, "utf8"));
95
+ }
96
+ export function writeAskReply(runDir, r) {
97
+ atomicWrite(askReplyPath(runDir), renderAskReply(r));
98
+ }
99
+ // ---- batches, answers ----
100
+ /** Add a batch of questions, keeping everything already in the file. The same id with the same text is skipped (a re-routed ask after a crash). */
101
+ export function addBatch(existing, asker, round, qs) {
102
+ const base = existing ?? { status: "pending", askedBy: asker, round, questions: [] };
103
+ const added = [];
104
+ for (const q of qs) {
105
+ const old = base.questions.find((x) => x.id === q.id);
106
+ if (old) {
107
+ if (old.text === q.text)
108
+ continue;
109
+ return { ok: false, error: `question id "${q.id}" is already used in this run for a different question; use a new id` };
110
+ }
111
+ added.push({ ...q, asker });
112
+ }
113
+ if (added.length === 0)
114
+ return { ok: true, reply: base };
115
+ return { ok: true, reply: { status: "pending", askedBy: asker, round, questions: [...base.questions, ...added] } };
116
+ }
117
+ export const isComplete = (c) => c.missing.length === 0 && c.invalid.length === 0;
118
+ function problem(q) {
119
+ const v = q.answer?.value.trim();
120
+ if (!v)
121
+ return undefined;
122
+ if (q.options?.length && !q.options.includes(v) && !/^other:\s*\S/.test(v))
123
+ return `must be one of: ${q.options.join(", ")} — or start with "other:" and write your own`;
124
+ return undefined;
125
+ }
126
+ /** Only questions whose answer was not sent yet count. */
127
+ export function checkAnswers(r) {
128
+ const c = { missing: [], invalid: [] };
129
+ for (const q of r.questions) {
130
+ if (q.delivered)
131
+ continue;
132
+ if (!q.answer?.value.trim())
133
+ c.missing.push(q.id);
134
+ else {
135
+ const reason = problem(q);
136
+ if (reason)
137
+ c.invalid.push({ id: q.id, reason });
138
+ }
139
+ }
140
+ return c;
141
+ }
142
+ export function describeCheck(c) {
143
+ return [...c.missing.map((id) => `${id}: no answer yet`), ...c.invalid.map((i) => `${i.id}: ${i.reason}`)];
144
+ }
145
+ export function applyDefaults(r) {
146
+ return {
147
+ ...r,
148
+ questions: r.questions.map((q) => (!q.delivered && !q.answer?.value.trim() && q.suggested ? { ...q, answer: { value: q.suggested.value, by: "default" } } : q)),
149
+ };
150
+ }
151
+ export function askersWithAnswers(r) {
152
+ return [...new Set(r.questions.filter((q) => !q.delivered && q.answer?.value.trim()).map((q) => q.asker))];
153
+ }
154
+ /** The body of the `reply` mail that carries the answers back to `asker`. */
155
+ export function renderAnswers(r, asker) {
156
+ const out = [`<answers>`];
157
+ for (const q of r.questions.filter((x) => !x.delivered && x.asker === asker && x.answer?.value.trim())) {
158
+ out.push(` <question id="${esc(q.id)}">`, ` <text>${esc(q.text)}</text>`, ` <answer by="${q.answer?.by}">${esc(q.answer?.value ?? "")}</answer>`, ` </question>`);
159
+ }
160
+ out.push(`</answers>`);
161
+ return out.join("\n");
162
+ }
163
+ export function markDelivered(r) {
164
+ return { ...r, status: "answered", questions: r.questions.map((q) => (q.answer?.value.trim() ? { ...q, delivered: true } : q)) };
165
+ }
@@ -0,0 +1,2 @@
1
+ /** For values the program's own invariants guarantee (never for user input): fail loudly instead of `value!`. */
2
+ export declare function must<T>(value: T | undefined | null, what: string): T;
package/dist/assert.js ADDED
@@ -0,0 +1,6 @@
1
+ /** For values the program's own invariants guarantee (never for user input): fail loudly instead of `value!`. */
2
+ export function must(value, what) {
3
+ if (value === undefined || value === null)
4
+ throw new Error(`internal: ${what} is missing`);
5
+ return value;
6
+ }