agent-lyceum 0.17.2 → 0.18.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 +16 -3
- package/README.zh-TW.md +16 -3
- package/dist/adapters/claude.js +1 -7
- package/dist/call.js +1 -2
- package/dist/cli.js +72 -10
- package/dist/config.d.ts +10 -6
- package/dist/config.js +24 -7
- package/dist/demo.d.ts +13 -0
- package/dist/demo.js +90 -0
- package/dist/dispatcher.d.ts +2 -1
- package/dist/dispatcher.js +3 -1
- package/dist/doctor.js +11 -12
- package/dist/paths.d.ts +0 -2
- package/dist/paths.js +0 -11
- package/dist/policy.d.ts +0 -6
- package/dist/policy.js +2 -10
- package/dist/project-lock.d.ts +10 -2
- package/dist/project-lock.js +23 -12
- package/dist/prompt.js +0 -3
- package/dist/run-session.d.ts +3 -1
- package/dist/run-session.js +20 -30
- package/dist/run-store.d.ts +1 -1
- package/dist/scaffold.js +0 -5
- package/dist/schema.d.ts +12 -22
- package/dist/schema.js +8 -7
- package/dist/solo.js +1 -2
- package/dist/validate.js +5 -38
- package/dist/worktree.d.ts +19 -12
- package/dist/worktree.js +35 -74
- package/package.json +1 -1
package/dist/worktree.js
CHANGED
|
@@ -4,16 +4,6 @@ import fs from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { atomicWrite } from "./fs-util.js";
|
|
7
|
-
/** Agents share the repo, or each non-lead agent gets its own git worktree. `auto` picks worktrees as soon as agents can run in parallel; several concurrent runs always do. */
|
|
8
|
-
export function resolveWorkspaceMode(d) {
|
|
9
|
-
if ((d.max_concurrent_runs ?? 1) > 1)
|
|
10
|
-
return "worktree";
|
|
11
|
-
if (d.workspace_mode === "worktree")
|
|
12
|
-
return "worktree";
|
|
13
|
-
if (d.workspace_mode === "shared")
|
|
14
|
-
return "shared";
|
|
15
|
-
return d.max_parallel > 1 ? "worktree" : "shared";
|
|
16
|
-
}
|
|
17
7
|
const IDENTITY = {
|
|
18
8
|
GIT_AUTHOR_NAME: "agent-lyceum",
|
|
19
9
|
GIT_AUTHOR_EMAIL: "agent-lyceum@localhost",
|
|
@@ -227,33 +217,42 @@ export const toplevel = (dir) => git(dir, ["rev-parse", "--show-toplevel"]);
|
|
|
227
217
|
export function dirtyPaths(dir) {
|
|
228
218
|
return parseStatus(execFileSync("git", ["status", "--porcelain", "-z", "--untracked-files=all"], { cwd: dir, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 }));
|
|
229
219
|
}
|
|
230
|
-
/**
|
|
231
|
-
export function
|
|
232
|
-
if (!isGitRepo(
|
|
233
|
-
|
|
234
|
-
|
|
220
|
+
/** Why `dir` cannot give every run and agent its own git worktree, or undefined when it can. */
|
|
221
|
+
export function isolationBlocker(dir) {
|
|
222
|
+
if (!isGitRepo(dir))
|
|
223
|
+
return "not a git repository";
|
|
224
|
+
if (tryGit(dir, ["rev-parse", "--verify", "-q", "HEAD"]) === undefined)
|
|
225
|
+
return "no commit yet";
|
|
235
226
|
// With git's reftable ref storage a linked worktree's HEAD lives in `<gitDir>/reftable/`, a place the agent can write, and the
|
|
236
227
|
// file `<gitDir>/HEAD` only says `ref: refs/heads/.invalid`: the check that a worktree's HEAD is its own branch cannot hold
|
|
237
|
-
// there, so every worktree would be refused in the middle of the run.
|
|
238
|
-
//
|
|
239
|
-
if (tryGit(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
228
|
+
// there, so every worktree would be refused in the middle of the run. (`rev-parse --show-ref-format` does not exist in older
|
|
229
|
+
// git, which only knows `files`.)
|
|
230
|
+
if (tryGit(dir, ["rev-parse", "--show-ref-format"]) === "reftable")
|
|
231
|
+
return "reftable ref storage";
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Where a run's agents work. A new run is isolated when the project dir can be (see `isolationBlocker`), otherwise it shares the
|
|
236
|
+
* checkout. A resumed run keeps what its state recorded, whatever the repo looks like now. Decided once, before the run's lock is taken.
|
|
237
|
+
*/
|
|
238
|
+
export function planWorkspace(project, resume) {
|
|
239
|
+
if (resume) {
|
|
240
|
+
if (resume.lead_workspace === true)
|
|
241
|
+
return { mode: "worktree", leadWorktree: true };
|
|
242
|
+
if (resume.workspace_mode === "worktree")
|
|
243
|
+
return { mode: "worktree", leadWorktree: false };
|
|
244
|
+
return { mode: "shared", leadWorktree: false };
|
|
245
|
+
}
|
|
246
|
+
const reason = isolationBlocker(project.dir);
|
|
247
|
+
return reason === undefined ? { mode: "worktree", leadWorktree: true } : { mode: "shared", leadWorktree: false, reason };
|
|
248
|
+
}
|
|
249
|
+
/** Refuse to resume a worktree run when its repo can no longer be isolated; returns nothing when it is fine. */
|
|
250
|
+
export function assertWorktreeRunnable(project) {
|
|
251
|
+
if (!isGitRepo(project.dir)) {
|
|
252
|
+
throw new Error(`This run works in git worktrees, but ${project.dir} is no longer a git repository.`);
|
|
251
253
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const shown = dirty.slice(0, 5).join(", ") + (dirty.length > 5 ? `, ... (${dirty.length} in all)` : "");
|
|
255
|
-
throw new Error(`Parallel runs start from a clean repo, but ${project.dir} has uncommitted changes: ${shown}. ` +
|
|
256
|
-
`Commit or stash them first (agent-lyceum never touches your own edits), or run sequentially with dispatcher.max_parallel: 1.`);
|
|
254
|
+
if (tryGit(project.dir, ["rev-parse", "--show-ref-format"]) === "reftable") {
|
|
255
|
+
throw new Error(`This run works in git worktrees, but ${project.dir} now uses git's reftable ref storage, whose worktrees agent-lyceum cannot yet verify as isolated.`);
|
|
257
256
|
}
|
|
258
257
|
}
|
|
259
258
|
/** `refs/agent-lyceum/<run>/base-0` pointing at the repo's HEAD: where a concurrent run starts. Nothing in the repo changes. */
|
|
@@ -427,39 +426,6 @@ export function removeAgentWorkspace(project, runId, agent) {
|
|
|
427
426
|
tryGit(top, ["branch", "-D", branch]);
|
|
428
427
|
fs.rmSync(`${root}.json`, { force: true });
|
|
429
428
|
}
|
|
430
|
-
/** Glob as used in `owns`: `**` crosses folders, `*` and `?` stay within one, `{a,b}` alternates. */
|
|
431
|
-
export function globMatch(glob, file) {
|
|
432
|
-
let re = "";
|
|
433
|
-
for (let i = 0; i < glob.length; i++) {
|
|
434
|
-
const c = glob.charAt(i);
|
|
435
|
-
if (c === "*" && glob[i + 1] === "*") {
|
|
436
|
-
if (glob[i + 2] === "/") {
|
|
437
|
-
re += "(?:.*/)?";
|
|
438
|
-
i += 2;
|
|
439
|
-
}
|
|
440
|
-
else {
|
|
441
|
-
re += ".*";
|
|
442
|
-
i += 1;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
else if (c === "*")
|
|
446
|
-
re += "[^/]*";
|
|
447
|
-
else if (c === "?")
|
|
448
|
-
re += "[^/]";
|
|
449
|
-
else if (c === "{") {
|
|
450
|
-
const end = glob.indexOf("}", i);
|
|
451
|
-
if (end < 0)
|
|
452
|
-
re += "\\{";
|
|
453
|
-
else {
|
|
454
|
-
re += `(?:${glob.slice(i + 1, end).split(",").map((s) => s.replace(/[.+^$()|[\]\\]/g, "\\$&")).join("|")})`;
|
|
455
|
-
i = end;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
else
|
|
459
|
-
re += c.replace(/[.+^$()|[\]\\}]/g, "\\$&");
|
|
460
|
-
}
|
|
461
|
-
return new RegExp(`^${re}$`).test(file);
|
|
462
|
-
}
|
|
463
429
|
const PROTECTED_NAMES = ["CLAUDE.md", "AGENTS.md"];
|
|
464
430
|
/** Commit whatever is in an agent's worktree (no hooks, no signing, dispatcher identity). Returns the HEAD afterwards and whether a commit was made. */
|
|
465
431
|
export function commitWorkspace(ws, message) {
|
|
@@ -475,10 +441,10 @@ export function commitWorkspace(ws, message) {
|
|
|
475
441
|
}
|
|
476
442
|
/**
|
|
477
443
|
* Commit whatever the agent left in its worktree and list what changed since its snapshot, checking every
|
|
478
|
-
* changed path (modified, added, deleted, or either end of a rename) against the
|
|
444
|
+
* changed path (modified, added, deleted, or either end of a rename) against the project
|
|
479
445
|
* folder, the protected instruction files, and symlink/submodule rules. Nothing is applied here.
|
|
480
446
|
*/
|
|
481
|
-
export function collectAgentChanges(workspace
|
|
447
|
+
export function collectAgentChanges(workspace) {
|
|
482
448
|
const { head } = commitWorkspace(workspace, `agent-lyceum: changes by ${workspace.agent}`);
|
|
483
449
|
const base = workspace.base;
|
|
484
450
|
const raw = base === head ? "" : agentGit(workspace, ["diff", "--raw", "-z", "--no-renames", "--no-abbrev", base, head]);
|
|
@@ -501,9 +467,6 @@ export function collectAgentChanges(workspace, owns) {
|
|
|
501
467
|
const p = rel === "" ? f.path : path.posix.relative(rel, f.path);
|
|
502
468
|
if (PROTECTED_NAMES.includes(p))
|
|
503
469
|
violations.push(`${f.path}: protected instruction file; only the lead may change it, by hand`);
|
|
504
|
-
const allowed = (candidate) => owns.length === 0 || owns.some((g) => globMatch(g, candidate));
|
|
505
|
-
if (!allowed(p))
|
|
506
|
-
violations.push(`${f.path}: outside this agent's owns (${owns.join(", ")})`);
|
|
507
470
|
if (f.mode === "160000")
|
|
508
471
|
violations.push(`${f.path}: submodule changes are not integrated`);
|
|
509
472
|
if (f.mode === "120000" && f.status !== "D") {
|
|
@@ -513,8 +476,6 @@ export function collectAgentChanges(workspace, owns) {
|
|
|
513
476
|
violations.push(`${f.path}: symlink to an absolute path (${target})`);
|
|
514
477
|
else if (resolved.startsWith(".."))
|
|
515
478
|
violations.push(`${f.path}: symlink leaves the project directory (${target})`);
|
|
516
|
-
else if (!allowed(resolved))
|
|
517
|
-
violations.push(`${f.path}: symlink points outside this agent's owns (${target})`);
|
|
518
479
|
}
|
|
519
480
|
}
|
|
520
481
|
return { workspace, base, head, files, violations };
|