agent-lyceum 0.17.1 → 0.17.3
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 +14 -1
- package/README.zh-TW.md +14 -1
- package/dist/cli.js +55 -1
- package/dist/demo.d.ts +13 -0
- package/dist/demo.js +91 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,13 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
**English** | [繁體中文](README.zh-TW.md)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Give a task to a **team of agents** (Claude Code and/or Codex) instead of one overloaded session. A lead splits the work, specialists such as a frontend dev, a reviewer or a PM do their part in your repo, and the run ends when the lead says it is done.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
- **Roles that stay in role.** Each agent has its own persona (`AGENT.md`) and long-term memory, so a reviewer keeps reviewing instead of drifting into writing code.
|
|
12
|
+
- **Agents can't rewrite the rules.** An agent can only write its own memory and outbox. It can't touch other agents' context or its own persona, and this is enforced by the OS sandbox where possible, not just by a prompt. See [Write scope](docs/write-scope.md).
|
|
13
|
+
- **Mix runtimes.** Claude Code and Codex agents work in the same team, and each can be limited to the paths it owns.
|
|
14
|
+
- **Nothing hidden.** Agents talk through plain-file mailboxes, so every message, decision and memory is a file you can read, grep and diff. The dispatcher never merges for you.
|
|
15
|
+
|
|
16
|
+
**Not a fit** for small, one-shot edits: a single Claude Code or Codex session is faster and cheaper. Use a team when the task is big enough to split, review or run in parallel.
|
|
8
17
|
|
|
9
18
|
## Quick start
|
|
10
19
|
|
|
11
20
|
Requires Node 20+, plus `claude` and/or `codex` on your `PATH` (logged in). macOS and Linux (Linux needs `bwrap` for OS sandboxing); Windows only gets warnings.
|
|
12
21
|
|
|
13
22
|
```bash
|
|
23
|
+
npx agent-lyceum demo # 3 agents fix a failing test in a throwaway sandbox (real calls)
|
|
24
|
+
|
|
25
|
+
# on your own repo:
|
|
14
26
|
npx agent-lyceum init # create the home + global agent library
|
|
15
27
|
npx agent-lyceum project add web --dir ~/code/web-app
|
|
16
28
|
npx agent-lyceum doctor --project web # check config, show enforcement levels
|
|
@@ -41,6 +53,7 @@ Its own data (config, mailboxes, logs, memory, worktrees) lives in `~/agent-lyce
|
|
|
41
53
|
| `resume` | Continue an interrupted run, or reopen a finished one with a follow-up. [details](docs/commands.md#resume) |
|
|
42
54
|
| `clear` | Delete a run, or remove a stale lock. [details](docs/commands.md#clear) |
|
|
43
55
|
| `call <agent>` | Ask one global agent on its own, outside any project. [details](docs/commands.md#call) |
|
|
56
|
+
| `demo` | Try a 3-agent team on a throwaway sample repo. [details](docs/commands.md#demo) |
|
|
44
57
|
| `tidy` | Have agents tidy their own memory (archives, never deletes). [details](docs/commands.md#tidy) |
|
|
45
58
|
|
|
46
59
|
`agent-lyceum <command> --help` prints the full options. Ctrl-C cancels cleanly; [exit codes](docs/commands.md#exit-codes) tell scripts how a run ended.
|
package/README.zh-TW.md
CHANGED
|
@@ -4,13 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
[English](README.md) | **繁體中文**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
把任務交給一個 **agent 團隊**(Claude Code 和/或 Codex),而不是塞給單一個負荷過重的 session。lead 拆分工作,前端、reviewer、PM 等專長角色在你的 repo 裡各做各的,lead 回報 done 即結束。
|
|
8
|
+
|
|
9
|
+
## 為什麼用它
|
|
10
|
+
|
|
11
|
+
- **角色不會跑掉。** 每個 agent 有自己的人設(`AGENT.md`)與長期記憶,reviewer 會一直做 review,不會漸漸變成在寫程式。
|
|
12
|
+
- **agent 改不了規則。** agent 只能寫自己的記憶與寄件匣(outbox),不能改其他 agent 的 context,也不能改自己的人設;能做到的地方由 OS 沙箱強制,而不只靠 prompt。見 [Write scope](docs/write-scope.zh-TW.md)。
|
|
13
|
+
- **混用 runtime。** Claude Code 與 Codex agent 可在同一個團隊,並各自限制可寫的路徑。
|
|
14
|
+
- **沒有黑箱。** agent 透過純檔案信箱溝通,每則訊息、決策與記憶都是你能讀、能 grep、能 diff 的檔案;dispatcher 不會替你 merge。
|
|
15
|
+
|
|
16
|
+
**不適合**小而單次的修改:單一 Claude Code 或 Codex session 更快更省。任務大到值得拆分、review 或平行執行時,再用團隊。
|
|
8
17
|
|
|
9
18
|
## 快速開始
|
|
10
19
|
|
|
11
20
|
需求:Node 20+,以及 `PATH` 中有 `claude` 和/或 `codex`(已登入)。支援 macOS 與 Linux(Linux 需要 `bwrap` 才有 OS 沙箱);Windows 只會顯示警告。
|
|
12
21
|
|
|
13
22
|
```bash
|
|
23
|
+
npx agent-lyceum demo # 3 個 agent 在拋棄式沙箱裡修好一個失敗的測試(真實呼叫)
|
|
24
|
+
|
|
25
|
+
# 用在你自己的 repo:
|
|
14
26
|
npx agent-lyceum init # 建立 home 與全域 agent 庫
|
|
15
27
|
npx agent-lyceum project add web --dir ~/code/web-app
|
|
16
28
|
npx agent-lyceum doctor --project web # 檢查設定並顯示各項防護等級
|
|
@@ -41,6 +53,7 @@ npx agent-lyceum status
|
|
|
41
53
|
| `resume` | 繼續中斷的 run,或用追加需求重開已完成的 run。 [詳細](docs/commands.zh-TW.md#resume) |
|
|
42
54
|
| `clear` | 刪除 run,或移除殘留的鎖。 [詳細](docs/commands.zh-TW.md#clear) |
|
|
43
55
|
| `call <agent>` | 在任何專案之外單獨呼叫一個全域 agent。 [詳細](docs/commands.zh-TW.md#call) |
|
|
56
|
+
| `demo` | 在拋棄式範例 repo 上試跑 3 個 agent 的團隊。 [詳細](docs/commands.zh-TW.md#demo) |
|
|
44
57
|
| `tidy` | 讓 agent 整理自己的記憶(只封存,不刪除)。 [詳細](docs/commands.zh-TW.md#tidy) |
|
|
45
58
|
|
|
46
59
|
`agent-lyceum <指令> --help` 會印出完整選項。Ctrl-C 可乾淨取消;腳本可依[結束碼](docs/commands.zh-TW.md#退出碼)判斷 run 的結果。
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import readline from "node:readline/promises";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
5
6
|
import { Command } from "commander";
|
|
6
7
|
import { ConfigError, findProjectForCwd, flattenResolved, listProjects, loadGlobal, resolveProject, resolveProjectWithSources } from "./config.js";
|
|
7
8
|
import { openInEditor, prepareAnswers } from "./ask-answer.js";
|
|
@@ -12,7 +13,7 @@ import { diagnoseProject, formatDoctor, preflightRuntimes } from "./doctor.js";
|
|
|
12
13
|
import { acquireProjectLock, forceUnlock, inspectRunLock, listRunLocks, listUnreadableRunLocks, LOCK_FILE, lockHolderAlive } from "./project-lock.js";
|
|
13
14
|
import { loadRunState, newRunId, outcomeOf, resolveRun, roundLimit, roundsLabel, runLabel, slugify, uniqueName } from "./run-store.js";
|
|
14
15
|
import { failureGuide } from "./run-outcome.js";
|
|
15
|
-
import { exitCodeForOutcome } from "./schema.js";
|
|
16
|
+
import { exitCodeForOutcome, RUNTIMES } from "./schema.js";
|
|
16
17
|
import { absPath, assertName, NAME_RE, projectPaths, resolveHome } from "./paths.js";
|
|
17
18
|
import { addProject, initHome, removeProject } from "./scaffold.js";
|
|
18
19
|
import { buildStatusReport, buildTaskListReport, formatMonitor, formatRunDetail, formatStatusWithLog, formatTaskList, latestUnfinishedRun, listRuns, reopenCandidates, runIsAlive } from "./status.js";
|
|
@@ -21,6 +22,7 @@ import { callAgent, callProject, callsDir, resolveCallAgent } from "./call.js";
|
|
|
21
22
|
import { formatEnforcement, validateProject } from "./validate.js";
|
|
22
23
|
import { must } from "./assert.js";
|
|
23
24
|
import { onlyAgent, soloProject } from "./solo.js";
|
|
25
|
+
import { DEMO_PROJECT, DEMO_TASK, defaultDemoDir, detectRuntime, prepareDemo } from "./demo.js";
|
|
24
26
|
const program = new Command();
|
|
25
27
|
program
|
|
26
28
|
.name("agent-lyceum")
|
|
@@ -772,11 +774,63 @@ program
|
|
|
772
774
|
fail(e instanceof ConfigError ? e.message : e.message);
|
|
773
775
|
}
|
|
774
776
|
});
|
|
777
|
+
program
|
|
778
|
+
.command("demo")
|
|
779
|
+
.summary("Try a 3-agent team on a tiny sample repo (real agents, a few minutes)")
|
|
780
|
+
.description("Build a throwaway sandbox (a tiny git repo with one failing test, plus its own agent-lyceum home), then run a lead, a frontend member and a QA member on it until `npm test` passes.\n\nIt uses real `claude` / `codex` calls, so it spends a little of your quota. Nothing outside the sandbox directory is touched; your own home and repos are not used.")
|
|
781
|
+
.option("--dir <path>", "the sandbox directory, must be empty or new (default: a new folder in the system temp dir)")
|
|
782
|
+
.option("--runtime <runtime>", `${RUNTIMES.join(" | ")} (default: whichever CLI is on your PATH)`)
|
|
783
|
+
.option("--setup-only", "build the sandbox and print the commands, but run no agents")
|
|
784
|
+
.option("-y, --yes", "do not ask before running real agents")
|
|
785
|
+
.action(async (opts) => {
|
|
786
|
+
try {
|
|
787
|
+
let runtime = opts.runtime;
|
|
788
|
+
if (runtime && !RUNTIMES.includes(runtime))
|
|
789
|
+
fail(`Unknown runtime "${runtime}". Use one of: ${RUNTIMES.join(", ")}.`);
|
|
790
|
+
runtime ??= detectRuntime();
|
|
791
|
+
if (!runtime) {
|
|
792
|
+
if (!opts.setupOnly)
|
|
793
|
+
fail("Neither `claude` nor `codex` was found on your PATH (and logged in). Install one, or use --setup-only to just look at the sandbox.");
|
|
794
|
+
runtime = "claude-code";
|
|
795
|
+
}
|
|
796
|
+
if (!opts.setupOnly && !opts.yes && interactive()) {
|
|
797
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
798
|
+
const ans = (await rl.question(`This runs a 3-agent team with real ${runtime} calls on a sample repo (a few minutes, a small amount of quota). Continue? [Y/n] `)).trim().toLowerCase();
|
|
799
|
+
rl.close();
|
|
800
|
+
if (ans === "n" || ans === "no")
|
|
801
|
+
fail("Aborted.");
|
|
802
|
+
}
|
|
803
|
+
const demo = prepareDemo(opts.dir ?? defaultDemoDir(), runtime);
|
|
804
|
+
const h = demo.home;
|
|
805
|
+
console.log(`Sandbox: ${demo.dir} (runtime: ${runtime})\nRepo with one failing test: ${demo.repo}\n`);
|
|
806
|
+
const next = [
|
|
807
|
+
`agent-lyceum --home ${h} status -p ${DEMO_PROJECT} # agents, mail, runs`,
|
|
808
|
+
`git -C ${demo.repo} log --oneline --stat # what the team changed`,
|
|
809
|
+
`rm -rf ${demo.dir} # clean up when done`,
|
|
810
|
+
];
|
|
811
|
+
if (opts.setupOnly) {
|
|
812
|
+
fs.writeFileSync(path.join(demo.dir, "task.md"), `${DEMO_TASK}\n`);
|
|
813
|
+
console.log(`Run the team yourself:\n agent-lyceum --home ${h} run -p ${DEMO_PROJECT} --task-file ${path.join(demo.dir, "task.md")}\n\nThen:\n${next.map((l) => ` ${l}`).join("\n")}`);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
console.log(`Task for the lead:\n${DEMO_TASK.split("\n").map((l) => ` | ${l}`).join("\n")}\n`);
|
|
817
|
+
// The real `run`, in a child: the demo shows exactly what a normal run does (progress, Ctrl-C, exit codes).
|
|
818
|
+
const r = spawnSync(process.execPath, [...process.execArgv, process.argv[1], "--home", h, "run", DEMO_TASK, "-p", DEMO_PROJECT, "--name", "demo-slugify"], { stdio: "inherit", cwd: demo.repo });
|
|
819
|
+
const code = r.status ?? 1;
|
|
820
|
+
console.log(code === 0 ? "\nDone. See what happened:" : `\nThe run ended with exit code ${code} (see docs/commands.md#exit-codes). Look around:`);
|
|
821
|
+
console.log(next.map((l) => ` ${l}`).join("\n"));
|
|
822
|
+
process.exit(code);
|
|
823
|
+
}
|
|
824
|
+
catch (e) {
|
|
825
|
+
fail(e.message);
|
|
826
|
+
}
|
|
827
|
+
});
|
|
775
828
|
// `--help` lists the commands in three layers.
|
|
776
829
|
const HELP_LAYERS = {
|
|
777
830
|
"Project:": ["init", "project"],
|
|
778
831
|
"Diagnose:": ["doctor", "status"],
|
|
779
832
|
"Run:": ["run", "resume", "clear", "call", "tidy"],
|
|
833
|
+
"Try it:": ["demo"],
|
|
780
834
|
};
|
|
781
835
|
for (const [heading, names] of Object.entries(HELP_LAYERS))
|
|
782
836
|
for (const c of program.commands)
|
package/dist/demo.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Runtime } from "./schema.js";
|
|
2
|
+
export declare const DEMO_PROJECT = "demo";
|
|
3
|
+
export declare const DEMO_TASK = "The sample repo has a failing test: `npm test` fails because slugify() in src/slug.js is not implemented.\n\nImplement slugify(text) so that it lowercases the text, turns every run of non-alphanumeric characters into a single \"-\", and trims leading and trailing \"-\". Do not edit the tests.\n\nPlan: the lead hands the implementation to fe-member, then asks qa-member to run `npm test` and review the change. Finish with `done` only after qa-member's verdict is pass.";
|
|
4
|
+
/** Which runtime CLI is on the PATH and answers `--version`; claude-code wins when both are. */
|
|
5
|
+
export declare function detectRuntime(): Runtime | undefined;
|
|
6
|
+
export interface DemoSetup {
|
|
7
|
+
dir: string;
|
|
8
|
+
home: string;
|
|
9
|
+
repo: string;
|
|
10
|
+
}
|
|
11
|
+
/** A throwaway sandbox: a tiny git repo with one failing test, plus its own agent-lyceum home with a registered project. Touches nothing outside `dir`. */
|
|
12
|
+
export declare function prepareDemo(dir: string, runtime: Runtime): DemoSetup;
|
|
13
|
+
export declare function defaultDemoDir(): string;
|
package/dist/demo.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { projectPaths } from "./paths.js";
|
|
6
|
+
import { addProject, initHome } from "./scaffold.js";
|
|
7
|
+
export const DEMO_PROJECT = "demo";
|
|
8
|
+
export const DEMO_TASK = `The sample repo has a failing test: \`npm test\` fails because slugify() in src/slug.js is not implemented.
|
|
9
|
+
|
|
10
|
+
Implement slugify(text) so that it lowercases the text, turns every run of non-alphanumeric characters into a single "-", and trims leading and trailing "-". Do not edit the tests.
|
|
11
|
+
|
|
12
|
+
Plan: the lead hands the implementation to fe-member, then asks qa-member to run \`npm test\` and review the change. Finish with \`done\` only after qa-member's verdict is pass.`;
|
|
13
|
+
const FILES = {
|
|
14
|
+
"package.json": `{
|
|
15
|
+
"name": "lyceum-demo",
|
|
16
|
+
"private": true,
|
|
17
|
+
"type": "module",
|
|
18
|
+
"scripts": { "test": "node --test" }
|
|
19
|
+
}
|
|
20
|
+
`,
|
|
21
|
+
"src/slug.js": `// Turn a title into a URL slug: "Hello, World!" -> "hello-world".
|
|
22
|
+
export function slugify(text) {
|
|
23
|
+
throw new Error("not implemented");
|
|
24
|
+
}
|
|
25
|
+
`,
|
|
26
|
+
"test/slug.test.js": `import assert from "node:assert/strict";
|
|
27
|
+
import { test } from "node:test";
|
|
28
|
+
import { slugify } from "../src/slug.js";
|
|
29
|
+
|
|
30
|
+
test("lowercases and joins words with a dash", () => {
|
|
31
|
+
assert.equal(slugify("Hello World"), "hello-world");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("collapses punctuation and repeated separators", () => {
|
|
35
|
+
assert.equal(slugify("Hello, World!!"), "hello-world");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("trims leading and trailing separators", () => {
|
|
39
|
+
assert.equal(slugify(" --Agent Lyceum-- "), "agent-lyceum");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("keeps digits", () => {
|
|
43
|
+
assert.equal(slugify("Top 10 tips"), "top-10-tips");
|
|
44
|
+
});
|
|
45
|
+
`,
|
|
46
|
+
};
|
|
47
|
+
/** Which runtime CLI is on the PATH and answers `--version`; claude-code wins when both are. */
|
|
48
|
+
export function detectRuntime() {
|
|
49
|
+
for (const [runtime, bin] of [["claude-code", "claude"], ["codex", "codex"]]) {
|
|
50
|
+
const r = spawnSync(bin, ["--version"], { stdio: "ignore" });
|
|
51
|
+
if (r.status === 0)
|
|
52
|
+
return runtime;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
/** A throwaway sandbox: a tiny git repo with one failing test, plus its own agent-lyceum home with a registered project. Touches nothing outside `dir`. */
|
|
57
|
+
export function prepareDemo(dir, runtime) {
|
|
58
|
+
const root = path.resolve(dir);
|
|
59
|
+
if (fs.existsSync(root) && fs.readdirSync(root).length)
|
|
60
|
+
throw new Error(`${root} is not empty. Choose another --dir (or remove it).`);
|
|
61
|
+
const home = path.join(root, "home");
|
|
62
|
+
const repo = path.join(root, "repo");
|
|
63
|
+
fs.mkdirSync(repo, { recursive: true });
|
|
64
|
+
for (const [file, text] of Object.entries(FILES)) {
|
|
65
|
+
fs.mkdirSync(path.dirname(path.join(repo, file)), { recursive: true });
|
|
66
|
+
fs.writeFileSync(path.join(repo, file), text);
|
|
67
|
+
}
|
|
68
|
+
const git = (...args) => execFileSync("git", ["-C", repo, "-c", "user.name=agent-lyceum demo", "-c", "user.email=demo@example.com", ...args], { stdio: "ignore" });
|
|
69
|
+
try {
|
|
70
|
+
git("init", "-q", "-b", "main");
|
|
71
|
+
git("add", "-A");
|
|
72
|
+
git("commit", "-q", "-m", "demo: slugify is not implemented yet");
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new Error("The demo needs `git` on your PATH.");
|
|
76
|
+
}
|
|
77
|
+
initHome(home);
|
|
78
|
+
addProject(home, DEMO_PROJECT, repo);
|
|
79
|
+
const cfg = projectPaths(home, DEMO_PROJECT).config;
|
|
80
|
+
let yaml = fs.readFileSync(cfg, "utf8");
|
|
81
|
+
yaml = yaml.replace("max_rounds: 0 ", "max_rounds: 20");
|
|
82
|
+
// One runtime for the whole team, and the roles scoped to the sample repo.
|
|
83
|
+
for (const agent of ["lead", "fe-member", "qa-member"])
|
|
84
|
+
yaml = yaml.replace(` ${agent}:\n`, ` ${agent}:\n runtime: ${runtime}\n`);
|
|
85
|
+
yaml = yaml.replace('# owns: ["src/web/**"]', 'owns: ["src/**"]').replace('# owns: ["tests/**"]', 'owns: ["test/**"]');
|
|
86
|
+
fs.writeFileSync(cfg, yaml);
|
|
87
|
+
return { dir: root, home, repo };
|
|
88
|
+
}
|
|
89
|
+
export function defaultDemoDir() {
|
|
90
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "agent-lyceum-demo-"));
|
|
91
|
+
}
|