agent-lyceum 0.9.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -65,7 +65,7 @@ claude --plugin-dir /path/to/agent-lyceum/plugins/status-monitor
65
65
 
66
66
  **Use it.**
67
67
  - **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.
68
- - **`/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.
68
+ - **`/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, `v` switches graph/text, `1`-`9` or Tab/Enter switch task, Esc returns to the prompt, running the command again also closes. It draws one block per agent, with the links between the lead and each member taken from `can_message` (`▼` the lead may write to the member, `▲` the member to the lead, `↕` both; yellow marks the latest hand-off). Each block shows what that agent is doing now, the mail it is handling and the mail queued for it as short readable sections by type (task: goal and acceptance criteria; reply: changes, verification, open items, risks; done: result and what is not done), and its wake count and output tokens. Above the graph: the run id and state, task, progress bar and checklist; below it: blocked integrations, notes and the result summary. `v` switches to the plain text list (per-runtime tokens, per-agent statistics, the last 8 wakes). Every block lists the agents it may message in a "→" line, with the latest hand-off marked "●" (this also covers member-to-member targets, which get no glyph). When the graph area is narrower than 70 columns (compared with the pane's own width, as the host reports it), the blocks stack in one column and the link glyphs are left out. Each running task is a tab, newest first; finished and interrupted runs are not listed. It refreshes with the polling interval while open.
69
69
 
70
70
  **Configure it.** In `~/.claude/settings.json` under `pluginConfigs."status-monitor@inline"` (`options`):
71
71
 
package/README.zh-TW.md CHANGED
@@ -65,7 +65,7 @@ claude --plugin-dir /path/to/agent-lyceum/plugins/status-monitor
65
65
 
66
66
  **操作。**
67
67
  - **通知:** 執行完成、被中斷或某個 agent 喚醒失敗時會跳出訊息(監看多個專案時前面加 `[專案名]`),不需要任何操作。
68
- - **`/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 次喚醒。每個執行中的任務是一個分頁,最新的排最前面;已結束或已中斷的不列出。面板開著時會依輪詢間隔自動更新。
68
+ - **`/status-monitor`:** 輸入即開啟面板(已開著則關閉)。session 啟動時預設不開,除非設定 `autoOpen`。在 **fullscreen 版面**(`~/.claude/settings.json` 設 `"tui": "fullscreen"` 後重開 session;`/tui fullscreen` 效果相同)且終端寬度 110 欄以上時,面板會靠右 dock 在終端寬度的一半,分頁與 **關閉** 都可以用滑鼠點。否則顯示在輸入框上方;版面在 session 啟動時就固定,從主畫面啟動的 session 不會變。鍵盤操作兩種版面都能用:`x` 關閉,`v` 切換圖形/文字,`1`-`9` 或 Tab/Enter 切換任務,Esc 回到輸入框,再執行一次指令也會關閉。面板為每個 agent 畫一個區塊,並依 `can_message` 在 lead 與各成員之間畫出連結(`▼` lead 可寄給成員、`▲` 成員可寄給 lead、`↕` 雙向;黃色表示最近一次寄信的那條邊)。每個區塊顯示該 agent 現在在做什麼、正在處理與排隊中的信(依信種整理成可讀的重點:task 看目標與驗收,reply 看變更、驗證、待辦與風險,done 看結果與未完成),以及喚醒次數與輸出 token。圖形上方是執行 id 與狀態、任務、進度條與清單;下方是受阻的整合、備註與結果摘要。按 `v` 切換成純文字清單(各 runtime 的 token、各 agent 統計、最近 8 次喚醒)。每個區塊以「→」一行列出它可以寄信的對象,最近一次寄信的對象標 「●」(成員之間的對象也在此,不畫符號)。當圖形區域寬度少於 70 欄(以面板本身的寬度比較,由 host 回報),區塊改成單欄堆疊,不畫連結符號。每個執行中的任務是一個分頁,最新的排最前面;已結束或已中斷的不列出。面板開著時會依輪詢間隔自動更新。
69
69
 
70
70
  **設定。** 在 `~/.claude/settings.json` 的 `pluginConfigs."status-monitor@inline"`(`options`):
71
71
 
package/dist/format.d.ts CHANGED
@@ -31,3 +31,14 @@ export interface DoneContract {
31
31
  * (the report's own `## Steps`, else the run's latest `stateSteps`).
32
32
  */
33
33
  export declare function doneContract(meta: Record<string, unknown>, body: string, stateSteps?: Step[]): DoneContract;
34
+ export interface DigestSection {
35
+ label: string;
36
+ lines: string[];
37
+ }
38
+ /**
39
+ * What a message is about, as short readable sections for the status panel: by type, the lines under the
40
+ * sections that matter (task: Goal, Acceptance criteria; reply: Changes, Verification, Open items, Risks;
41
+ * done: Result, Not done; ask: each question; failure: the first paragraph). With none of those, the one-line brief.
42
+ * Never throws: a body it cannot read gives [].
43
+ */
44
+ export declare function digestOf(type: string, body: string): DigestSection[];
package/dist/format.js CHANGED
@@ -100,3 +100,83 @@ export function doneContract(meta, body, stateSteps) {
100
100
  : undefined;
101
101
  return { outcome, missing, verification };
102
102
  }
103
+ const DIGEST_MAX_LINES = 3;
104
+ const DIGEST_MAX_CHARS = 80;
105
+ /** Which `##` sections say what a message of each type is about, and the label the panel shows for them. */
106
+ const DIGEST_SECTIONS = {
107
+ task: [
108
+ { name: "Goal", label: "目標" },
109
+ { name: "Acceptance criteria", label: "驗收" },
110
+ ],
111
+ reply: [
112
+ { name: "Changes", label: "變更" },
113
+ { name: "Verification", label: "驗證" },
114
+ { name: "Open items", label: "待辦" },
115
+ { name: "Risks", label: "風險" },
116
+ ],
117
+ done: [
118
+ { name: "Result", label: "結果" },
119
+ { name: "Not done", label: "未完成" },
120
+ ],
121
+ };
122
+ function digestLine(l) {
123
+ const t = l
124
+ .trim()
125
+ .replace(/^[-*]\s+(\[[ xX]\]\s+)?/, "")
126
+ .replace(/\*\*|`/g, "")
127
+ .trim();
128
+ return t.length > DIGEST_MAX_CHARS ? `${t.slice(0, DIGEST_MAX_CHARS - 1)}…` : t;
129
+ }
130
+ /** Readable lines of a block of text: markdown marks gone, empty / heading / warning / `None` lines dropped, at most 3 lines. */
131
+ function digestLines(text) {
132
+ const lines = text
133
+ .split(/\r?\n/)
134
+ .map(digestLine)
135
+ .filter((l) => l !== "" && !/^#+\s/.test(l) && !l.startsWith("[agent-lyceum]") && !/^none\.?$/i.test(l));
136
+ if (lines.length <= DIGEST_MAX_LINES)
137
+ return lines;
138
+ const last = lines[DIGEST_MAX_LINES - 1] ?? "";
139
+ const cut = last.endsWith("…") ? last : `${last.slice(0, last.length >= DIGEST_MAX_CHARS ? DIGEST_MAX_CHARS - 1 : last.length)}…`;
140
+ return [...lines.slice(0, DIGEST_MAX_LINES - 1), cut];
141
+ }
142
+ /**
143
+ * What a message is about, as short readable sections for the status panel: by type, the lines under the
144
+ * sections that matter (task: Goal, Acceptance criteria; reply: Changes, Verification, Open items, Risks;
145
+ * done: Result, Not done; ask: each question; failure: the first paragraph). With none of those, the one-line brief.
146
+ * Never throws: a body it cannot read gives [].
147
+ */
148
+ export function digestOf(type, body) {
149
+ try {
150
+ const out = [];
151
+ if (type === "ask") {
152
+ for (const m of body.matchAll(/<question\b[^>]*?\bid="([^"]*)"[^>]*>[\s\S]*?<text>([\s\S]*?)<\/text>/g)) {
153
+ const lines = digestLines(m[2] ?? "");
154
+ if (lines.length)
155
+ out.push({ label: m[1] ?? "問題", lines });
156
+ }
157
+ }
158
+ else if (type === "failure") {
159
+ const first = body
160
+ .split(/\n[ \t]*\n/)
161
+ .map(digestLines)
162
+ .find((l) => l.length);
163
+ if (first)
164
+ out.push({ label: "訊息", lines: first });
165
+ }
166
+ else {
167
+ for (const s of DIGEST_SECTIONS[type] ?? []) {
168
+ const text = sectionText(body, s.name);
169
+ const lines = text === undefined ? [] : digestLines(text);
170
+ if (lines.length)
171
+ out.push({ label: s.label, lines });
172
+ }
173
+ }
174
+ if (out.length)
175
+ return out;
176
+ const brief = briefOf(body);
177
+ return brief ? [{ label: "摘要", lines: [digestLine(brief)] }] : [];
178
+ }
179
+ catch {
180
+ return [];
181
+ }
182
+ }
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { realInvoker } from "./adapters/index.js";
4
- import { briefOf, doneContract, parseSteps } from "./format.js";
4
+ import { briefOf, digestOf, doneContract, parseSteps } from "./format.js";
5
5
  import { ProtectedGuard } from "./guard.js";
6
6
  import { addBatch, askersWithAnswers, askReplyPath, checkAnswers, describeCheck, isComplete, markDelivered, parseAskBody, readAskReply, renderAnswers, writeAskReply } from "./ask-reply.js";
7
7
  import { deliver, deliverOnce, ensureProjectDirs, finishDone, listUnread, rejectDone, routeOutboxes, unreadFiles } from "./mailbox.js";
@@ -222,7 +222,7 @@ export class RunSession {
222
222
  const att = beginAttempt(runDir, claim.id);
223
223
  say(`[round ${state.rounds}/${cfg.max_rounds}] waking ${agent.name} (${agent.runtime})${attempt ? ` — retry ${attempt}` : ""}`);
224
224
  const sessionId = agent.resume ? state.sessions[agent.name] : undefined;
225
- const handling = unread.map((m) => ({ from: m.meta.from, type: m.meta.type, subject: m.meta.subject, brief: briefOf(m.body) }));
225
+ const handling = unread.map((m) => ({ from: m.meta.from, type: m.meta.type, subject: m.meta.subject, brief: briefOf(m.body), digest: digestOf(m.meta.type, m.body) }));
226
226
  state.active[agent.name] = { round, since: new Date(started).toISOString(), handling };
227
227
  this.saveState();
228
228
  result = await invoke({ ...base, signal: opts.signal, logDir: attemptLogDir(runDir, att.id), userPrompt: attempt ? retryPrompt(base.userPrompt, result?.error) : base.userPrompt, sessionId }).catch((e) => ({
@@ -1,5 +1,5 @@
1
1
  import type { ResolvedProject } from "./config.js";
2
- import type { Step } from "./format.js";
2
+ import type { DigestSection, Step } from "./format.js";
3
3
  import type { RunOutcome } from "./schema.js";
4
4
  export declare const STATE_FILE = "state.json";
5
5
  export declare const STATE_SCHEMA_VERSION = 2;
@@ -11,6 +11,8 @@ export interface WakeTopic {
11
11
  subject: string;
12
12
  /** What the message is about, in one line. */
13
13
  brief?: string;
14
+ /** The same, as short readable sections by message type (see `digestOf`). */
15
+ digest?: DigestSection[];
14
16
  }
15
17
  export interface SentTopic {
16
18
  to: string;
package/dist/status.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type ActiveWake, type EndReason, type RunState, type WakeRecord } from "./run-store.js";
2
2
  import type { RunOutcome } from "./schema.js";
3
3
  import type { ResolvedProject } from "./config.js";
4
- import type { Step } from "./format.js";
4
+ import { type DigestSection, type Step } from "./format.js";
5
5
  export declare function latestRun(project: ResolvedProject): {
6
6
  dir: string;
7
7
  state: RunState;
@@ -39,6 +39,8 @@ export interface QueueItem {
39
39
  from: string;
40
40
  type: string;
41
41
  subject: string;
42
+ brief?: string;
43
+ digest?: DigestSection[];
42
44
  }
43
45
  export interface RunReport {
44
46
  run_id: string;
@@ -95,6 +97,8 @@ export interface AgentReport {
95
97
  runtime?: string;
96
98
  lead: boolean;
97
99
  unread: number;
100
+ /** Who this agent may send mail to (`all` expanded, itself left out). */
101
+ can_message: string[];
98
102
  last_wake?: {
99
103
  at: string;
100
104
  ok: boolean;
package/dist/status.js CHANGED
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { askReplyPath, readAskReply } from "./ask-reply.js";
4
4
  import { RESULT_FILE } from "./dispatcher.js";
5
5
  import { bindRunProject, loadRunState, outcomeOf } from "./run-store.js";
6
+ import { briefOf, digestOf } from "./format.js";
6
7
  import { listUnread } from "./mailbox.js";
7
8
  import { tidyAdvice } from "./memory-tidy.js";
8
9
  import { inspectProjectLock, lockHolderAlive } from "./project-lock.js";
@@ -147,7 +148,14 @@ function queueOf(project) {
147
148
  return x.agent === project.lead ? -1 : 1;
148
149
  return path.basename(x.msg.file).localeCompare(path.basename(y.msg.file));
149
150
  });
150
- return rows.map((r) => ({ agent: r.agent, from: r.msg.meta.from, type: r.msg.meta.type, subject: r.msg.meta.subject }));
151
+ return rows.map((r) => ({
152
+ agent: r.agent,
153
+ from: r.msg.meta.from,
154
+ type: r.msg.meta.type,
155
+ subject: r.msg.meta.subject,
156
+ brief: briefOf(r.msg.body),
157
+ digest: digestOf(r.msg.meta.type, r.msg.body),
158
+ }));
151
159
  }
152
160
  /** One run as `status` sees it. Without `project`, liveness falls back to the recorded pid and there is no queue. */
153
161
  export function buildRunReport(run, project, opts = {}) {
@@ -236,6 +244,7 @@ export function buildStatusReport(base, runId) {
236
244
  runtime: a.runtime,
237
245
  lead: a.name === project.lead,
238
246
  unread: listUnread(project, a.name).length,
247
+ can_message: (a.canMessage === "all" ? Object.keys(project.agents) : a.canMessage).filter((n) => n !== a.name),
239
248
  last_wake: run?.state.last_wake[a.name],
240
249
  })),
241
250
  run: report,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-lyceum",
3
- "version": "0.9.2",
3
+ "version": "0.11.0",
4
4
  "description": "Config + dispatcher for teams of Claude Code / Codex agents with scoped context and file-based mailboxes",
5
5
  "keywords": [
6
6
  "ai",