cyberlegion 0.3.2 → 0.5.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.
Files changed (33) hide show
  1. package/.claude-plugin/plugin.json +20 -0
  2. package/.codex-plugin/plugin.json +20 -0
  3. package/.plugin/pins.json +3 -0
  4. package/agents/headless-legate.md +59 -0
  5. package/bin/cyberlegion.mjs +18 -1
  6. package/dist/cli.mjs +7823 -72
  7. package/dist/index.d.mts +262 -11
  8. package/dist/index.mjs +2298 -4
  9. package/package.json +10 -3
  10. package/plugin.json +24 -0
  11. package/readme.md +63 -0
  12. package/skills/dispatch-governance/README.md +29 -0
  13. package/skills/dispatch-governance/SKILL.md +143 -0
  14. package/skills/dispatch-governance/scripts/cyberlegion.mjs +31 -0
  15. package/skills/init-cyberlegion/README.md +14 -0
  16. package/skills/init-cyberlegion/SKILL.md +112 -0
  17. package/skills/init-cyberlegion/scripts/cyberlegion.mjs +31 -0
  18. package/skills/legate/README.md +22 -0
  19. package/skills/legate/SKILL.md +62 -0
  20. package/skills/legate/scripts/cyberlegion.mjs +31 -0
  21. package/skills/manage-inbox/README.md +12 -0
  22. package/skills/manage-inbox/SKILL.md +90 -0
  23. package/skills/manage-inbox/scripts/cyberlegion.mjs +31 -0
  24. package/skills/relay-governance/README.md +36 -0
  25. package/skills/relay-governance/SKILL.md +204 -0
  26. package/skills/relay-governance/scripts/cyberlegion.mjs +31 -0
  27. package/skills/session-adapter-governance/README.md +17 -0
  28. package/skills/session-adapter-governance/SKILL.md +74 -0
  29. package/skills/subagent-backend-governance/README.md +35 -0
  30. package/skills/subagent-backend-governance/SKILL.md +99 -0
  31. package/skills/subagent-backend-governance/scripts/cyberlegion.mjs +31 -0
  32. package/dist/cli.d.mts +0 -12
  33. package/dist/watch-D33fQcUI.mjs +0 -2044
package/dist/index.d.mts CHANGED
@@ -55,8 +55,15 @@ interface AgentRecord {
55
55
  brief?: string;
56
56
  spawnedBy?: string;
57
57
  /** Absent ⇒ session (backward compat, no migration). 'standing' = a session-independent,
58
- * prune-exempt owner inbox minted by `unit register --standing`. */
59
- kind?: 'session' | 'standing';
58
+ * prune-exempt owner inbox minted by `unit register --standing`. 'service' = a project service's
59
+ * endpoint (`service.ts`): session-independent and prune-exempt like a standing record, but never
60
+ * surfaced as owner mail — its mailbox belongs to whichever unit currently owns the service. */
61
+ kind?: 'session' | 'standing' | 'service';
62
+ /** Only meaningful on a `kind: 'service'` record — which project service this endpoint is. */
63
+ service?: {
64
+ project: string;
65
+ name: string;
66
+ };
60
67
  /** Only meaningful on a `kind: 'standing'` record — the id of the unit currently standing in for
61
68
  * it (`unit claim`), a per-record singleton pointer that moves as the principal moves between
62
69
  * units (last claim wins). Resolved live, never trusted: a presence unit that has exited reads as
@@ -64,6 +71,42 @@ interface AgentRecord {
64
71
  * stays inert until re-claimed. Independent of the hub's `mainPane` (the human's read-pane). */
65
72
  presence?: string;
66
73
  }
