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/dist/policy.d.ts CHANGED
@@ -8,7 +8,6 @@ export interface AgentEnforcement {
8
8
  otherContext: Level;
9
9
  shared: Level;
10
10
  repoInstructions: Level;
11
- owns: Level;
12
11
  /** Absolute paths outside the repo this agent may write. */
13
12
  writablePaths: string[];
14
13
  notes: string[];
@@ -22,9 +21,6 @@ export interface WritePolicy {
22
21
  allowFiles: string[];
23
22
  /** Files/dirs that must stay unwritable (checked even inside the repo). */
24
23
  deny: string[];
25
- /** Repo-relative globs the agent owns (only enforced when max_parallel > 1). */
26
- owns: string[];
27
- restrictRepoToOwns: boolean;
28
24
  }
29
25
  export declare function commonFile(project: ResolvedProject): string;
30
26
  export declare function outboxDir(project: ResolvedProject, agent: string): string;
@@ -42,7 +38,5 @@ export declare function sharedIndex(project: ResolvedProject): string | undefine
42
38
  export declare function repoDirFor(project: ResolvedProject, agent: string): string;
43
39
  export declare function writePolicy(project: ResolvedProject, agent: ResolvedAgent): WritePolicy;
44
40
  export declare function enforcementFor(project: ResolvedProject, agent: ResolvedAgent): AgentEnforcement;
45
- /** Static directory prefixes (absolute) of an agent's `owns` globs. */
46
- export declare function ownsDirs(project: ResolvedProject, agent: ResolvedAgent): string[];
47
41
  /** Can `agent` legitimately change this protected file? */
48
42
  export declare function mayEditProtected(project: ResolvedProject, agent: ResolvedAgent, file: string): boolean;
package/dist/policy.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { findOnPath } from "./fs-util.js";
4
- import { globStaticPrefix } from "./paths.js";
5
4
  export function commonFile(project) {
6
5
  return path.join(project.paths.shared, "common", "COMMON.md");
7
6
  }
@@ -123,8 +122,6 @@ export function writePolicy(project, agent) {
123
122
  readDirs: shared && !mayWriteShared ? [shared] : [],
124
123
  allowFiles,
125
124
  deny: [...new Set(deny)],
126
- owns: agent.owns,
127
- restrictRepoToOwns: project.dispatcher.max_parallel > 1 && agent.owns.length > 0 && !isLead,
128
125
  };
129
126
  }
130
127
  let sandboxProbe;
