pi-legion 3.0.3 → 4.0.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 (67) hide show
  1. package/CHANGELOG.md +263 -2
  2. package/CONTRIBUTING.md +4 -17
  3. package/README.md +29 -22
  4. package/agents/worker.md +1 -1
  5. package/docs/contracts/display-liveness-v1.md +373 -0
  6. package/docs/contracts/drop-non-pi-kinds-v4.md +198 -0
  7. package/docs/contracts/esc-abort-v1.md +4 -4
  8. package/docs/contracts/esc-no-retire-v3.md +2 -2
  9. package/docs/contracts/launch-admission-queue-v1.md +839 -0
  10. package/docs/contracts/status-panel-v1.md +174 -0
  11. package/docs/inprocess-supervisor.md +37 -4
  12. package/docs/legion-v2-contract.md +18 -2
  13. package/docs/performance-2026-10-19.md +199 -0
  14. package/docs/spawn-mode-visibility-v1.md +206 -0
  15. package/docs/vexillatio-contract.md +3 -1
  16. package/index.ts +633 -84
  17. package/package.json +4 -2
  18. package/skills/pi-legion/SKILL.md +2 -3
  19. package/src/agents/agents.ts +17 -10
  20. package/src/agents/overrides.ts +0 -1
  21. package/src/agents/presets.ts +14 -40
  22. package/src/agents/settings.ts +26 -77
  23. package/src/agents/step-model.ts +7 -22
  24. package/src/api.ts +1 -35
  25. package/src/extension/event-bus.ts +80 -8
  26. package/src/extension/legion-commands.ts +43 -17
  27. package/src/extension/legion-doctor.ts +1 -1
  28. package/src/extension/legion-stop.ts +39 -13
  29. package/src/extension/legion-tools.ts +21 -4
  30. package/src/extension/liveness.ts +200 -0
  31. package/src/extension/notify.ts +6 -6
  32. package/src/extension/playbook.ts +6 -6
  33. package/src/extension/recycle.ts +6 -2
  34. package/src/extension/runtime.ts +422 -18
  35. package/src/extension/slash.ts +0 -21
  36. package/src/extension/spawn-render.ts +183 -0
  37. package/src/extension/summary.ts +1 -1
  38. package/src/legion/db.ts +165 -1
  39. package/src/legion/events.ts +81 -16
  40. package/src/legion/nodes.ts +13 -2
  41. package/src/legion/rebuild.ts +7 -1
  42. package/src/legion/usage.ts +58 -4
  43. package/src/runs/orchestrator.ts +1165 -62
  44. package/src/runs/worktree.ts +190 -21
  45. package/src/shared/ignored-kind.ts +57 -0
  46. package/src/shared/progress.ts +11 -0
  47. package/src/shared/session.ts +199 -19
  48. package/src/shared/types.ts +50 -66
  49. package/src/supervisor/admission-queue.ts +516 -0
  50. package/src/supervisor/inprocess-supervisor.ts +108 -33
  51. package/src/supervisor/proc-memory.ts +67 -0
  52. package/src/supervisor/routing-supervisor.ts +58 -1
  53. package/src/tui/live-overlay.ts +14 -0
  54. package/src/tui/session-messages-file.ts +100 -0
  55. package/src/tui/status.ts +103 -21
  56. package/src/tui/tree-view.ts +36 -3
  57. package/src/tui/viz/layout.ts +54 -3
  58. package/src/tui/viz/model.ts +67 -14
  59. package/src/tui/viz/palette.ts +11 -0
  60. package/src/tui/viz/panels.ts +222 -8
  61. package/src/tui/viz/railway.ts +28 -25
  62. package/src/tui/viz/view.ts +110 -23
  63. package/src/backends/legacy/client.ts +0 -638
  64. package/src/backends/legacy/cursor-chat.ts +0 -348
  65. package/src/backends/legacy/runner.ts +0 -101
  66. package/src/backends/legacy/types.ts +0 -138
  67. package/src/runs/kind.ts +0 -260