74
+ /** A registered project: one git repository, shared by its default checkout and every linked
75
+ * worktree of it. Keyed by its git common dir, never by a pane or a display name. */
76
+ interface ProjectRecord {
77
+ /** Stable reference derived from the canonical git common dir (`project.ts`). */
78
+ id: string;
79
+ /** Display name — the default checkout's directory basename. Not unique; never a key. */
80
+ name: string;
81
+ /** The default checkout's root. */
82
+ root: string;
83
+ /** The canonical (realpath) git common dir the id is derived from. */
84
+ commonDir: string;
85
+ registeredAt: string;
86
+ }
87
+ /** A project service's ownership lease (`service.ts`) — who authoritatively owns the service now,
88
+ * persisted independently of any runtime so ownership survives the runtime that held it. */
89
+ interface ServiceLease {
90
+ project: string;
91
+ service: string;
92
+ /** The service endpoint's record id — the durable mailbox and the stable reference. */
93
+ endpoint: string;
94
+ /** Fencing generation: bumped on every change of authority (a new reservation or a handoff), so a
95
+ * runtime holding an older generation is provably stale. */
96
+ generation: number;
97
+ /** `vacant` — no owner, no start in progress. `reserved` — a start is in progress (or failed and
98
+ * not yet expired). `active` — `holder` owns the service at `generation`. */
99
+ state: 'vacant' | 'reserved' | 'active';
100
+ holder?: string;
101
+ reservation?: {
102
+ token: string;
103
+ by?: string;
104
+ at: string;
105
+ expiresAt: string;
106
+ forced?: boolean;
107
+ };
108
+ updatedAt: string;
109
+ }
67
110
  interface InboxSnapshot {
68
111
  unread: Message[];
69
112
  read: Message[];
@@ -89,6 +132,14 @@ interface Store {
89
132
  listAgents(): AgentRecord[];
90
133
  removeAgent(id: string): void;
91
134
  removeAgentData(id: string): void;
135
+ /** Upsert a project record (keyed by `rec.id`). */
136
+ putProject(rec: ProjectRecord): void;
137
+ getProject(id: string): ProjectRecord | undefined;
138
+ listProjects(): ProjectRecord[];
139
+ /** Upsert a service lease (keyed by `lease.project` + `lease.service`). Callers mutate a lease only
140
+ * while holding that service's named lock (`service.ts`). */
141
+ putServiceLease(lease: ServiceLease): void;
142
+ getServiceLease(project: string, service: string): ServiceLease | undefined;
92
143
  putPaneIndex(pane: string, agentId: string): void;
93
144
  resolvePaneId(pane: string): string | undefined;
94
145
  /** Reverse-lookup: an agent's pane from the index, when the agent record itself carries none
@@ -120,6 +171,10 @@ interface IdContext {
120
171
  env?: NodeJS.ProcessEnv;
121
172
  exec?: Exec$1;
122
173
  now?: () => number;
174
+ /** The argv that re-invokes this very CLI — node, its loader flags, and the entry script
175
+ * (`selfInvocation`). `unit spawn` puts a `cyberlegion` shim over it on the new session's PATH,
176
+ * so a unit reports through the install that spawned it. Absent, spawn writes no shim. */
177
+ self?: string[];
123
178
  }
124
179
  declare function loadAgent(store: Store, id: string): AgentRecord | undefined;
125
180
  declare function listAgents(store: Store): AgentRecord[];
@@ -192,17 +247,25 @@ declare function listAgentDefs(input?: {
192
247
  interface RealizeLaunchOptions {
193
248
  /** Overrides def.model when present. */
194
249
  model?: string;
250
+ /** Overrides def.effort when present. */
251
+ effort?: string;
195
252
  /** Overrides def.harness when present; falls back to 'claude' when neither is set. */
196
253
  harness?: Harness;
197
254
  }
198
255
  interface RealizedLaunch {
199
256
  harness: Harness;
200
257
  command: string;
258
+ /** The model and effort the command launches with, from whichever source won; absent when the
259
+ * harness default applies. */
260
+ model?: string;
261
+ effort?: string;
262
+ /** The def's instructions, for a harness whose CLI cannot take them (cursor): `unit spawn` writes
263
+ * them in front of the brief instead. Absent when the command carries them, or there are none. */
264
+ briefInstructions?: string;
201
265
  }
202
- /** Build the harness launch invocation for a def — explicit `model`/`harness` win over the def's
203
- * own tags, which win over the harness default. Every harness gets the same shape (bin,
204
- * `--model` when known, `--append-system-prompt` carrying the def's instructions body) since the
205
- * exact per-harness flag surface is realized later by the launching caller, not by cyberlegion. */
266
+ /** Build the harness launch invocation for a def — explicit `model`/`effort`/`harness` win over the
267
+ * def's own tags, which win over the harness default. The def's effort and instructions each go
268
+ * through the harness's own control (see `modelAndEffortArgs`, `instructionArgs`). */
206
269
  declare function realizeLaunch(def: AgentDef, opts?: RealizeLaunchOptions): RealizedLaunch;
207
270
  //#endregion
208
271
  //#region src/decommission.d.ts
@@ -210,18 +273,33 @@ interface DecommissionInput {
210
273
  id: string;
211
274
  /** Discard uncommitted changes in the worktree. Never overrides refusing the primary checkout. */
212
275
  force?: boolean;
276
+ /**
277
+ * Reap the unit but leave its worktree on disk, so a pool can detach it and hand it to the next
278
+ * unit — far cheaper than a fresh checkout per unit. Never overrides refusing the primary
279
+ * checkout: that guard is about identity, not destructiveness.
280
+ */
281
+ keepWorktree?: boolean;
213
282
  }
214
283
  interface DecommissionResult {
215
284
  agent: AgentRecord;
216
285
  worktreeRoot?: string;
286
+ /**
287
+ * The worktree left on disk by `keepWorktree` — set only when a worktree was actually there to
288
+ * keep, so a pool manager can treat its presence as "this path is reusable" rather than
289
+ * re-checking `worktreeRoot` against disk.
290
+ */
291
+ retainedWorktree?: string;
217
292
  pane?: string;
218
293
  }
219
294
  /**
220
295
  * Tear a unit down and reap its registry record — the deterministic inverse of `spawn`. Refuses
221
- * the primary checkout (absolute — `--force` never overrides it) and a dirty worktree unless
222
- * `--force`. Teardown always precedes reap: an already-gone worktree or pane is tolerated, but a
223
- * genuine worktree-removal failure aborts and leaves the record intact so the operation is
224
- * retryable.
296
+ * the primary checkout (absolute — neither `--force` nor `--keep-worktree` overrides it) and a
297
+ * dirty worktree unless `--force`. Teardown always precedes reap: an already-gone worktree or pane
298
+ * is tolerated, but a genuine worktree-removal failure aborts and leaves the record intact so the
299
+ * operation is retryable.
300
+ *
301
+ * `keepWorktree` reaps everything else — pane, record, pane pointer, brief — and leaves the
302
+ * checkout on disk, reporting it as `retainedWorktree`.
225
303
  */
226
304
  declare function decommission(ctx: IdContext, input: DecommissionInput): DecommissionResult;
227
305
  //#endregion
@@ -372,6 +450,9 @@ declare const paths: {
372
450
  dataDir: (root: string, id: string) => string;
373
451
  briefFile: (root: string, id: string) => string;
374
452
  mainPaneFile: (root: string) => string;
453
+ projectsDir: (root: string) => string;
454
+ serviceLeaseFile: (root: string, project: string, service: string) => string;
455
+ projectFile: (root: string, id: string) => string;
375
456
  /** A message's file path within `toId`'s unread/read inbox dir, keyed by its own collision-free
376
457
  * id — validated the same as an agent id (it's the same class of risk: a peer- or CLI-controlled
377
458
  * string becoming a filename). */
@@ -379,6 +460,168 @@ declare const paths: {
379
460
  messageReadFile: (root: string, toId: string, msgId: string) => string;
380
461
  };
381
462
  //#endregion
463
+ //#region src/project.d.ts
464
+ interface ProjectContext {
465
+ store: Store;
466
+ exec?: Exec;
467
+ now?: () => number;
468
+ }
469
+ /**
470
+ * Register (or idempotently refresh) the project containing `dir` — any checkout of it, default or
471
+ * linked. Keeps the first `registeredAt`, so re-registering from a worktree is a no-op in effect.
472
+ */
473
+ declare function registerProject(ctx: ProjectContext, input?: {
474
+ dir?: string;
475
+ }): ProjectRecord;
476
+ declare function listProjects(store: Store): ProjectRecord[];
477
+ /**
478
+ * Resolve a project from anywhere — by id, by a path inside any of its checkouts, or by name when
479
+ * exactly one registered project carries it. A path registers its project on first use: git has
480
+ * already confirmed it is a repository, and registering is idempotent, so there is nothing a typo
481
+ * could mint. A name never registers — nothing can be found by a name no checkout has reported —
482
+ * and an ambiguous name names its candidates rather than picking one.
483
+ */
484
+ declare function resolveProject(ctx: ProjectContext, ref: string): ProjectRecord;
485
+ //#endregion
486
+ //#region src/service.d.ts
487
+ interface ServiceContext extends IdContext {
488
+ /** Whether a unit's session is still there. Defaults to asking its multiplexer (`sessionLive`). */
489
+ isLive?: (unit: AgentRecord) => boolean;
490
+ }
491
+ type ServiceHealth = 'vacant' | 'starting' | 'expired' | 'healthy' | 'unhealthy';
492
+ interface ServiceView {
493
+ project: ProjectRecord;
494
+ endpoint: AgentRecord;
495
+ lease: ServiceLease;
496
+ /** The owning unit's record, when the lease is active and the record still exists. */
497
+ owner?: AgentRecord;
498
+ health: ServiceHealth;
499
+ /** Whether the owner's session can be controlled (focused, nudged, read) — `pane` when it has a
500
+ * multiplexer pane, `none` otherwise. Resolving the address never implies control. */
501
+ control: 'pane' | 'none';
502
+ /** A human-readable qualification of `health`/`control`, when there is one. */
503
+ note?: string;
504
+ }
505
+ type AcquireResult = {
506
+ outcome: 'resolved';
507
+ lease: ServiceLease;
508
+ view: ServiceView;
509
+ } | {
510
+ outcome: 'starting';
511
+ lease: ServiceLease;
512
+ view: ServiceView;
513
+ } | {
514
+ outcome: 'reserved';
515
+ lease: ServiceLease;
516
+ view: ServiceView;
517
+ token: string;
518
+ };
519
+ type OwnershipErrorCode = 'stale' | 'unit-not-live' | 'unknown-unit';
520
+ /** A refused ownership transition. `stale` means the caller's generation, token, or holder claim is
521
+ * not the current one — the caller is not (or no longer) the authority it claims to be. */
522
+ declare class ServiceOwnershipError extends Error {
523
+ readonly code: OwnershipErrorCode;
524
+ constructor(code: OwnershipErrorCode, message: string);
525
+ }
526
+ /** How long a reservation holds before another caller may take over a start that never bound. */
527
+ declare const DEFAULT_RESERVATION_TTL_MS: number;
528
+ declare function serviceEndpointId(projectId: string, name: string): string;
529
+ /** Read a service's ownership without changing anything. Throws for a service never started. */
530
+ declare function resolveService(ctx: ServiceContext, projectRef: string, name: string): ServiceView;
531
+ interface AcquireInput {
532
+ /** Who is starting the service — recorded on the reservation for diagnosis. */
533
+ by?: string;
534
+ ttlMs?: number;
535
+ /** Replace even a healthy owner or an unexpired reservation. Must name the current generation, so
536
+ * a force decided on a stale read fails instead of displacing an owner the caller never saw. */
537
+ force?: {
538
+ generation: number;
539
+ };
540
+ }
541
+ /**
542
+ * Contact-or-start: resolve the service's healthy owner, or reserve the right to start one. Exactly
543
+ * one of any number of concurrent callers gets `reserved`; the rest see `starting` (a start is in
544
+ * progress) or `resolved`. A vacant service, an unhealthy owner, and an expired reservation are all
545
+ * reservable, each under a new generation that fences out whatever held the old one.
546
+ */
547
+ declare function acquireService(ctx: ServiceContext, projectRef: string, name: string, input?: AcquireInput): AcquireResult;
548
+ /**
549
+ * Complete a reservation: make `unit` the owner at the reserved generation. Refused when the
550
+ * reservation is no longer the current one (it expired and someone else re-reserved, or it was
551
+ * released) — the starter then holds a runtime that is not the authority and must stop it. A
552
+ * reservation that expired with nobody taking over is still bindable: nothing replaced it.
553
+ */
554
+ declare function bindService(ctx: ServiceContext, projectRef: string, name: string, input: {
555
+ generation: number;
556
+ token: string;
557
+ unit: string;
558
+ }): ServiceView;
559
+ /**
560
+ * Give the service up: abandon a reservation (a failed start — pass its `token`) or step down as
561
+ * owner (pass the holder `unit`). Either must match the current generation. The generation is kept;
562
+ * the next acquire bumps it.
563
+ */
564
+ declare function releaseService(ctx: ServiceContext, projectRef: string, name: string, input: {
565
+ generation: number;
566
+ token?: string;
567
+ unit?: string;
568
+ }): ServiceView;
569
+ /**
570
+ * Transfer ownership from the current holder to another live unit, under a new generation. Only the
571
+ * holder at the current generation can hand off, and afterwards it is stale.
572
+ */
573
+ declare function handoffService(ctx: ServiceContext, projectRef: string, name: string, input: {
574
+ generation: number;
575
+ from: string;
576
+ to: string;
577
+ }): ServiceView;
578
+ /**
579
+ * The fencing check: succeed only when `unit` owns the service at exactly `generation`. A runtime
580
+ * calls this before acting as the service's authority; a stale runtime — replaced, handed off, or
581
+ * recovered past — is refused.
582
+ */
583
+ declare function verifyOwnership(ctx: ServiceContext, projectRef: string, name: string, input: {
584
+ unit: string;
585
+ generation: number;
586
+ }): ServiceView;
587
+ /**
588
+ * Run `fn` as the verified owner, holding the service lock so no transition can interleave between
589
+ * the check and the act. For short critical sections only — the lock is bounded-wait for everyone
590
+ * else — and never reentrant: `fn` must not call another service transition on the same service.
591
+ */
592
+ declare function withOwnership<T>(ctx: ServiceContext, projectRef: string, name: string, input: {
593
+ unit: string;
594
+ generation: number;
595
+ }, fn: (view: ServiceView) => T): T;
596
+ interface StartInput<T extends {
597
+ unit: string;
598
+ }> extends AcquireInput {
599
+ /** Launch the runtime that will own the service, returning its unit id. Called only by the one
600
+ * caller holding the reservation; `generation` is the generation that runtime will own at. */
601
+ launch: (reservation: {
602
+ generation: number;
603
+ endpoint: string;
604
+ }) => Promise<T>;
605
+ }
606
+ type StartResult<T> = {
607
+ outcome: 'resolved' | 'starting';
608
+ view: ServiceView;
609
+ } | {
610
+ outcome: 'started';
611
+ view: ServiceView;
612
+ launched: T;
613
+ };
614
+ /**
615
+ * Resolve-or-start end to end: acquire, and only when this caller wins the reservation, launch a
616
+ * runtime and bind it. A launch that throws releases the reservation (best-effort — an expiry covers
617
+ * a release that itself fails) so the start is retryable at once. A launch that finishes after its
618
+ * reservation was superseded is refused at bind: the launched unit is not the owner, and the error
619
+ * names it so the caller can stop it.
620
+ */
621
+ declare function startService<T extends {
622
+ unit: string;
623
+ }>(ctx: ServiceContext, projectRef: string, name: string, input: StartInput<T>): Promise<StartResult<T>>;
624
+ //#endregion
382
625
  //#region src/session.d.ts
383
626
  interface SpawnInput {
384
627
  harness?: string;
@@ -390,6 +633,9 @@ interface SpawnInput {
390
633
  * (avoids a session.ts ↔ agentdef/realize.ts import cycle, since `realizeLaunch` already reads
391
634
  * `LAUNCH_MAP` from here). */
392
635
  command?: string;
636
+ /** A def's instructions its harness cannot take on the command line (an agent-def's
637
+ * `realizeLaunch` output, for cursor): written in front of the brief under their own heading. */
638
+ briefInstructions?: string;
393
639
  /** Branch to create the unit's worktree on; defaults to `cyberlegion/unit-<id>`. */
394
640
  branch?: string;
395
641
  /** Where to check out the unit's worktree; defaults to a sibling of the primary checkout
@@ -458,6 +704,11 @@ declare class FileStore implements Store {
458
704
  listAgents(): AgentRecord[];
459
705
  removeAgent(id: string): void;
460
706
  removeAgentData(id: string): void;
707
+ putProject(rec: ProjectRecord): void;
708
+ getProject(id: string): ProjectRecord | undefined;
709
+ listProjects(): ProjectRecord[];
710
+ putServiceLease(lease: ServiceLease): void;
711
+ getServiceLease(project: string, service: string): ServiceLease | undefined;
461
712
  putPaneIndex(pane: string, agentId: string): void;
462
713
  resolvePaneId(pane: string): string | undefined;
463
714
  findPaneByAgentId(agentId: string): string | undefined;
@@ -521,4 +772,4 @@ interface WatchOptions {
521
772
  */
522
773
  declare function watchMail(ctx: MsgContext, input: WatchInput, onMessage: (msg: InboxItem) => void, opts?: WatchOptions): Promise<void>;
523
774
  //#endregion
524
- export { type AgentDef, type AgentRecord, type AwaitInput, type AwaitOutcome, type DecommissionInput, type DecommissionResult, type Exec, FileStore, type Format, type Harness, type HookEvent, type IdContext, type InboxItem, type InboxQuery, type InboxSnapshot, type Message, type MuxProbe, type RegisterInput, type RootOptions, type SendInput, type SpawnInput, type SpawnResult, type Store, ack, assertDistinctFromPrimary, awaitReply, bumpLastSeen, decommission, deleteMessage, detectHarness, emit, ensureMarker, fail, gitWorktreeAdapter, inbox, injectInbox, install, listAgentDefs, listAgents, loadAgent, nextStep, paths, peek, probeMultiplexer, projectRoot, prune, realExec, realizeLaunch, register, resolveAgent, resolveAgentDef, resolveBody, resolvePrimaryRoot, resolveProjectLocalRoot, resolveRecipient, resolveRoot, resolveSelfId, selectSessionAdapter, send, spawn, spawnAndWake, toonList, toonObject, touch, watchMail };
775
+ export { type AcquireInput, type AcquireResult, type AgentDef, type AgentRecord, type AwaitInput, type AwaitOutcome, DEFAULT_RESERVATION_TTL_MS, type DecommissionInput, type DecommissionResult, type Exec, FileStore, type Format, type Harness, type HookEvent, type IdContext, type InboxItem, type InboxQuery, type InboxSnapshot, type Message, type MuxProbe, type OwnershipErrorCode, type ProjectContext, type ProjectRecord, type RegisterInput, type RootOptions, type SendInput, type ServiceContext, type ServiceHealth, type ServiceLease, ServiceOwnershipError, type ServiceView, type SpawnInput, type SpawnResult, type StartInput, type StartResult, type Store, ack, acquireService, assertDistinctFromPrimary, awaitReply, bindService, bumpLastSeen, decommission, deleteMessage, detectHarness, emit, ensureMarker, fail, gitWorktreeAdapter, handoffService, inbox, injectInbox, install, listAgentDefs, listAgents, listProjects, loadAgent, nextStep, paths, peek, probeMultiplexer, projectRoot, prune, realExec, realizeLaunch, register, registerProject, releaseService, resolveAgent, resolveAgentDef, resolveBody, resolvePrimaryRoot, resolveProject, resolveProjectLocalRoot, resolveRecipient, resolveRoot, resolveSelfId, resolveService, selectSessionAdapter, send, serviceEndpointId, spawn, spawnAndWake, startService, toonList, toonObject, touch, verifyOwnership, watchMail, withOwnership };