agent-lyceum 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/status.js CHANGED
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { askReplyPath, readAskReply } from "./ask-reply.js";
4
4
  import { RESULT_FILE } from "./dispatcher.js";
5
- import { bindRunProject, loadRunState, outcomeOf } from "./run-store.js";
5
+ import { bindRunProject, loadRunState, outcomeOf, runLabel } from "./run-store.js";
6
6
  import { briefOf, digestOf } from "./format.js";
7
7
  import { listUnread } from "./mailbox.js";
8
8
  import { tidyAdvice } from "./memory-tidy.js";
@@ -33,6 +33,10 @@ export function latestRun(project) {
33
33
  export function latestUnfinishedRun(project) {
34
34
  return listRuns(project, Infinity).find((r) => r.state.end_reason !== "done" && (r.state.end_reason || !runIsAlive(r.state, project)));
35
35
  }
36
+ /** Runs a follow-up text can reopen: the finished ones (end_reason "done"). Newest first. */
37
+ export function reopenCandidates(project) {
38
+ return listRuns(project, Infinity).filter((r) => r.state.end_reason === "done");
39
+ }
36
40
  export function listRuns(project, limit) {
37
41
  const root = project.paths.runs;
38
42
  if (!fs.existsSync(root))
@@ -182,6 +186,7 @@ export function buildRunReport(run, project, opts = {}) {
182
186
  }
183
187
  return {
184
188
  run_id: s.run_id,
189
+ name: s.name,
185
190
  dir: run.dir,
186
191
  state: s.end_reason === "waiting" ? "waiting" : s.end_reason ? "ended" : live ? "running" : "interrupted",
187
192
  live,
@@ -279,7 +284,7 @@ function summaryBlock(run, now, c) {
279
284
  const s = run;
280
285
  const live = s.live;
281
286
  const lines = [
282
- `${c.bold("執行 " + s.run_id)} [${stateColor(c, reportLabel(s))}] 第 ${s.rounds}/${s.max_rounds} 輪`,
287
+ `${c.bold("執行 " + runLabel(s))} [${stateColor(c, reportLabel(s))}] 第 ${s.rounds}/${s.max_rounds} 輪`,
283
288
  `${L("任務:")}${taskText(s)}`,
284
289
  ];
285
290
  const steps = stepsLine(s.steps, c, s.end_reason === "done");
@@ -317,7 +322,7 @@ function summaryBlock(run, now, c) {
317
322
  /** What a run is doing / did, one line per wake. */
318
323
  function describeRun(s, now, c, limit = Infinity) {
319
324
  const lines = [
320
- `${c.bold("執行 " + s.run_id)} [${stateColor(c, reportLabel(s))}] 輪次 ${s.rounds}/${s.max_rounds} 輸出 tokens ${tokens(s.output_tokens)}`,
325
+ `${c.bold("執行 " + runLabel(s))} [${stateColor(c, reportLabel(s))}] 輪次 ${s.rounds}/${s.max_rounds} 輸出 tokens ${tokens(s.output_tokens)}`,
321
326
  ` 任務:${taskText(s)}`,
322
327
  ];
323
328
  const steps = stepsLine(s.steps, c);
@@ -383,10 +388,12 @@ export function formatTaskListReport(report, color = false) {
383
388
  const head = `${c.bold("專案:")}${report.project.name} ${c.dim(`(共 ${runs.length} 個任務)`)}`;
384
389
  if (!runs.length)
385
390
  return `${head}\n\n無執行紀錄。`;
386
- const header = ["ID", "狀態", "輪次", "目前步驟", "任務"];
391
+ const named = runs.some((s) => s.name);
392
+ const header = [...(named ? ["名稱"] : []), "ID", "狀態", "輪次", "目前步驟", "任務"];
387
393
  const rows = [header];
388
394
  for (const s of runs) {
389
395
  rows.push([
396
+ ...(named ? [s.name ?? ""] : []),
390
397
  s.run_id,
391
398
  reportLabel(s),
392
399
  `${s.rounds}/${s.max_rounds}`,
@@ -398,7 +405,7 @@ export function formatTaskListReport(report, color = false) {
398
405
  const line = (r, ri) => r
399
406
  .map((t, i) => {
400
407
  const p = i === r.length - 1 ? t : padCols(t, w[i] ?? 0);
401
- return ri === 0 ? c.bold(p) : i === 1 ? stateColor(c, t) + " ".repeat(Math.max(0, (w[i] ?? 0) - cols(t))) : p;
408
+ return ri === 0 ? c.bold(p) : i === (named ? 2 : 1) ? stateColor(c, t) + " ".repeat(Math.max(0, (w[i] ?? 0) - cols(t))) : p;
402
409
  })
403
410
  .join(" ")
404
411
  .trimEnd();
package/dist/validate.js CHANGED
@@ -100,8 +100,8 @@ export function validateProject(project) {
100
100
  const enforcement = Object.values(project.agents).map((a) => enforcementFor(project, a));
101
101
  if (project.dispatcher.strict) {
102
102
  for (const e of enforcement) {
103
- for (const k of ["memory", "agentMd", "otherContext"]) {
104
- if (e[k] !== "os")
103
+ for (const k of ["memory", "agentMd", "otherContext", "shared"]) {
104
+ if (e[k] !== "os" && !(k === "shared" && e[k] === "n/a"))
105
105
  err(`strict: agent "${e.agent}" ${k} protection is "${e[k]}", not OS-enforced.`);
106
106
  }
107
107
  }
@@ -109,10 +109,10 @@ export function validateProject(project) {
109
109
  return { issues, enforcement, ok: !issues.some((i) => i.level === "error") };
110
110
  }
111
111
  export function formatEnforcement(list) {
112
- const header = ["agent", "runtime", "memory", "AGENT.md", "others' ctx", "repo CLAUDE/AGENTS", "owns"];
112
+ const header = ["agent", "runtime", "memory", "AGENT.md", "others' ctx", "handoff", "repo CLAUDE/AGENTS", "owns"];
113
113
  const rows = [header];
114
114
  for (const e of list)
115
- rows.push([e.agent, e.runtime, e.memory, e.agentMd, e.otherContext, e.repoInstructions, e.owns]);
115
+ rows.push([e.agent, e.runtime, e.memory, e.agentMd, e.otherContext, e.shared, e.repoInstructions, e.owns]);
116
116
  const w = header.map((_, i) => Math.max(...rows.map((r) => (r[i] ?? "").length)));
117
117
  const lines = rows.map((r) => r.map((c, i) => c.padEnd(w[i] ?? 0)).join(" ").trimEnd());
118
118
  lines.splice(1, 0, w.map((n) => "-".repeat(n)).join(" "));
@@ -162,6 +162,16 @@ export interface ResultBranch {
162
162
  }
163
163
  /** Commit the lead's checkout on its branch (once, at the end of a run) and write what changed as a patch. Never touches your repo's working tree. */
164
164
  export declare function finishLeadBranch(ws: AgentWorkspace, message: string, patchFile: string): ResultBranch;
165
+ /**
166
+ * A reopened run whose lead branch was already merged into your HEAD starts its next segment on a fresh branch cut from HEAD,
167
+ * so the merged work is not offered again. Only when nothing would be lost: the branch's last committed head (`resultHead`) is
168
+ * in HEAD, the worktree is still at that head, and it holds no uncommitted work and nothing whose state is unknown.
169
+ * Otherwise the worktree and branch are returned unchanged. Your working tree, index and HEAD are never touched.
170
+ */
171
+ export declare function recutLeadWorkspace(project: ResolvedProject, runId: string, agent: string, ws: AgentWorkspace, resultHead: string | undefined): {
172
+ ws: AgentWorkspace;
173
+ recut: boolean;
174
+ };
165
175
  /** Names of the agents that have a worktree for this run. */
166
176
  export declare function listWorkspaceAgents(project: ResolvedProject, runId: string): string[];
167
177
  /** The snapshot refs (`refs/agent-lyceum/<run>/...`) of a run. */
package/dist/worktree.js CHANGED
@@ -686,6 +686,31 @@ export function finishLeadBranch(ws, message, patchFile) {
686
686
  fs.writeFileSync(patchFile, agentGit(ws, ["diff", "--binary", "--full-index", ws.base, head]) + "\n");
687
687
  return { branch: ws.branch, head, base: ws.base, files: files.length, patch: files.length ? patchFile : undefined };
688
688
  }
689
+ /**
690
+ * A reopened run whose lead branch was already merged into your HEAD starts its next segment on a fresh branch cut from HEAD,
691
+ * so the merged work is not offered again. Only when nothing would be lost: the branch's last committed head (`resultHead`) is
692
+ * in HEAD, the worktree is still at that head, and it holds no uncommitted work and nothing whose state is unknown.
693
+ * Otherwise the worktree and branch are returned unchanged. Your working tree, index and HEAD are never touched.
694
+ */
695
+ export function recutLeadWorkspace(project, runId, agent, ws, resultHead) {
696
+ const keep = { ws, recut: false };
697
+ if (!resultHead)
698
+ return keep;
699
+ if (tryGit(toplevel(project.dir), ["merge-base", "--is-ancestor", resultHead, "HEAD"]) === undefined)
700
+ return keep;
701
+ const work = inspectWorkspace(project, runId, agent, ws);
702
+ if (!work || work.unknown || work.dirty.length)
703
+ return keep;
704
+ try {
705
+ if (agentGit(ws, ["rev-parse", "HEAD"]) !== resultHead)
706
+ return keep;
707
+ }
708
+ catch {
709
+ return keep;
710
+ }
711
+ removeAgentWorkspace(project, runId, agent);
712
+ return { ws: prepareAgentWorkspace(project, runId, agent, snapshotHead(project, runId)), recut: true };
713
+ }
689
714
  /** Names of the agents that have a worktree for this run. */
690
715
  export function listWorkspaceAgents(project, runId) {
691
716
  const dir = path.join(project.paths.root, "worktrees", runId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-lyceum",
3
- "version": "0.13.1",
3
+ "version": "0.15.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",