package/src/runs/kind.ts DELETED
@@ -1,260 +0,0 @@
1
- /**
2
- * Kind-specific fill-ins for a Herdr-only control plane.
3
- *
4
- * Every child, including pi, is driven the same way:
5
- * herdr agent start --kind K --pane P -- <native argv>
6
- * herdr agent prompt <name> <task>
7
- * herdr agent wait <name>
8
- * herdr pane read / session jsonl (result parser, not a second pipeline)
9
- *
10
- * This module only decides the native argv after `--`, how to spell `--model`,
11
- * and the prompt text. It does not own wait, notify, or recycle.
12
- */
13
-
14
- import {
15
- type AgentConfig,
16
- type AgentKind,
17
- isThinkingLevel,
18
- } from "../shared/types.ts";
19
- import { splitThinkingSuffix } from "../agents/model-scope.ts";
20
- import { formatChildTask } from "../extension/child-guard.ts";
21
- import {
22
- applyThinkingSuffix,
23
- type BuildArgsInput,
24
- buildPiArgs,
25
- } from "./args.ts";
26
-
27
- const CURSOR_EFFORTS = ["low", "medium", "high", "xhigh"] as const;
28
- type CursorEffort = (typeof CURSOR_EFFORTS)[number];
29
-
30
- const PI_SHAPED_MODEL =
31
- /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+(?::[A-Za-z0-9._-]+)?$/;
32
-
33
- export interface KindStartPlan {
34
- /** Native CLI argv after `herdr agent start --`. Never includes the task. */
35
- args: string[];
36
- tempFiles: string[];
37
- /** Always delivered with `herdr agent prompt` after start is ready. */
38
- taskText: string;
39
- /** `--model` value actually sent. */
40
- nativeModel?: string;
41
- /** Stored on ChildRecord; pi keeps the unsuffixed candidate. */
42
- recordModel?: string;
43
- }
44
-
45
- export function planKindStart(input: BuildArgsInput): KindStartPlan {
46
- const nativeModel = nativeModelFor(
47
- input.agent.kind,
48
- input.model,
49
- input.thinking,
50
- );
51
- const taskText = composePrompt(input);
52
- if (input.agent.kind === "pi") {
53
- const built = buildPiArgs({ ...input, includeTask: false });
54
- return {
55
- args: built.args,
56
- tempFiles: built.tempFiles,
57
- taskText,
58
- ...(input.model ? { recordModel: input.model } : {}),
59
- ...(nativeModel ? { nativeModel } : {}),
60
- };
61
- }
62
- return {
63
- args: nativeStartArgs(input.agent, nativeModel),
64
- tempFiles: [],
65
- taskText,
66
- ...(nativeModel ? { nativeModel, recordModel: nativeModel } : {}),
67
- };
68
- }
69
-
70
- /** Map a resolved model id onto the flag the target CLI actually accepts. */
71
- export function nativeModelFor(
72
- kind: AgentKind,
73
- model: string | undefined,
74
- thinking?: string | false,
75
- ): string | undefined {
76
- if (kind === "pi") return applyThinkingSuffix(model, thinking);
77
- const raw = model?.trim();
78
- if (!raw) return undefined;
79
- if (kind === "cursor") return cursorModel(raw, thinking);
80
- if (isPiShapedModel(raw)) return undefined;
81
- return raw;
82
- }
83
-
84
- export function isPiShapedModel(model: string): boolean {
85
- return PI_SHAPED_MODEL.test(model.trim());
86
- }
87
-
88
- function nativeStartArgs(
89
- agent: AgentConfig,
90
- nativeModel: string | undefined,
91
- ): string[] {
92
- const args: string[] = [];
93
- if (nativeModel) args.push("--model", nativeModel);
94
- if (agent.kind === "cursor") {
95
- // `--force` skips command approval. Workspace trust is a different
96
- // dialog; without `--trust` the TUI sits on "Workspace Trust Required"
97
- // and `herdr agent prompt` never starts a turn.
98
- args.push("--trust");
99
- // Unattended by default: a subagent has no human at its pane, so cursor's
100
- // allowlist approval ("Run this command? Not in allowlist: …") is a
101
- // deadlock, not a safety feature — the child waits forever for a `y`
102
- // nobody will type. `--force` (Run Everything) is therefore the default;
103
- // `onBlocked: "forward"`/`"notify"` opts back into a human gate by
104
- // dropping the flag, and herdr then surfaces the block to the parent.
105
- // Deny-list entries (`~/.cursor/cli-config.json` permissions.deny) still
106
- // apply: `--force` means "unless explicitly denied", not "no rules".
107
- if (agent.onBlocked !== "forward" && agent.onBlocked !== "notify") {
108
- args.push("--force");
109
- }
110
- }
111
- return args;
112
- }
113
-
114
- function composePrompt(input: BuildArgsInput): string {
115
- const task = formatChildTask(input.task, {
116
- allowNested: input.allowNestedSubagents === true,
117
- ...(input.worktreeBranch ? { worktreeBranch: input.worktreeBranch } : {}),
118
- });
119
- // Pi gets the system prompt (and settings-injected child context) via
120
- // `--system-prompt` on start. Other CLIs have no equivalent flag, so both
121
- // ride along in the Herdr prompt, role prompt first.
122
- if (input.agent.kind === "pi") return task;
123
- const parts = [input.agent.systemPrompt?.trim(), input.childContext?.trim()]
124
- .filter(Boolean)
125
- .join("\n\n");
126
- return parts ? `${parts}\n\n${task}` : task;
127
- }
128
-
129
- /**
130
- * Cursor CLI slugs look like `cursor-grok-4.6-high`, not `provider/id:thinking`.
131
- * A parent pi model (`cb/glm-5.3`) is dropped rather than forwarded.
132
- *
133
- * Grok slug schema, measured against `cursor-agent --list-models` (2026-09-27):
134
- * grok-4.5 / 4.6 → `cursor-grok-<v>-<effort>(-fast)?` (prefixed)
135
- * grok-4.7+ → `grok-<v>-<effort>(-fast)?` (NO prefix)
136
- * SDK parameter schemas (Cursor.models.list(), ~/.pi/agent/cursor-sdk-model-list.json):
137
- * grok-4.5 / 4.6 → effort, fast
138
- * grok-4.7 → context(256k|500k), reasoning_effort, fast
139
- */
140
-
141
- /** `grok-4.6` → 406, `grok-4.7` → 407, `grok-4.10` → 410. Dot-decimal would misorder 4.10 < 4.7. */
142
- function grokVersion(id: string): number | undefined {
143
- const m = id.match(/^grok-(\d+)\.(\d+)$/);
144
- if (!m || m[1] === undefined || m[2] === undefined) return undefined;
145
- return Number(m[1]) * 100 + Number(m[2]);
146
- }
147
-
148
- /** Highest grok whose CLI slug still carries the `cursor-` prefix (measured: 4.6). */
149
- const GROK_LAST_PREFIXED = 406;
150
-
151
- /** grok ≤4.6 slugs carry the `cursor-` prefix; 4.7+ dropped it. */
152
- function grokSlugPrefix(version: number): string {
153
- return version <= GROK_LAST_PREFIXED ? "cursor-grok" : "grok";
154
- }
155
-
156
- /**
157
- * Expand a pi-cursor-sdk style context alias — `grok-4.7@500k`,
158
- * `cursor/grok-4.7@500k` — into the bracket form the CLI accepts:
159
- * `grok-4.7[context=500k,reasoning_effort=xhigh,fast=false]`.
160
- *
161
- * Measured (2026-09-27): the CLI rejects PARTIAL bracket lists, so every SDK
162
- * param of the model must be present — `fast` gets an explicit `false`.
163
- *
164
- * Only the grok-4.7 schema is known well enough to expand (context,
165
- * reasoning_effort, fast — verified against Cursor.models.list()); any other
166
- * @alias (claude/gpt/... use different effort param names, and future grok
167
- * versions may change schema) cannot be safely rewritten, and the CLI has no
168
- * `@` syntax of its own — so those return undefined, which the orchestrator
169
- * treats as an unexpressible explicit model and refuses BEFORE any pane is
170
- * created, instead of letting the child crash on start.
171
- */
172
- function expandCursorSdkAlias(
173
- compact: string,
174
- effort: CursorEffort,
175
- ): string | undefined {
176
- const alias = compact.match(/^(?:cursor\/)?(.+?)@(\d+[km])$/);
177
- if (!alias || alias[1] === undefined || alias[2] === undefined) return undefined;
178
- const base = alias[1];
179
- const context = alias[2].toLowerCase();
180
- // Only the grok-4.7 schema is verified; 4.5/4.6 expose no context param,
181
- // and anything newer may have drifted.
182
- if (grokVersion(base) !== 407) return undefined;
183
- return `${base}[context=${context},reasoning_effort=${effort},fast=false]`;
184
- }
185
-
186
- export function cursorModel(
187
- model: string,
188
- thinking?: string | false,
189
- ): string | undefined {
190
- // A pi-style `:level` suffix (`grok-4.7@500k:xhigh`) is the embedded form
191
- // of `thinking`; the suffix wins, mirroring pi's applyThinkingSuffix
192
- // precedence for provider/id:level ids.
193
- const split = splitThinkingSuffix(model.trim());
194
- const effectiveThinking = split.thinking ?? thinking;
195
- const compact = split.baseModel.trim().replace(/\s+/g, "-");
196
- if (!compact) return undefined;
197
- if (isPiShapedModel(compact)) return undefined;
198
- if (compact.includes("[")) return compact;
199
-
200
- const effort = cursorEffort(effectiveThinking);
201
- const lower = compact.toLowerCase();
202
-
203
- // Cursor ships two Autos: legacy `auto`/`default` (bundled Auto pricing)
204
- // and Router `auto-smart` (Balance/Intelligence bill the routed model).
205
- // Do not collapse `auto` into Auto Balance — cheap search agents want the
206
- // legacy slug, and billing lists them as distinct line items.
207
- if (lower === "auto" || lower === "default") return "auto";
208
-
209
- if (
210
- lower === "auto-smart" ||
211
- lower === "auto-balance" ||
212
- lower === "autobalance"
213
- ) {
214
- return "auto-smart[optimize_for=balanced]";
215
- }
216
-
217
- // pi-cursor-sdk context alias: preserve the context variant via the
218
- // bracket form instead of degrading to the default-context slug. An alias
219
- // whose schema we cannot expand returns undefined — the orchestrator
220
- // refuses an explicit model before creating a pane (better than a child
221
- // that crashes on start with an id the CLI cannot parse).
222
- if (compact.includes("@")) {
223
- return expandCursorSdkAlias(lower, effort ?? "high");
224
- }
225
-
226
- const bareGrok = lower.match(/^(?:cursor-)?grok-(\d+\.\d+)$/);
227
- if (bareGrok && bareGrok[1] !== undefined) {
228
- const version = grokVersion(`grok-${bareGrok[1]}`) ?? 0;
229
- return `${grokSlugPrefix(version)}-${bareGrok[1]}-${effort ?? "high"}`;
230
- }
231
-
232
- // An already-slugged grok, with or without the `cursor-` prefix: rebuild
233
- // the prefix per version so a pre-fix `cursor-grok-4.7-xhigh` is repaired,
234
- // and swap the effort when thinking was given.
235
- const slugged = lower.match(
236
- /^(?:cursor-)?grok-(\d+\.\d+)-(low|medium|high|xhigh)(-fast)?$/,
237
- );
238
- if (slugged && slugged[1] !== undefined && slugged[2] !== undefined) {
239
- const version = grokVersion(`grok-${slugged[1]}`) ?? 0;
240
- const keep = effort ?? slugged[2];
241
- return `${grokSlugPrefix(version)}-${slugged[1]}-${keep}${slugged[3] ?? ""}`;
242
- }
243
-
244
- // The `:level` split above only serves the grok/alias branches. Any other
245
- // bare slug carrying one (`composer-1.5:high`) is not ours to rewrite —
246
- // hand the ORIGINAL string to the CLI rather than silently dropping the
247
- // suffix.
248
- return split.thinking !== undefined ? model.trim() : compact;
249
- }
250
-
251
- function cursorEffort(thinking?: string | false): CursorEffort | undefined {
252
- if (thinking === undefined) return undefined;
253
- if (thinking === false || thinking === "off") return "low";
254
- if (thinking === "minimal") return "low";
255
- if (thinking === "max") return "xhigh";
256
- if ((CURSOR_EFFORTS as readonly string[]).includes(thinking)) {
257
- return thinking as CursorEffort;
258
- }
259
- return isThinkingLevel(thinking) ? "high" : undefined;
260
- }