agent-lyceum 0.16.0 → 0.16.2
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 +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/format.d.ts +2 -0
- package/dist/format.js +8 -0
- package/dist/run-session.js +8 -4
- package/dist/run-store.d.ts +3 -1
- package/dist/run-store.js +1 -0
- package/dist/status.d.ts +6 -1
- package/dist/status.js +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -146,7 +146,7 @@ Every run has a **name**: made from the first line of the task (lowercase letter
|
|
|
146
146
|
A finished (`done`) run can be reopened with a follow-up request: `agent-lyceum resume [run] "<follow-up>"` (or `--task-file f`). The lead gets the text as a new `task`, and the run goes on with the same run dir, task memory, sessions and lead worktree.
|
|
147
147
|
|
|
148
148
|
- **Which runs:** only a finished run can be reopened. A run that is still running is refused; a run that is not finished is refused when given text (resume it without text first). Without text nothing changes: a finished run just prints its result. Without a run, the candidates are the finished runs, newest first: exactly one is used, none is an error, and several are listed (in a terminal you pick one by number or name; without a terminal the command fails and lists them). A lone argument counts as follow-up text only when it cannot be a run reference (it contains whitespace or a character a run name cannot contain); a one-word follow-up needs `resume <run> "<text>"` or `--task-file`.
|
|
149
|
-
- **What is kept:** the follow-up is saved as `runs/<run-id>/followups/<n>.md`. Each `done` report is kept as `done-<n>.md`, and `result.md` is always the latest. `state.json` gains `name`, `reopened` and `
|
|
149
|
+
- **What is kept:** the follow-up is saved as `runs/<run-id>/followups/<n>.md`. Each `done` report is kept as `done-<n>.md`, and `result.md` is always the latest. `state.json` gains `name`, `reopened`, `round_floor` and `step_base` (all optional, so older runs still load). The progress (`## Steps`) carries on: the checklist at the reopen stays, and the steps the lead lists for the follow-up are added after it (a step it lists again only takes the new tick); for the follow-up's `completed` only its own steps must be ticked. Unresolved blocked integrations stay; the done-contract reminders start afresh for each reopened segment.
|
|
150
150
|
- **Round limit:** `max_rounds` counts from the reopen: `rounds` keeps accumulating, and the limit is the rounds at reopen plus `max_rounds`.
|
|
151
151
|
- **The lead's branch** (runs with a lead worktree): if the branch was already merged into your `HEAD` and the lead worktree is clean and untouched since the run ended, the worktree and the branch (same name, `agent-lyceum/<run-id>/<lead>`) are re-cut from your current `HEAD`, and a line says so. Otherwise the same worktree and branch continue. If a pull request was already recorded (`pr_url`) and the branch was not re-cut, a new `open_pr: true` only pushes ("pull request updated") while that pull request is still open; if it was merged (a squash merge included) or closed, a new pull request is opened instead. If the branch was re-cut, the recorded `pr_url` is cleared (the old URL stays in the run's notes) and a new pull request is opened when asked. agent-lyceum still never merges.
|
|
152
152
|
- **Handoff notes:** before it sends `done`, the lead writes what a follow-up will need into `task-memory/<run-id>/_shared/` in the agent-lyceum home (`MEMORY.md` is the index): decisions and why, interfaces agreed, current state, what is left and the next steps, pitfalls met. Members can read this folder but not write it (a run started with `run --agent` has one agent, which is its own lead and writes it; `call` has none). Once a run is reopened, every wake-up of every agent gets `MEMORY.md` in its prompt (the first 8 KB; a longer index is cut and flagged), and the reopened `task` mail's `Upstream` names it; the first segment's prompts do not contain it. If the lead sends `done` without having changed `MEMORY.md` in that segment, the run is not blocked: a note `shared-not-updated` is added to the run's notes. `clear` deletes the folder with the task memory; `tidy` does not touch it. It holds notes about this task only; lessons that outlive it belong in project or global memory.
|
package/README.zh-TW.md
CHANGED
|
@@ -146,7 +146,7 @@ Agent 欄位:`runtime`(`claude-code`|`codex`;若 `model` 可辨識則可
|
|
|
146
146
|
已完成(`done`)的 run 可以用追加要求重開:`agent-lyceum resume [run] "<追加要求>"`(或 `--task-file f`)。lead 會把這段文字當成新的 `task` 收到,run 沿用同一個 run 目錄、任務記憶、session 與 lead 的 worktree 繼續。
|
|
147
147
|
|
|
148
148
|
- **哪些 run:** 只有已完成的 run 能重開。仍在執行的 run 會拒絕;尚未完成的 run 若帶文字也會拒絕(先不帶文字 resume 它)。不帶文字時行為不變:已完成的 run 只印出結果。沒指定 run 時,候選是已完成的 run,由新到舊:剛好一個就用它,沒有就報錯,有多個就列出(在終端機可用編號或名稱選;非互動環境則報錯並列出候選)。只有一個參數時,只有在它不可能是 run 參照(含空白,或含 run 名稱不能用的字元)才會被當成追加要求;單一個詞的追加要求要寫成 `resume <run> "<text>"` 或用 `--task-file`。
|
|
149
|
-
- **保留什麼:** 追加要求存成 `runs/<run-id>/followups/<n>.md`。每份 `done` 報告保留為 `done-<n>.md`,`result.md` 永遠是最新一份。`state.json` 新增 `name`、`reopened`、`round_floor`(都是選填,舊的 run
|
|
149
|
+
- **保留什麼:** 追加要求存成 `runs/<run-id>/followups/<n>.md`。每份 `done` 報告保留為 `done-<n>.md`,`result.md` 永遠是最新一份。`state.json` 新增 `name`、`reopened`、`round_floor`、`step_base`(都是選填,舊的 run 照樣能讀)。進度(`## Steps`)會接續:重開時的清單保留,lead 為追加要求列的步驟接在後面(再次列出的舊步驟只更新勾選狀態);追加要求的 `completed` 只要求它自己的步驟全部勾選。未解決的 blocked integration 會保留;每段重開的 done 規範提醒重新計算。
|
|
150
150
|
- **輪數上限:** `max_rounds` 從重開時起算:`rounds` 持續累加,上限是重開當下的輪數加 `max_rounds`。
|
|
151
151
|
- **lead 的 branch**(lead 有 worktree 的 run):若該 branch 已合併進你的 `HEAD`,且 lead 的 worktree 乾淨、自 run 結束後沒被動過,worktree 與 branch(同名,`agent-lyceum/<run-id>/<lead>`)會從你目前的 `HEAD` 重新切出,並印一行說明。否則沿用原本的 worktree 與 branch。若已記錄 PR(`pr_url`)且 branch 沒有重切,新的 `open_pr: true` 只會 push(「pull request updated」),而且只在該 PR 仍開啟時;若它已被合併(含 squash merge)或關閉,就改開一個新的 PR;若 branch 重切了,已記錄的 `pr_url` 會被清掉(舊網址留在 run 的 notes),要求時再開一個新的 PR。agent-lyceum 仍然從不合併。
|
|
152
152
|
- **交接筆記:** lead 在送 `done` 之前,把追加要求會用到的內容寫進 agent-lyceum home 裡的 `task-memory/<run-id>/_shared/`(`MEMORY.md` 為索引):做過的決定與理由、介面約定、目前狀態、剩下的事與下一步、踩過的坑。成員可讀不可寫(用 `run --agent` 啟動的 run 只有一個 agent,它就是自己的 lead,所以可寫;`call` 沒有這個資料夾)。run 重開後,每個 agent 的每次喚醒都在 prompt 裡拿到 `MEMORY.md`(前 8 KB,較長的索引會被截斷並標示),重開的 `task` 信的 `Upstream` 也會指向它;第一段的 prompt 不含它。若 lead 在該段沒有更動 `MEMORY.md` 就送出 `done`,run 不會被擋,只會在 run 的 notes 加一筆 `shared-not-updated`。`clear` 會連同任務記憶一併刪除;`tidy` 不會動它。只放這個 task 的筆記,跨 task 的教訓仍放 project 或 global memory。
|
package/dist/format.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface Step {
|
|
|
11
11
|
}
|
|
12
12
|
/** Checklist items (`- [ ] x` / `- [x] x`). With `section`, only those under a `## Steps` heading; otherwise the whole text. */
|
|
13
13
|
export declare function parseSteps(body: string, section?: boolean): Step[];
|
|
14
|
+
/** `base` in its order, with a step the `latest` list re-lists taking its tick from there, then the rest of `latest`. */
|
|
15
|
+
export declare function mergeSteps(base: Step[] | undefined, latest: Step[]): Step[];
|
|
14
16
|
/** One line saying what a message is about: the first line under Changes/Goal, else its first prose line. */
|
|
15
17
|
export declare function briefOf(body: string): string | undefined;
|
|
16
18
|
/** Text under the `##` heading `name` (case-insensitive) up to the next heading, or undefined if there is no such heading. */
|
package/dist/format.js
CHANGED
|
@@ -31,6 +31,14 @@ export function parseSteps(body, section = true) {
|
|
|
31
31
|
done: m[1] !== " ",
|
|
32
32
|
}));
|
|
33
33
|
}
|
|
34
|
+
/** `base` in its order, with a step the `latest` list re-lists taking its tick from there, then the rest of `latest`. */
|
|
35
|
+
export function mergeSteps(base, latest) {
|
|
36
|
+
if (!base?.length)
|
|
37
|
+
return latest;
|
|
38
|
+
const tick = new Map(latest.map((x) => [x.text, x.done]));
|
|
39
|
+
const known = new Set(base.map((x) => x.text));
|
|
40
|
+
return [...base.map((x) => ({ text: x.text, done: tick.get(x.text) ?? x.done })), ...latest.filter((x) => !known.has(x.text))];
|
|
41
|
+
}
|
|
34
42
|
/** One line saying what a message is about: the first line under Changes/Goal, else its first prose line. */
|
|
35
43
|
export function briefOf(body) {
|
|
36
44
|
const lines = body.split(/\r?\n/);
|
package/dist/run-session.js
CHANGED
|
@@ -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, digestOf, doneContract, parseSteps } from "./format.js";
|
|
4
|
+
import { briefOf, digestOf, doneContract, mergeSteps, parseSteps } from "./format.js";
|
|
5
5
|
import { ProtectedGuard } from "./guard.js";
|
|
6
6
|
import { addBatch, askersWithAnswers, askReplyPath, checkAnswers, describeCheck, isComplete, markDelivered, parseAskBody, readAskReply, renderAnswers, resolveOptionLetters, writeAskReply } from "./ask-reply.js";
|
|
7
7
|
import { deliver, deliverOnce, ensureProjectDirs, finishDone, listUnread, rejectDone, routeOutboxes, unreadFiles } from "./mailbox.js";
|
|
@@ -143,6 +143,8 @@ export class RunSession {
|
|
|
143
143
|
state.max_rounds = roundLimit(state, cfg.max_rounds);
|
|
144
144
|
state.result_branch = undefined;
|
|
145
145
|
state.done_rejections = 0;
|
|
146
|
+
// The progress so far stays shown: the follow-up's own steps are added after it.
|
|
147
|
+
state.step_base = resume.steps;
|
|
146
148
|
if (recut && resume.pr_url) {
|
|
147
149
|
// The recorded pull request is the merged branch's: it must not be mistaken for the new segment's.
|
|
148
150
|
state.notes = [...(resume.notes ?? []), `The earlier pull request ${resume.pr_url} belongs to the merged branch; a new one is opened for the follow-up work.`];
|
|
@@ -193,7 +195,7 @@ export class RunSession {
|
|
|
193
195
|
to: project.lead,
|
|
194
196
|
type: "task",
|
|
195
197
|
subject: opts.followup.text.split("\n").find((l) => l.trim())?.slice(0, 80) ?? "Follow-up",
|
|
196
|
-
body: `## Goal\n${opts.followup.text.trimEnd()}\n\n## Acceptance criteria\nAs stated in the goal; the earlier result still holds unless the goal changes it.\n\n## Scope\nThis run's earlier work and files.\n\n## Upstream\nThe previous final report is in \`${path.join(runDir, `done-${n}.md`)}\`; the original task is in \`${path.join(runDir, "task.md")}\`.${handoffLine}\n`,
|
|
198
|
+
body: `## Goal\n${opts.followup.text.trimEnd()}\n\n## Acceptance criteria\nAs stated in the goal; the earlier result still holds unless the goal changes it.\n\n## Scope\nThis run's earlier work and files.\n\n## Upstream\nThe previous final report is in \`${path.join(runDir, `done-${n}.md`)}\`; the original task is in \`${path.join(runDir, "task.md")}\`.${handoffLine}\n${state.step_base?.length ? "\n## Progress\nThe earlier checklist stays in the progress; your `## Steps` lists only this follow-up's steps, which are shown after it.\n" : ""}`,
|
|
197
199
|
});
|
|
198
200
|
log("reopen", { n });
|
|
199
201
|
}
|
|
@@ -468,7 +470,9 @@ export class RunSession {
|
|
|
468
470
|
// A done that breaks the completion contract goes back to the lead (a limited number of times).
|
|
469
471
|
let contract;
|
|
470
472
|
if (route.done) {
|
|
471
|
-
|
|
473
|
+
// Steps from before the last reopen were judged by the earlier done; only this segment's must be ticked.
|
|
474
|
+
const earlier = new Set((state.step_base ?? []).map((x) => x.text));
|
|
475
|
+
contract = doneContract(route.done.meta, route.done.body, state.steps?.filter((x) => !earlier.has(x.text)));
|
|
472
476
|
if (contract.missing.length && (state.done_rejections ?? 0) < MAX_DONE_REJECTIONS) {
|
|
473
477
|
state.done_rejections = (state.done_rejections ?? 0) + 1;
|
|
474
478
|
const list = contract.missing.map((m) => `- ${m}`).join("\n");
|
|
@@ -501,7 +505,7 @@ export class RunSession {
|
|
|
501
505
|
if (route.done)
|
|
502
506
|
handedOff(route.done.from, { to: "(done)", type: "done", subject: route.done.subject });
|
|
503
507
|
if (route.steps)
|
|
504
|
-
state.steps = route.steps;
|
|
508
|
+
state.steps = mergeSteps(state.step_base, route.steps);
|
|
505
509
|
this.saveState();
|
|
506
510
|
// Output is routed: only now is the input these wake-ups handled consumed.
|
|
507
511
|
for (const a of batch) {
|
package/dist/run-store.d.ts
CHANGED
|
@@ -97,8 +97,10 @@ export interface RunState {
|
|
|
97
97
|
active: Record<string, ActiveWake>;
|
|
98
98
|
/** Finished wakes, oldest first. */
|
|
99
99
|
wakes: WakeRecord[];
|
|
100
|
-
/** Checklist progress: seeded from the task, then replaced by the lead's latest `## Steps
|
|
100
|
+
/** Checklist progress: seeded from the task, then replaced by the lead's latest `## Steps` (after a reopen, added to `step_base`). */
|
|
101
101
|
steps?: Step[];
|
|
102
|
+
/** The checklist as it stood at the last reopen: the follow-up's steps are shown after it, and its open steps do not hold up the follow-up's done. */
|
|
103
|
+
step_base?: Step[];
|
|
102
104
|
/** Readable name; lets `resume`, `clear` and `status` find the run without its id. Absent on runs from older versions. */
|
|
103
105
|
name?: string;
|
|
104
106
|
/** One entry per time a finished run was reopened with a follow-up. */
|
package/dist/run-store.js
CHANGED
|
@@ -39,6 +39,7 @@ const WireState = z
|
|
|
39
39
|
wakes: z.array(z.any()).optional(),
|
|
40
40
|
solo_agent: z.string().optional(),
|
|
41
41
|
steps: z.array(z.object({ text: z.string(), done: z.boolean() })).optional(),
|
|
42
|
+
step_base: z.array(z.object({ text: z.string(), done: z.boolean() })).optional(),
|
|
42
43
|
name: z.string().optional(),
|
|
43
44
|
reopened: z.array(z.object({ at: z.string() })).optional(),
|
|
44
45
|
round_floor: nonNeg.optional(),
|
package/dist/status.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { type ActiveWake, type EndReason, type RunState, type WakeRecord } from
|
|
|
2
2
|
import type { RunOutcome } from "./schema.js";
|
|
3
3
|
import type { ResolvedProject } from "./config.js";
|
|
4
4
|
import { type DigestSection, type Step } from "./format.js";
|
|
5
|
+
/** The run to show: the newest one still running (a resumed run keeps its old id, so newer runs may sit above it), else the newest run. */
|
|
6
|
+
export declare function currentRun(project: ResolvedProject): {
|
|
7
|
+
dir: string;
|
|
8
|
+
state: RunState;
|
|
9
|
+
} | undefined;
|
|
5
10
|
export declare function latestRun(project: ResolvedProject): {
|
|
6
11
|
dir: string;
|
|
7
12
|
state: RunState;
|
|
@@ -140,7 +145,7 @@ export declare function buildRunReport(run: {
|
|
|
140
145
|
}, project?: ResolvedProject, opts?: {
|
|
141
146
|
detail?: boolean;
|
|
142
147
|
}): RunReport;
|
|
143
|
-
/** The status of a project: its agents and one run (the newest, or the one named by `runId`). */
|
|
148
|
+
/** The status of a project: its agents and one run (the newest running one, else the newest, or the one named by `runId`). */
|
|
144
149
|
export declare function buildStatusReport(base: ResolvedProject, runId?: string): StatusReport;
|
|
145
150
|
/** Every run of the project, newest first. */
|
|
146
151
|
export declare function buildTaskListReport(project: ResolvedProject): TaskListReport;
|
package/dist/status.js
CHANGED
|
@@ -7,6 +7,11 @@ import { briefOf, digestOf } from "./format.js";
|
|
|
7
7
|
import { listUnread } from "./mailbox.js";
|
|
8
8
|
import { tidyAdvice } from "./memory-tidy.js";
|
|
9
9
|
import { inspectRunLock, lockHolderAlive } from "./project-lock.js";
|
|
10
|
+
/** The run to show: the newest one still running (a resumed run keeps its old id, so newer runs may sit above it), else the newest run. */
|
|
11
|
+
export function currentRun(project) {
|
|
12
|
+
const running = listRuns(project, Infinity).find((r) => !r.state.end_reason && runIsAlive(r.state, project));
|
|
13
|
+
return running ?? latestRun(project);
|
|
14
|
+
}
|
|
10
15
|
export function latestRun(project) {
|
|
11
16
|
const root = project.paths.runs;
|
|
12
17
|
if (!fs.existsSync(root))
|
|
@@ -220,7 +225,7 @@ export function buildRunReport(run, project, opts = {}) {
|
|
|
220
225
|
};
|
|
221
226
|
}
|
|
222
227
|
const projectInfo = (p) => ({ name: p.name, dir: p.dir, lead: p.lead });
|
|
223
|
-
/** The status of a project: its agents and one run (the newest, or the one named by `runId`). */
|
|
228
|
+
/** The status of a project: its agents and one run (the newest running one, else the newest, or the one named by `runId`). */
|
|
224
229
|
export function buildStatusReport(base, runId) {
|
|
225
230
|
let run;
|
|
226
231
|
if (runId) {
|
|
@@ -230,7 +235,7 @@ export function buildStatusReport(base, runId) {
|
|
|
230
235
|
run = { dir, state: loadRunState(dir) };
|
|
231
236
|
}
|
|
232
237
|
else
|
|
233
|
-
run =
|
|
238
|
+
run = currentRun(base);
|
|
234
239
|
// Mail is per run: count the unread mail of the run being shown.
|
|
235
240
|
const project = run ? bindRunProject(base, run.dir, run.state.mail_layout) : base;
|
|
236
241
|
const report = run ? buildRunReport(run, base) : undefined;
|