agent-lyceum 0.21.1 → 0.21.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/dist/cli.js CHANGED
@@ -23,6 +23,7 @@ import { callAgent, callProject, callsDir, resolveCallAgent } from "./call.js";
23
23
  import { formatEnforcement, validateProject } from "./validate.js";
24
24
  import { must } from "./assert.js";
25
25
  import { runMonitor } from "./monitor-lifecycle.js";
26
+ import { createStatusWatcher } from "./status-watch.js";
26
27
  import { LAUNCH_PPID, startedByWrapper, watchParent } from "./parent-watch.js";
27
28
  import { onlyAgent, soloProject } from "./solo.js";
28
29
  import { DEMO_PROJECT, DEMO_TASK, defaultDemoDir, detectRuntime, prepareDemo } from "./demo.js";
@@ -758,8 +759,40 @@ program
758
759
  .option("--monitor", "keep the page open and refresh it (Ctrl-C to quit)")
759
760
  .option("--interval <sec>", "refresh interval for --monitor", "2")
760
761
  .option("--json", "print the report as JSON (no colour); works with --task-list and --task-id")
762
+ .option("--watch", "with --json: keep running and print one JSON line (the project report plus one per running task) whenever it changes (Ctrl-C to quit)")
763
+ .option("--heartbeat <sec>", "with --watch: how often to re-check when no file event came", "5")
761
764
  .action(async (opts) => {
762
765
  try {
766
+ if (opts.watch) {
767
+ if (!opts.json)
768
+ fail("--watch needs --json.");
769
+ if (opts.monitor || opts.taskList || opts.taskId !== undefined)
770
+ fail("--watch cannot be combined with --monitor, --task-list or --task-id.");
771
+ const beat = Number(opts.heartbeat);
772
+ if (!(beat > 0))
773
+ fail("--heartbeat must be a positive number of seconds.");
774
+ loadProject(opts.project); // fail now on an unknown project, not as a frame
775
+ const watcher = createStatusWatcher({
776
+ load: () => loadProject(opts.project),
777
+ dir: (p) => p.paths.runs,
778
+ write: (line) => process.stdout.write(`${line}\n`),
779
+ debounceMs: 100,
780
+ });
781
+ runMonitor({
782
+ draw: watcher.beat,
783
+ intervalMs: beat * 1000,
784
+ stdout: process.stdout,
785
+ stdin: process.stdin,
786
+ ppid: () => process.ppid,
787
+ exit: (code) => {
788
+ watcher.close();
789
+ process.exit(code);
790
+ },
791
+ onSignal: (sig, handler) => process.on(sig, handler),
792
+ fail: (message) => fail(message),
793
+ });
794
+ await new Promise(() => { });
795
+ }
763
796
  const pr = loadProject(typeof opts.taskList === "string" ? opts.taskList : opts.project);
764
797
  if (opts.json) {
765
798
  if (opts.monitor)
@@ -0,0 +1,35 @@
1
+ import type { ResolvedProject } from "./config.js";
2
+ import { type StatusReport } from "./status.js";
3
+ /** What `status --json --watch` prints, one JSON object per line: the project's report plus one per running task. */
4
+ export type WatchFrame = {
5
+ schema_version: 1;
6
+ report?: StatusReport;
7
+ runs?: Record<string, StatusReport>;
8
+ error?: string;
9
+ };
10
+ /**
11
+ * The frame for now. Every running task gets its own report; a task that was running in the previous frame
12
+ * and has ended since is included once more, so the reader sees it finish (and can toast) before it is dropped.
13
+ * `tracked` is that previous set; it is replaced by the ids running now.
14
+ */
15
+ export declare function buildWatchFrame(project: ResolvedProject, tracked: Set<string>): WatchFrame;
16
+ export interface WatchDeps {
17
+ /** The project, loaded again each time so config edits and new runs show up. */
18
+ load: () => ResolvedProject;
19
+ /** The directory whose changes mean the status may have changed (the project's runs directory). */
20
+ dir: (project: ResolvedProject) => string;
21
+ write: (line: string) => void;
22
+ debounceMs: number;
23
+ watch?: (dir: string, onChange: () => void) => {
24
+ close: () => void;
25
+ };
26
+ }
27
+ /**
28
+ * Print a frame now and again whenever something under the runs directory changes (debounced; a round writes several files).
29
+ * A frame equal to the last one is not printed. `beat()` is the slow fallback: it re-arms a watcher that could not start
30
+ * (the directory did not exist yet), and picks up what a missed file event or a dead lock holder left unnoticed.
31
+ */
32
+ export declare function createStatusWatcher(d: WatchDeps): {
33
+ beat: () => void;
34
+ close: () => void;
35
+ };
@@ -0,0 +1,82 @@
1
+ import fs from "node:fs";
2
+ import { buildStatusReport, buildTaskListReport } from "./status.js";
3
+ /**
4
+ * The frame for now. Every running task gets its own report; a task that was running in the previous frame
5
+ * and has ended since is included once more, so the reader sees it finish (and can toast) before it is dropped.
6
+ * `tracked` is that previous set; it is replaced by the ids running now.
7
+ */
8
+ export function buildWatchFrame(project, tracked) {
9
+ const running = buildTaskListReport(project)
10
+ .runs.filter((r) => r.state === "running")
11
+ .map((r) => r.run_id);
12
+ const runs = {};
13
+ for (const id of new Set([...running, ...tracked]))
14
+ runs[id] = buildStatusReport(project, id);
15
+ const report = buildStatusReport(project);
16
+ tracked.clear();
17
+ for (const id of running)
18
+ tracked.add(id);
19
+ return { schema_version: 1, report, runs };
20
+ }
21
+ function fsWatch(dir, onChange) {
22
+ return fs.watch(dir, { recursive: true }, onChange);
23
+ }
24
+ /**
25
+ * Print a frame now and again whenever something under the runs directory changes (debounced; a round writes several files).
26
+ * A frame equal to the last one is not printed. `beat()` is the slow fallback: it re-arms a watcher that could not start
27
+ * (the directory did not exist yet), and picks up what a missed file event or a dead lock holder left unnoticed.
28
+ */
29
+ export function createStatusWatcher(d) {
30
+ const tracked = new Set();
31
+ const watch = d.watch ?? fsWatch;
32
+ let lastLine = "";
33
+ let watching = "";
34
+ let watcher;
35
+ let timer;
36
+ const emit = () => {
37
+ let frame;
38
+ let project;
39
+ try {
40
+ project = d.load();
41
+ frame = buildWatchFrame(project, tracked);
42
+ }
43
+ catch (e) {
44
+ frame = { schema_version: 1, error: e.message };
45
+ }
46
+ const line = JSON.stringify(frame);
47
+ if (line !== lastLine) {
48
+ lastLine = line;
49
+ d.write(line);
50
+ }
51
+ if (project)
52
+ arm(d.dir(project));
53
+ };
54
+ const arm = (dir) => {
55
+ if (watcher && watching === dir)
56
+ return;
57
+ watcher?.close();
58
+ watcher = undefined;
59
+ if (!fs.existsSync(dir))
60
+ return;
61
+ try {
62
+ watcher = watch(dir, schedule);
63
+ watching = dir;
64
+ }
65
+ catch {
66
+ /* recursive watch unsupported or the directory vanished: the beat covers it */
67
+ }
68
+ };
69
+ const schedule = () => {
70
+ if (timer)
71
+ clearTimeout(timer);
72
+ timer = setTimeout(emit, d.debounceMs);
73
+ };
74
+ return {
75
+ beat: emit,
76
+ close: () => {
77
+ if (timer)
78
+ clearTimeout(timer);
79
+ watcher?.close();
80
+ },
81
+ };
82
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-lyceum",
3
- "version": "0.21.1",
3
+ "version": "0.21.2",
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",