@@ -143,12 +140,11 @@ export function enforcementFor(project, agent) {
143
140
  const pol = writePolicy(project, agent);
144
141
  const os = osSandboxAvailable();
145
142
  const notes = [];
146
- const parallel = project.dispatcher.max_parallel > 1;
147
143
  const isLeadAgent = agent.name === project.lead;
148
144
  let e;
149
145
  if (agent.runtime === "claude-code") {
150
146
  const lvl = os ? "os" : "tool-rules";
151
- e = { memory: lvl, agentMd: lvl, otherContext: lvl, shared: isLeadAgent ? "n/a" : lvl, repoInstructions: lvl, owns: parallel ? "tool-rules" : "n/a" };
147
+ e = { memory: lvl, agentMd: lvl, otherContext: lvl, shared: isLeadAgent ? "n/a" : lvl, repoInstructions: lvl };
152
148
  if (!os)
153
149
  notes.push("OS sandbox unavailable (Windows, or Linux without bubblewrap): Bash can bypass Edit rules.");
154
150
  if (!isLeadAgent && !os)
@@ -156,7 +152,7 @@ export function enforcementFor(project, agent) {
156
152
  }
157
153
  else {
158
154
  const lvl = os ? "os" : "prompt-only";
159
- e = { memory: lvl, agentMd: lvl, otherContext: lvl, shared: isLeadAgent ? "n/a" : os ? "os" : "post-hoc", repoInstructions: "post-hoc", owns: parallel ? "prompt-only" : "n/a" };
155
+ e = { memory: lvl, agentMd: lvl, otherContext: lvl, shared: isLeadAgent ? "n/a" : os ? "os" : "post-hoc", repoInstructions: "post-hoc" };
160
156
  notes.push("Codex cannot forbid single files inside the repo; CLAUDE.md/AGENTS.md changes are detected and reverted after each wake-up.");
161
157
  if (!isLeadAgent && !os)
162
158
  notes.push("Without the OS sandbox only the handoff index (_shared/MEMORY.md) is checked after each wake-up; other files in _shared are not protected.");
@@ -171,10 +167,6 @@ export function enforcementFor(project, agent) {
171
167
  notes,
172
168
  };
173
169
  }
174
- /** Static directory prefixes (absolute) of an agent's `owns` globs. */
175
- export function ownsDirs(project, agent) {
176
- return agent.owns.map((g) => path.join(project.dir, globStaticPrefix(g)));
177
- }
178
170
  /** Can `agent` legitimately change this protected file? */
179
171
  export function mayEditProtected(project, agent, file) {
180
172
  return writePolicy(project, agent).allowFiles.includes(file);
@@ -4,10 +4,13 @@ export declare const LOCK_FILE = "lock.json";
4
4
  export declare const LOCKS_DIR = "locks";
5
5
  export interface LockOptions {
6
6
  heartbeatMs?: number;
7
- /** Most runs that may hold a lock of this project at once, this one included. Default 1. */
8
- maxConcurrent?: number;
7
+ /** The run works directly in the project checkout (not in its own worktree): only one such run may be active. It is written with `exclusive` too, so older binaries keep out. */
8
+ checkout?: boolean;
9
9
  /** The holder needs the whole project (e.g. a memory tidy): refused while any other lock is alive, and it keeps every other lock out while held. */
10
10
  exclusive?: boolean;
11
+ /** For a checkout run's refusal message: the checkout directory, and why this run has no worktree. */
12
+ dir?: string;
13
+ reason?: string;
11
14
  }
12
15
  export interface LockInfo {
13
16
  token: string;
@@ -20,7 +23,12 @@ export interface LockInfo {
20
23
  heartbeat_at: string;
21
24
  /** Held by an operation that needs the project to itself (a memory tidy). */
22
25
  exclusive?: boolean;
26
+ /** `true`: works in the project checkout; `false`: in its own worktree (or holds no files, like `clear`). Absent: an older version's run, treated as a checkout run. */
27
+ checkout?: boolean;
23
28
  }
29
+ export type LockKind = "checkout" | "whole" | "worktree";
30
+ /** What a lock protects: the checkout, the whole project (tidy/restore), or nothing shared (a worktree run). Locks of older versions count as checkout runs. */
31
+ export declare function lockKind(l: Pick<LockInfo, "exclusive" | "checkout">): LockKind;
24
32
  export interface ProjectLease {
25
33
  readonly info: LockInfo;
26
34
  /** True once another process replaced our lock file; the run should stop writing. */
@@ -10,6 +10,16 @@ export const LOCK_FILE = "lock.json";
10
10
  export const LOCKS_DIR = "locks";
11
11
  const ADMIT_FILE = ".admit";
12
12
  const HEARTBEAT_MS = 5000;
13
+ /** What a lock protects: the checkout, the whole project (tidy/restore), or nothing shared (a worktree run). Locks of older versions count as checkout runs. */
14
+ export function lockKind(l) {
15
+ if (l.exclusive && l.checkout === true)
16
+ return "checkout";
17
+ if (l.exclusive)
18
+ return "whole";
19
+ if (l.checkout === false)
20
+ return "worktree";
21
+ return "checkout";
22
+ }
13
23
  /** Field 22 (starttime, in clock ticks) of a /proc/<pid>/stat line. The command name (field 2) may contain spaces and ")". */
14
24
  export function parseProcStat(text) {
15
25
  const end = text.lastIndexOf(")");
@@ -137,10 +147,9 @@ function heldError(info) {
137
147
  return new Error(`Run ${info.run_id} is still running (pid ${info.pid}, heartbeat ${Number.isFinite(late) ? late : "?"}s ago). ` +
138
148
  `A run cannot be started or resumed twice; wait for it to end, or stop it first.`);
139
149
  }
140
- function limitError(active, max) {
141
- const list = active.map((l) => `${l.run_id} (pid ${l.pid})`).join(", ");
142
- return new Error(`This project already has ${active.length} run(s) active: ${list}. At most ${max} may run at once (dispatcher.max_concurrent_runs: ${max}); ` +
143
- `wait for one to end, stop one, or raise max_concurrent_runs in project.yaml.`);
150
+ function checkoutBusyError(holder, dir, reason) {
151
+ return new Error(`Run ${holder.run_id} (pid ${holder.pid}) is working directly in ${dir}, and this run would too (${reason}), so only one can be active. ` +
152
+ `Wait for it to end or stop it; in a git repo with a commit, runs work in their own worktrees and can run side by side.`);
144
153
  }
145
154
  function exclusiveError(holder) {
146
155
  return new Error(`${holder.run_id} (pid ${holder.pid}) needs the project to itself (a memory tidy or restore); wait for it to finish, then try again.`);
@@ -214,7 +223,6 @@ export function acquireProjectLock(root, runId, opts = {}) {
214
223
  assertRunId(runId);
215
224
  fs.mkdirSync(path.join(root, LOCKS_DIR), { recursive: true });
216
225
  const file = runLockFile(root, runId);
217
- const max = opts.maxConcurrent ?? 1;
218
226
  const now = new Date().toISOString();
219
227
  const info = {
220
228
  token: crypto.randomBytes(16).toString("hex"),
@@ -224,8 +232,10 @@ export function acquireProjectLock(root, runId, opts = {}) {
224
232
  run_id: runId,
225
233
  acquired_at: now,
226
234
  heartbeat_at: now,
227
- ...(opts.exclusive ? { exclusive: true } : {}),
235
+ ...(opts.exclusive || opts.checkout ? { exclusive: true } : {}),
236
+ checkout: opts.checkout === true,
228
237
  };
238
+ const kind = lockKind(info);
229
239
  withAdmission(root, () => {
230
240
  // 1. This run's own lock: a live holder means it is already running; a dead one is replaced.
231
241
  const own = readLockFile(file);
@@ -246,13 +256,14 @@ export function acquireProjectLock(root, runId, opts = {}) {
246
256
  fs.rmSync(legacyFile(root), { force: true });
247
257
  // 3. The other runs that are active now.
248
258
  const active = listRunLocks(root).filter((l) => l.run_id !== runId && lockHolderAlive(l));
249
- const exclusiveHolder = active.find((l) => l.exclusive);
250
- if (exclusiveHolder)
251
- throw exclusiveError(exclusiveHolder);
252
- if (opts.exclusive && active.length)
259
+ const whole = active.find((l) => lockKind(l) === "whole");
260
+ if (whole)
261
+ throw exclusiveError(whole);
262
+ if (kind === "whole" && active.length)
253
263
  throw needsIdleError(active);
254
- if (active.length >= max)
255
- throw limitError(active, max);
264
+ const busy = kind === "checkout" ? active.find((l) => lockKind(l) === "checkout") : undefined;
265
+ if (busy)
266
+ throw checkoutBusyError(busy, opts.dir ?? root, opts.reason ?? "no worktree is available for it");
256
267
  if (!tryCreate(file, info))
257
268
  throw new Error(`Could not take the run lock ${file}; another process just took it. Try again.`);
258
269
  });
package/dist/prompt.js CHANGED
@@ -131,9 +131,6 @@ export function buildSystemPrompt(project, agent) {
131
131
  lines.push("", "## Rules");
132
132
  lines.push("- Never edit any `AGENT.md`, `COMMON.md`, or the repository's own `CLAUDE.md`/`AGENTS.md`" +
133
133
  (isLead || agent.canEditAgentMd ? " unless you need to as lead (changes are logged)." : "."), "- Never touch another agent's memory, inbox or outbox, or the team's config files.", `- The shared team context is in \`${commonFile(project)}\`${isLead ? " (you maintain it; keep it short)" : " (read-only)"}.`, PR_ATTRIBUTION_RULE, NO_DAEMONS_RULE);
134
- if (project.dispatcher.max_parallel > 1 && agent.owns.length > 0 && !isLead) {
135
- lines.push(`- You own these repo paths and must only change files there: ${agent.owns.join(", ")}.`);
136
- }
137
134
  return lines.join("\n");
138
135
  }
139
136
  /** Messages one wake-up handles: the oldest one, or for the lead a run of `reply`/`failure` mail (up to LEAD_BATCH_LIMIT) so it decides once on all of it. */
@@ -1,7 +1,7 @@
1
1
  import { type Invoker } from "./adapters/index.js";
2
2
  import type { ResolvedProject } from "./config.js";
3
3
  import type { RunOutcome } from "./schema.js";
4
- import { type ResultBranch } from "./worktree.js";
4
+ import { type ResultBranch, type WorkspacePlan } from "./worktree.js";
5
5
  import { newRunId, type EndReason, type RunState } from "./run-store.js";
6
6
  import { type PreparedTask } from "./task.js";
7
7
  import { type CommandRunner, type PrOutcome } from "./open-pr.js";
@@ -52,6 +52,8 @@ export interface RunOptions {
52
52
  task?: PreparedTask;
53
53
  /** Continue this earlier run (same run dir, sessions and counters) instead of starting a new one. */
54
54
  resume?: RunState;
55
+ /** Where the run's agents work, decided by `planWorkspace` before the run's lock was taken; the session never recomputes it. */
56
+ plan: WorkspacePlan;
55
57
  /** Reopen a done run: this text becomes a new task for the lead. Only with `resume` of a run that ended `done`. */
56
58
  followup?: {
57
59
  text: string;
@@ -6,13 +6,12 @@ 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";
8
8
  import { abandonClaim, attemptLogDir, sourceIdOf, beginAttempt, claimMessages, commitClaim, finishAttempt, markOutputReady, parkOutbox, recoverRunMail, RouteJournal } from "./message-store.js";
9
- import { isInside } from "./paths.js";
10
9
  import { createRunLog } from "./run-log.js";
11
10
  import { endOutcome, resolveDoneOutcome } from "./run-outcome.js";
12
- import { outboxDir, ownsDirs, sharedIndex } from "./policy.js";
11
+ import { outboxDir, sharedIndex } from "./policy.js";
13
12
  import { buildSystemPrompt, buildUserPrompt, pickMessages } from "./prompt.js";
14
13
  import { atomicWrite, readFileCapped } from "./fs-util.js";
15
- import { assertWorktreeRunnable, collectAgentChanges, dirtyPaths, existingAgentWorkspace, finishLeadBranch, integrateAgentChanges, prepareAgentWorkspace, recutLeadWorkspace, refusedIntegration, resolveWorkspaceMode, snapshotBase, snapshotHead, toplevel } from "./worktree.js";
14
+ import { assertWorktreeRunnable, collectAgentChanges, dirtyPaths, existingAgentWorkspace, finishLeadBranch, integrateAgentChanges, prepareAgentWorkspace, recutLeadWorkspace, refusedIntegration, snapshotBase, snapshotHead, toplevel } from "./worktree.js";
16
15
  import { bindRunProject, newRunId, newRunState, roundLimit, roundsLabel, saveRunState } from "./run-store.js";
17
16
  import { soloProject } from "./solo.js";
18
17
  import { taskMessageBody } from "./task.js";
@@ -91,12 +90,13 @@ export class RunSession {
91
90
  const layout = resume ? resume.mail_layout : "run";
92
91
  // Each run gets its own memory directory per agent and (new runs) its own mailboxes.
93
92
  const project = bindRunProject(base, runDir, layout);
94
- // Parallel agents work in their own git worktrees; refuse early (before anything is written) when that cannot be done.
95
- // Whether the lead has its own worktree is decided when the run starts and kept for its resumes, whatever the config says by then.
96
- const concurrent = resume ? resume.lead_workspace === true : cfg.max_concurrent_runs > 1;
97
- const wsMode = concurrent ? "worktree" : resolveWorkspaceMode({ ...cfg, max_concurrent_runs: 1 });
98
- if (wsMode === "worktree")
99
- assertWorktreeRunnable(project, { fresh: !resume, concurrent });
93
+ // Where agents work was decided before the lock was taken and is kept for the run's resumes. A resumed run that uses worktrees
94
+ // is refused early (before anything is written) when its repo can no longer give them.
95
+ const { plan } = opts;
96
+ const concurrent = plan.leadWorktree;
97
+ const wsMode = plan.mode;
98
+ if (resume && wsMode === "worktree")
99
+ assertWorktreeRunnable(project);
100
100
  // Concurrent runs: the lead works in its own worktree cut from HEAD, so no other run (and not you) shares its files.
101
101
  let leadSpace;
102
102
  let recut = false;
@@ -123,6 +123,9 @@ export class RunSession {
123
123
  say(` starting from HEAD ${leadSpace.base.slice(0, 8)}${uncommitted ? ` (your ${uncommitted} uncommitted file(s) are not part of this run)` : ""}; ${project.lead} works on branch ${leadSpace.branch}`);
124
124
  log("lead-workspace", { base: leadSpace.base, branch: leadSpace.branch, dir: leadSpace.dir, uncommitted });
125
125
  }
126
+ if (!resume && plan.mode === "shared" && plan.reason) {
127
+ say(` agents take turns in ${project.dir} (no separate git worktrees: ${plan.reason}); commit to a git repo to run them in parallel`);
128
+ }
126
129
  let state;
127
130
  if (resume) {
128
131
  state = { ...resume, max_rounds: roundLimit(resume, cfg.max_rounds), pid: process.pid, ended_at: undefined, end_reason: undefined, failure: undefined, workspace_mode: wsMode, outcome: undefined, outcome_note: undefined, verification: undefined, active: {}, wakes: [...resume.wakes] };
@@ -261,26 +264,13 @@ export class RunSession {
261
264
  return rows.map((r) => r.a);
262
265
  }
263
266
  pickBatch(pending) {
264
- const { cfg, project } = this;
267
+ const { project } = this;
265
268
  const lead = pending.find((a) => a.name === project.lead);
266
- if (lead || cfg.max_parallel <= 1)
267
- return [must(lead ?? pending[0], "an agent to wake")];
268
- const batch = [];
269
- const taken = [];
270
- for (const a of pending) {
271
- if (batch.length >= cfg.max_parallel)
272
- break;
273
- const mine = ownsDirs(project, a);
274
- const clash = mine.some((x) => taken.some((y) => isInside(x, y) || isInside(y, x)));
275
- if (clash || mine.length === 0) {
276
- if (batch.length === 0)
277
- batch.push(a);
278
- continue;
279
- }
280
- batch.push(a);
281
- taken.push(...mine);
282
- }
283
- return batch;
269
+ if (lead)
270
+ return [lead];
271
+ if (this.opts.plan.mode !== "worktree")
272
+ return [must(pending[0], "an agent to wake")];
273
+ return pending;
284
274
  }
285
275
  async wake(agent) {
286
276
  const { invoke, say, cfg, runDir, project, log, state, claims, attempts, noSessionNoted, opts } = this;
@@ -399,7 +389,7 @@ export class RunSession {
399
389
  let changes;
400
390
  let r;
401
391
  try {
402
- changes = collectAgentChanges(ws, agent.owns);
392
+ changes = collectAgentChanges(ws);
403
393
  r = integrateAgentChanges(project, changes, this.leadSpace);
404
394
  }
405
395
  catch (e) {
@@ -669,7 +659,7 @@ export class RunSession {
669
659
  if (endReason !== "done" || outcome !== "completed")
670
660
  return skip(`the run did not end completed (${endReason}, outcome ${outcome}).`);
671
661
  if (!this.leadSpace)
672
- return skip("this run has no worktree of its own, so there is no branch to push. Set dispatcher.max_concurrent_runs: 2 (or more) in project.yaml.");
662
+ return skip(`this run works directly in ${this.project.dir} (not a git repo with a commit), so there is no branch to push.`);
673
663
  if (!rb)
674
664
  return skip(`the work could not be committed on ${this.leadSpace.branch}.`);
675
665
  if (!rb.files)
@@ -78,7 +78,7 @@ export interface RunState {
78
78
  snapshots?: number;
79
79
  /** Concurrent runs: where the lead's work ended up (`git merge <branch>`). Set when the run ends. */
80
80
  result_branch?: ResultBranch;
81
- /** Set when the run started with the lead in its own worktree (max_concurrent_runs > 1 then); its resumes keep that, whatever the config says later. */
81
+ /** Set when the run started with the lead in its own worktree (isolation mode); its resumes keep that, whatever the config says later. */
82
82
  lead_workspace?: true;
83
83
  /** The pull request the dispatcher opened for this run (when the lead asked for one and it worked). */
84
84
  pr_url?: string;
package/dist/scaffold.js CHANGED
@@ -86,9 +86,6 @@ team:
86
86
  lead: lead
87
87
  dispatcher:
88
88
  max_rounds: 0 # 0 = no limit on wake-ups; set a number to stop a run after that many
89
- max_parallel: 1 # >1 needs non-overlapping "owns" for every non-lead agent, and a git repo
90
- max_concurrent_runs: 1 # >1 lets several runs of this project be active at once, each in its own git worktree (needs a git repo); every run's work stays on its own branch
91
- workspace_mode: auto # auto = a git worktree per non-lead agent when max_parallel > 1; shared | worktree to force
92
89
  wake_timeout_sec: 600
93
90
  retry: 2 # an agent that fails is called again up to this many times; if it still fails the run stops (resume it after fixing the cause)
94
91
  log_max_bytes: 8388608 # log.jsonl is renamed to log.1.jsonl past this size; 0 = never
@@ -101,11 +98,9 @@ agents:
101
98
  can_edit_agent_md: true
102
99
  fe-member:
103
100
  can_message: [lead]
104
- # owns: ["src/web/**"]
105
101
  # memory: { project: agents/fe-member/memory }
106
102
  qa-member:
107
103
  can_message: [lead]
108
- # owns: ["tests/**"]
109
104
  `;
110
105
  fs.writeFileSync(pp.config, yaml);
111
106
  const common = path.join(pp.shared, "common", "COMMON.md");
package/dist/schema.d.ts CHANGED
@@ -39,23 +39,22 @@ export declare const AgentPartial: z.ZodObject<{
39
39
  none: "none";
40
40
  search: "search";
41
41
  }>>;
42
- owns: z.ZodOptional<z.ZodArray<z.ZodString>>;
42
+ owns: z.ZodOptional<z.ZodUnknown>;
43
43
  }, z.core.$strict>;
44
44
  export type AgentPartialT = z.infer<typeof AgentPartial>;
45
45
  export declare const DispatcherPartial: z.ZodObject<{
46
46
  max_rounds: z.ZodOptional<z.ZodNumber>;
47
- max_parallel: z.ZodOptional<z.ZodNumber>;
48
- max_concurrent_runs: z.ZodOptional<z.ZodNumber>;
47
+ max_parallel: z.ZodOptional<z.ZodUnknown>;
48
+ max_concurrent_runs: z.ZodOptional<z.ZodUnknown>;
49
49
  wake_timeout_sec: z.ZodOptional<z.ZodNumber>;
50
50
  retry: z.ZodOptional<z.ZodNumber>;
51
51
  strict: z.ZodOptional<z.ZodBoolean>;
52
- workspace_mode: z.ZodOptional<z.ZodEnum<{
53
- auto: "auto";
54
- shared: "shared";
55
- worktree: "worktree";
56
- }>>;
52
+ workspace_mode: z.ZodOptional<z.ZodUnknown>;
57
53
  log_max_bytes: z.ZodOptional<z.ZodNumber>;
58
54
  }, z.core.$strict>;
55
+ /** Settings that no longer exist; files that still carry them load, and `validate` warns. */
56
+ export declare const REMOVED_DISPATCHER_KEYS: readonly ["max_parallel", "max_concurrent_runs", "workspace_mode"];
57
+ export declare const REMOVED_AGENT_KEYS: readonly ["owns"];
59
58
  export declare const GlobalConfig: z.ZodObject<{
60
59
  agents: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
61
60
  runtime: z.ZodOptional<z.ZodEnum<{
@@ -85,7 +84,7 @@ export declare const GlobalConfig: z.ZodObject<{
85
84
  none: "none";
86
85
  search: "search";
87
86
  }>>;
88
- owns: z.ZodOptional<z.ZodArray<z.ZodString>>;
87
+ owns: z.ZodOptional<z.ZodUnknown>;
89
88
  }, z.core.$strict>>>;
90
89
  }, z.core.$strict>;
91
90
  export declare const ProjectConfig: z.ZodObject<{
@@ -95,16 +94,12 @@ export declare const ProjectConfig: z.ZodObject<{
95
94
  }, z.core.$strict>;
96
95
  dispatcher: z.ZodDefault<z.ZodObject<{
97
96
  max_rounds: z.ZodOptional<z.ZodNumber>;
98
- max_parallel: z.ZodOptional<z.ZodNumber>;
99
- max_concurrent_runs: z.ZodOptional<z.ZodNumber>;
97
+ max_parallel: z.ZodOptional<z.ZodUnknown>;
98
+ max_concurrent_runs: z.ZodOptional<z.ZodUnknown>;
100
99
  wake_timeout_sec: z.ZodOptional<z.ZodNumber>;
101
100
  retry: z.ZodOptional<z.ZodNumber>;
102
101
  strict: z.ZodOptional<z.ZodBoolean>;
103
- workspace_mode: z.ZodOptional<z.ZodEnum<{
104
- auto: "auto";
105
- shared: "shared";
106
- worktree: "worktree";
107
- }>>;
102
+ workspace_mode: z.ZodOptional<z.ZodUnknown>;
108
103
  log_max_bytes: z.ZodOptional<z.ZodNumber>;
109
104
  }, z.core.$strict>>;
110
105
  agents: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -135,19 +130,14 @@ export declare const ProjectConfig: z.ZodObject<{
135
130
  none: "none";
136
131
  search: "search";
137
132
  }>>;
138
- owns: z.ZodOptional<z.ZodArray<z.ZodString>>;
133
+ owns: z.ZodOptional<z.ZodUnknown>;
139
134
  }, z.core.$strict>>>;
140
135
  }, z.core.$strict>;
141
136
  export interface DispatcherSettings {
142
137
  max_rounds: number;
143
- max_parallel: number;
144
- /** How many runs of one project may be active at once. Above 1, every run works in its own git worktree. */
145
- max_concurrent_runs: number;
146
138
  wake_timeout_sec: number;
147
139
  retry: number;
148
140
  strict: boolean;
149
- /** "auto": a git worktree per non-lead agent when agents can run in parallel, otherwise one shared checkout. */
150
- workspace_mode: "auto" | "shared" | "worktree";
151
141
  /** log.jsonl is rotated to log.1.jsonl past this size; 0 = never. */
152
142
  log_max_bytes: number;
153
143
  }
package/dist/schema.js CHANGED
@@ -38,21 +38,25 @@ export const AgentPartial = z
38
38
  can_edit_agent_md: z.boolean().optional(),
39
39
  can_ask_user: z.boolean().optional(),
40
40
  allow_web: z.enum(WEB_LEVELS).optional(),
41
- owns: z.array(z.string()).optional(),
41
+ /** Removed setting: still accepted (any value) so old files load; ignored, with a warning from `validate`. */
42
+ owns: z.unknown().optional(),
42
43
  })
43
44
  .strict();
44
45
  export const DispatcherPartial = z
45
46
  .object({
46
47
  max_rounds: z.number().int().min(0).optional(),
47
- max_parallel: z.number().int().positive().optional(),
48
- max_concurrent_runs: z.number().int().positive().optional(),
48
+ max_parallel: z.unknown().optional(),
49
+ max_concurrent_runs: z.unknown().optional(),
49
50
  wake_timeout_sec: z.number().int().positive().optional(),
50
51
  retry: z.number().int().min(0).optional(),
51
52
  strict: z.boolean().optional(),
52
- workspace_mode: z.enum(["auto", "shared", "worktree"]).optional(),
53
+ workspace_mode: z.unknown().optional(),
53
54
  log_max_bytes: z.number().int().min(0).optional(),
54
55
  })
55
56
  .strict();
57
+ /** Settings that no longer exist; files that still carry them load, and `validate` warns. */
58
+ export const REMOVED_DISPATCHER_KEYS = ["max_parallel", "max_concurrent_runs", "workspace_mode"];
59
+ export const REMOVED_AGENT_KEYS = ["owns"];
56
60
  export const GlobalConfig = z
57
61
  .object({
58
62
  agents: z.record(z.string(), AgentPartial).default({}),
@@ -68,12 +72,9 @@ export const ProjectConfig = z
68
72
  .strict();
69
73
  export const DISPATCHER_DEFAULTS = {
70
74
  max_rounds: 0,
71
- max_parallel: 1,
72
- max_concurrent_runs: 1,
73
75
  wake_timeout_sec: 600,
74
76
  retry: 2,
75
77
  strict: false,
76
- workspace_mode: "auto",
77
78
  log_max_bytes: 8 * 1024 * 1024,
78
79
  };
79
80
  /** How a run turned out, independent of why it stopped (`EndReason`). Only "completed" means success. */
package/dist/solo.js CHANGED
@@ -15,8 +15,7 @@ export function soloProject(project, name, hints = {}) {
15
15
  ...project,
16
16
  solo: "run",
17
17
  lead: name,
18
- dispatcher: { ...project.dispatcher, max_parallel: 1, workspace_mode: "shared" },
19
- agents: { ...project.agents, [name]: { ...a, canMessage: [], canAskUser: false, canEditAgentMd: false, owns: [] } },
18
+ agents: { ...project.agents, [name]: { ...a, canMessage: [], canAskUser: false, canEditAgentMd: false } },
20
19
  };
21
20
  }
22
21
  /** `project` with only `name` listed: what a preflight of a solo run needs to look at. */
package/dist/validate.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import fs from "node:fs";
2
- import { enforcementFor, memoryDirs, ownsDirs } from "./policy.js";
2
+ import { enforcementFor, memoryDirs } from "./policy.js";
3
3
  import { isInside } from "./paths.js";
4
4
  import { inferRuntime, RUNTIME_EFFORTS } from "./schema.js";
5
- import { isGitRepo, resolveWorkspaceMode } from "./worktree.js";
6
5
  import { must } from "./assert.js";
7
6
  export function validateProject(project) {
8
7
  const issues = [];
@@ -55,40 +54,8 @@ export function validateProject(project) {
55
54
  }
56
55
  }
57
56
  }
58
- // Parallelism requires disjoint `owns`.
59
- if (project.dispatcher.max_parallel > 1) {
60
- const withOwns = Object.values(project.agents).filter((a) => a.name !== project.lead);
61
- for (const a of withOwns) {
62
- if (a.owns.length === 0)
63
- err(`max_parallel > 1 requires "owns" for agent "${a.name}".`);
64
- }
65
- for (let i = 0; i < withOwns.length; i++) {
66
- for (let j = i + 1; j < withOwns.length; j++) {
67
- const ai = must(withOwns[i], "agent");
68
- const aj = must(withOwns[j], "agent");
69
- for (const x of ownsDirs(project, ai)) {
70
- for (const y of ownsDirs(project, aj)) {
71
- if (isInside(x, y) || isInside(y, x)) {
72
- err(`"owns" overlap: ${ai.name} (${x}) vs ${aj.name} (${y})`);
73
- }
74
- }
75
- }
76
- }
77
- }
78
- }
79
- // Parallel agents and concurrent runs each need their own checkout, which needs git.
80
- const concurrent = project.dispatcher.max_concurrent_runs > 1;
81
- if (concurrent && project.dispatcher.workspace_mode === "shared") {
82
- err(`dispatcher.workspace_mode "shared" cannot be combined with max_concurrent_runs > 1: every run needs its own git worktree (use "auto" or "worktree").`);
83
- }
84
- else if (concurrent && fs.existsSync(project.dir) && !isGitRepo(project.dir)) {
85
- err(`max_concurrent_runs > 1 needs ${project.dir} to be a git repository (each run works in its own git worktree). Set max_concurrent_runs: 1.`);
86
- }
87
- else if (project.dispatcher.workspace_mode === "shared" && project.dispatcher.max_parallel > 1) {
88
- err(`dispatcher.workspace_mode "shared" cannot be combined with max_parallel > 1: parallel agents need separate git worktrees (use "auto" or "worktree").`);
89
- }
90
- else if (!concurrent && resolveWorkspaceMode(project.dispatcher) === "worktree" && fs.existsSync(project.dir) && !isGitRepo(project.dir)) {
91
- err(`Parallel runs (worktree workspaces) need ${project.dir} to be a git repository. Run sequentially instead (max_parallel: 1, workspace_mode: auto or shared).`);
57
+ for (const ig of project.ignoredSettings ?? []) {
58
+ warn(`${ig.file}: ignoring removed setting(s) ${ig.keys.join(", ")}. Concurrency needs no configuration now: in a git repo with a commit every run and every agent gets its own worktree. Delete these lines.`);
92
59
  }
93
60
  // Context paths should live outside the repo (the tool never writes into it).
94
61
  for (const a of Object.values(project.agents)) {
@@ -109,10 +76,10 @@ export function validateProject(project) {
109
76
  return { issues, enforcement, ok: !issues.some((i) => i.level === "error") };
110
77
  }
111
78
  export function formatEnforcement(list) {
112
- const header = ["agent", "runtime", "memory", "AGENT.md", "others' ctx", "handoff", "repo CLAUDE/AGENTS", "owns"];
79
+ const header = ["agent", "runtime", "memory", "AGENT.md", "others' ctx", "handoff", "repo CLAUDE/AGENTS"];
113
80
  const rows = [header];
114
81
  for (const e of list)
115
- rows.push([e.agent, e.runtime, e.memory, e.agentMd, e.otherContext, e.shared, e.repoInstructions, e.owns]);
82
+ rows.push([e.agent, e.runtime, e.memory, e.agentMd, e.otherContext, e.shared, e.repoInstructions]);
116
83
  const w = header.map((_, i) => Math.max(...rows.map((r) => (r[i] ?? "").length)));
117
84
  const lines = rows.map((r) => r.map((c, i) => c.padEnd(w[i] ?? 0)).join(" ").trimEnd());
118
85
  lines.splice(1, 0, w.map((n) => "-".repeat(n)).join(" "));
@@ -1,8 +1,6 @@
1
1
  import type { ResolvedProject } from "./config.js";
2
- import type { DispatcherSettings } from "./schema.js";
2
+ import type { RunState } from "./run-store.js";
3
3
  export type WorkspaceMode = "shared" | "worktree";
4
- /** 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. */
5
- export declare function resolveWorkspaceMode(d: Pick<DispatcherSettings, "workspace_mode" | "max_parallel"> & Partial<Pick<DispatcherSettings, "max_concurrent_runs">>): WorkspaceMode;
6
4
  export interface AgentWorkspace {
7
5
  agent: string;
8
6
  /** Root of the git worktree. */
@@ -58,11 +56,22 @@ export declare function isGitRepo(dir: string): boolean;
58
56
  export declare const toplevel: (dir: string) => string;
59
57
  /** Files in the repo with changes nobody committed (modified, staged or new and not ignored). Paths are relative to the repo root. */
60
58
  export declare function dirtyPaths(dir: string): string[];
61
- /** Refuse to start a worktree run when it cannot be isolated; returns nothing when it is fine. */
62
- export declare function assertWorktreeRunnable(project: ResolvedProject, opts: {
63
- fresh: boolean;
64
- concurrent?: boolean;
65
- }): void;
59
+ /** Why `dir` cannot give every run and agent its own git worktree, or undefined when it can. */
60
+ export declare function isolationBlocker(dir: string): string | undefined;
61
+ export interface WorkspacePlan {
62
+ mode: WorkspaceMode;
63
+ /** The lead works in its own worktree (isolation mode); otherwise it works in the checkout. */
64
+ leadWorktree: boolean;
65
+ /** Why the run works in the checkout, when it was probed. */
66
+ reason?: string;
67
+ }
68
+ /**
69
+ * Where a run's agents work. A new run is isolated when the project dir can be (see `isolationBlocker`), otherwise it shares the
70
+ * checkout. A resumed run keeps what its state recorded, whatever the repo looks like now. Decided once, before the run's lock is taken.
71
+ */
72
+ export declare function planWorkspace(project: ResolvedProject, resume?: RunState): WorkspacePlan;
73
+ /** Refuse to resume a worktree run when its repo can no longer be isolated; returns nothing when it is fine. */
74
+ export declare function assertWorktreeRunnable(project: ResolvedProject): void;
66
75
  /** `refs/agent-lyceum/<run>/base-0` pointing at the repo's HEAD: where a concurrent run starts. Nothing in the repo changes. */
67
76
  export declare function snapshotHead(project: ResolvedProject, runId: string): string;
68
77
  /**
@@ -97,8 +106,6 @@ export interface ChangeSet {
97
106
  /** Why these changes may not be integrated (empty when they may). */
98
107
  violations: string[];
99
108
  }
100
- /** Glob as used in `owns`: `**` crosses folders, `*` and `?` stay within one, `{a,b}` alternates. */
101
- export declare function globMatch(glob: string, file: string): boolean;
102
109
  /** Commit whatever is in an agent's worktree (no hooks, no signing, dispatcher identity). Returns the HEAD afterwards and whether a commit was made. */
103
110
  export declare function commitWorkspace(ws: AgentWorkspace, message: string): {
104
111
  head: string;
@@ -106,10 +113,10 @@ export declare function commitWorkspace(ws: AgentWorkspace, message: string): {
106
113
  };
107
114
  /**
108
115
  * Commit whatever the agent left in its worktree and list what changed since its snapshot, checking every
109
- * changed path (modified, added, deleted, or either end of a rename) against the agent's `owns`, the project
116
+ * changed path (modified, added, deleted, or either end of a rename) against the project
110
117
  * folder, the protected instruction files, and symlink/submodule rules. Nothing is applied here.
111
118
  */
112
- export declare function collectAgentChanges(workspace: AgentWorkspace, owns: string[]): ChangeSet;
119
+ export declare function collectAgentChanges(workspace: AgentWorkspace): ChangeSet;
113
120
  export type IntegrationResult = {
114
121
  status: "integrated";
115
122
  files: ChangedFile[];