pi-legion 0.16.0 → 2.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.
- package/CONTRIBUTING.md +58 -0
- package/README.md +313 -640
- package/agents/advisor.md +7 -5
- package/agents/centurion.md +53 -0
- package/agents/designer.md +33 -33
- package/agents/planner.md +1 -2
- package/agents/prototype.md +2 -2
- package/agents/reviewer.md +67 -31
- package/agents/scout.md +6 -2
- package/agents/worker.md +12 -3
- package/docs/contracts/esc-abort-v1.md +131 -0
- package/docs/contracts/esc-cascade-v2.md +100 -0
- package/docs/design.md +44 -25
- package/docs/legion-v2-contract.md +461 -0
- package/docs/legion-v2-delivery.md +62 -0
- package/docs/retire-hardening-contract.md +125 -0
- package/docs/subagents-teams.md +76 -0
- package/docs/tui-viewer-contract.md +276 -0
- package/docs/vexillatio-contract.md +162 -0
- package/index.ts +1462 -286
- package/package.json +12 -9
- package/skills/pi-legion/SKILL.md +74 -96
- package/src/agents/agents.ts +6 -11
- package/src/agents/overrides.ts +2 -1
- package/src/agents/paths.ts +1 -1
- package/src/agents/settings.ts +268 -20
- package/src/api.ts +47 -48
- package/src/{herdr → backends/legacy}/client.ts +14 -69
- package/src/{shared → backends/legacy}/cursor-chat.ts +2 -2
- package/src/{herdr → backends/legacy}/runner.ts +22 -9
- package/src/backends/legacy/types.ts +138 -0
- package/src/extension/auto-doctor.ts +144 -0
- package/src/extension/budget.ts +2 -2
- package/src/extension/child-guard.ts +214 -201
- package/src/extension/event-bus.ts +263 -0
- package/src/extension/legion-commands.ts +719 -0
- package/src/extension/legion-doctor.ts +888 -0
- package/src/extension/legion-stop.ts +150 -0
- package/src/extension/legion-tools.ts +816 -0
- package/src/extension/notify.ts +24 -19
- package/src/extension/playbook.ts +20 -89
- package/src/extension/recycle.ts +2 -7
- package/src/extension/runtime.ts +655 -105
- package/src/extension/scout-guard.ts +483 -0
- package/src/extension/slash.ts +89 -438
- package/src/extension/spawn-hints.ts +79 -0
- package/src/extension/summary.ts +17 -58
- package/src/legion/budget.ts +165 -0
- package/src/legion/db.ts +242 -0
- package/src/legion/events.ts +150 -0
- package/src/legion/mail.ts +412 -0
- package/src/legion/nodes.ts +461 -0
- package/src/legion/rebuild.ts +533 -0
- package/src/legion/state-machine.ts +313 -0
- package/src/legion/usage.ts +101 -0
- package/src/runs/acceptance.ts +82 -25
- package/src/runs/args.ts +51 -5
- package/src/runs/hygiene.ts +335 -0
- package/src/runs/orchestrator.ts +790 -1683
- package/src/runs/owner.ts +61 -0
- package/src/runs/provision.ts +135 -0
- package/src/runs/store.ts +44 -17
- package/src/runs/verify-queue.ts +195 -0
- package/src/shared/name.ts +29 -0
- package/src/shared/output-cap.ts +57 -0
- package/src/shared/progress.ts +57 -135
- package/src/shared/session.ts +243 -171
- package/src/shared/types.ts +107 -208
- package/src/supervisor/child-memory.ts +140 -0
- package/src/supervisor/proc-tree.ts +103 -0
- package/src/supervisor/rpc-supervisor.ts +1033 -0
- package/src/supervisor/types.ts +153 -0
- package/src/supervisor/ui-proxy.ts +226 -0
- package/src/tui/chat-items.ts +330 -0
- package/src/tui/live-overlay.ts +874 -0
- package/src/tui/status.ts +553 -27
- package/src/tui/tree-view.ts +808 -0
- package/src/tui/viz/canvas.ts +119 -0
- package/src/tui/viz/layout.ts +111 -0
- package/src/tui/viz/model.ts +123 -0
- package/src/tui/viz/palette.ts +188 -0
- package/src/tui/viz/panels.ts +530 -0
- package/src/tui/viz/railway.ts +313 -0
- package/src/tui/viz/rate.ts +211 -0
- package/src/tui/viz/view.ts +228 -0
- package/src/extension/parent-label.ts +0 -115
- package/src/profiles/classify.ts +0 -375
- package/src/profiles/profiles.ts +0 -730
- package/src/runs/layout.ts +0 -258
package/src/shared/types.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - Additive changes (new optional fields) are allowed.
|
|
7
7
|
* - Breaking changes require coordinating all modules.
|
|
8
8
|
*
|
|
9
|
-
* Design authority:
|
|
9
|
+
* Design authority: docs/legion-v2-contract.md.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
// =============================================================================
|
|
@@ -17,7 +17,7 @@ export type AgentScope = "user" | "project" | "both";
|
|
|
17
17
|
|
|
18
18
|
export type SystemPromptMode = "replace" | "append";
|
|
19
19
|
|
|
20
|
-
/**
|
|
20
|
+
/** Agent kinds parsed from role files; the RPC supervisor accepts only `pi`. */
|
|
21
21
|
export const AGENT_KINDS = [
|
|
22
22
|
"pi",
|
|
23
23
|
"claude",
|
|
@@ -60,9 +60,6 @@ export type AgentKind = (typeof AGENT_KINDS)[number];
|
|
|
60
60
|
*/
|
|
61
61
|
export type ModelOrigin = "explicit" | "inherited";
|
|
62
62
|
|
|
63
|
-
/** Where a subagent's pane is placed (design §8.3). */
|
|
64
|
-
export type Placement = "split-down" | "split-right" | "new-tab";
|
|
65
|
-
|
|
66
63
|
/** What to do when an agent reports `blocked` (design §5.3). */
|
|
67
64
|
export type OnBlockedPolicy = "forward" | "auto-approve" | "notify";
|
|
68
65
|
|
|
@@ -123,6 +120,12 @@ export interface AgentConfig {
|
|
|
123
120
|
systemPromptMode: SystemPromptMode;
|
|
124
121
|
inheritProjectContext: boolean;
|
|
125
122
|
inheritSkills: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Load the host's discovered extensions (settings `packages`, project
|
|
125
|
+
* extensions) in the child. Default false: children get `--no-extensions`
|
|
126
|
+
* plus only explicit `--extension` paths (#43).
|
|
127
|
+
*/
|
|
128
|
+
inheritExtensions?: boolean;
|
|
126
129
|
|
|
127
130
|
// ── behavior ──
|
|
128
131
|
output?: string;
|
|
@@ -135,15 +138,15 @@ export interface AgentConfig {
|
|
|
135
138
|
completionGuard?: boolean;
|
|
136
139
|
maxSubagentDepth?: number;
|
|
137
140
|
allowNestedSubagents?: boolean;
|
|
141
|
+
utility?: boolean;
|
|
138
142
|
toolBudget?: ToolBudgetConfig;
|
|
139
143
|
turnBudget?: TurnBudgetConfig;
|
|
140
144
|
disabled?: boolean;
|
|
141
145
|
alias?: string[];
|
|
142
146
|
extraFields?: Record<string, string>;
|
|
143
147
|
|
|
144
|
-
// ──
|
|
148
|
+
// ── execution metadata ──
|
|
145
149
|
kind: AgentKind;
|
|
146
|
-
placement?: Placement;
|
|
147
150
|
worktree?: boolean;
|
|
148
151
|
steer?: boolean;
|
|
149
152
|
onBlocked?: OnBlockedPolicy;
|
|
@@ -283,16 +286,12 @@ export interface NestedPathEntry {
|
|
|
283
286
|
export const MAX_NESTED_PATH_ENTRIES = 4;
|
|
284
287
|
|
|
285
288
|
export interface ChildRecord {
|
|
286
|
-
/**
|
|
289
|
+
/** Supervisor-local child name; unique among live children in this run. */
|
|
287
290
|
name: string;
|
|
288
|
-
/** Pane id, or null once recycled (F12: pane is not the durable carrier). */
|
|
289
|
-
paneId: string | null;
|
|
290
|
-
/** Owning tab when the child got its own tab (placement: "new-tab"). */
|
|
291
|
-
tabId?: string;
|
|
292
291
|
/** ★ resume credential — must persist. */
|
|
293
292
|
sessionFile: string;
|
|
294
293
|
sessionId?: string;
|
|
295
|
-
/**
|
|
294
|
+
/** Stable per-run child identity; legacy records used a pane owner token. */
|
|
296
295
|
ownerToken: string;
|
|
297
296
|
|
|
298
297
|
// lifecycle
|
|
@@ -300,7 +299,7 @@ export interface ChildRecord {
|
|
|
300
299
|
spawnedAt: string;
|
|
301
300
|
retiredAt?: string;
|
|
302
301
|
|
|
303
|
-
// outcome (snapshot taken
|
|
302
|
+
// outcome (snapshot taken before child retirement — F27)
|
|
304
303
|
execution?: Execution;
|
|
305
304
|
acceptance?: AcceptanceResult;
|
|
306
305
|
/**
|
|
@@ -321,7 +320,7 @@ export interface ChildRecord {
|
|
|
321
320
|
onBlocked?: OnBlockedPolicy;
|
|
322
321
|
/** Snapshotted at launch; missing `{"ok":…}` on success becomes rejected. */
|
|
323
322
|
completionGuard?: boolean;
|
|
324
|
-
/** Isolated git worktree the child
|
|
323
|
+
/** Isolated git worktree used by the child; left on disk after retire. */
|
|
325
324
|
worktreePath?: string;
|
|
326
325
|
/** Branch created in that worktree; the child ships via MR, not the parent checkout. */
|
|
327
326
|
worktreeBranch?: string;
|
|
@@ -335,23 +334,8 @@ export interface ChildRecord {
|
|
|
335
334
|
/** Model actually started with (resolved + fallback). */
|
|
336
335
|
model?: string;
|
|
337
336
|
thinking?: string | false;
|
|
338
|
-
/**
|
|
339
|
-
* Models that were requested but could not be handed to the target CLI's
|
|
340
|
-
* `--model` (`nativeModelFor` returns undefined for them), so the child runs
|
|
341
|
-
* on the CLI's own default instead.
|
|
342
|
-
*
|
|
343
|
-
* Only reachable for an INHERITED model — an explicitly chosen one is
|
|
344
|
-
* refused before launch (see `Orchestrator.launch`'s `modelOrigin`). This is
|
|
345
|
-
* the intended case: a `kind: cursor` role with no model of its own
|
|
346
|
-
* inherits the parent's pi-shaped model, which must not be forwarded.
|
|
347
|
-
*/
|
|
337
|
+
/** Models that were requested but cannot be represented by the RPC child. */
|
|
348
338
|
modelDropped?: string[];
|
|
349
|
-
/**
|
|
350
|
-
* Exact text sent with `herdr agent prompt`. Non-pi collect reads the pane,
|
|
351
|
-
* which still contains this echo; strip it before extracting a verdict so a
|
|
352
|
-
* template `{"ok": true}` in the system prompt cannot attest success.
|
|
353
|
-
*/
|
|
354
|
-
promptText?: string;
|
|
355
339
|
}
|
|
356
340
|
|
|
357
341
|
export interface RunRecord {
|
|
@@ -361,15 +345,10 @@ export interface RunRecord {
|
|
|
361
345
|
cwd: string;
|
|
362
346
|
|
|
363
347
|
herdr: {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
* Parent Pi's herdr pane (`HERDR_PANE_ID`). Scopes type-tab labels and
|
|
369
|
-
* control-action lookup so two parent sessions in the same repo/Space
|
|
370
|
-
* cannot steer or recycle each other's children.
|
|
371
|
-
*/
|
|
372
|
-
parentPaneId?: string;
|
|
348
|
+
/** Current transport marker under the historic run.json key. */
|
|
349
|
+
supervisor?: "rpc";
|
|
350
|
+
/** Reserved for M2 tree identity. */
|
|
351
|
+
node_id?: string;
|
|
373
352
|
};
|
|
374
353
|
|
|
375
354
|
/** Lineage: root → this node. */
|
|
@@ -385,159 +364,6 @@ export interface RunRecord {
|
|
|
385
364
|
updatedAt: string;
|
|
386
365
|
}
|
|
387
366
|
|
|
388
|
-
// =============================================================================
|
|
389
|
-
// herdr client (design §9) — thin, testable wrapper over the CLI
|
|
390
|
-
// =============================================================================
|
|
391
|
-
|
|
392
|
-
export interface HerdrError {
|
|
393
|
-
code: string;
|
|
394
|
-
message: string;
|
|
395
|
-
details?: unknown;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
export type HerdrResult<T> =
|
|
399
|
-
| { ok: true; value: T }
|
|
400
|
-
| { ok: false; error: HerdrError };
|
|
401
|
-
|
|
402
|
-
export interface PaneInfo {
|
|
403
|
-
pane_id: string;
|
|
404
|
-
tab_id: string;
|
|
405
|
-
workspace_id: string;
|
|
406
|
-
agent_status?: string;
|
|
407
|
-
cwd?: string | null;
|
|
408
|
-
terminal_title_stripped?: string;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
export interface AgentInfo {
|
|
412
|
-
name?: string | null;
|
|
413
|
-
pane_id: string;
|
|
414
|
-
tab_id: string;
|
|
415
|
-
workspace_id: string;
|
|
416
|
-
agent: string | null;
|
|
417
|
-
agent_status: string;
|
|
418
|
-
cwd?: string | null;
|
|
419
|
-
agent_session?: { kind: string; source: string; value: string } | null;
|
|
420
|
-
state_labels?: Record<string, string>;
|
|
421
|
-
tokens?: unknown;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
export interface TabInfo {
|
|
425
|
-
tab_id: string;
|
|
426
|
-
workspace_id: string;
|
|
427
|
-
label?: string | null;
|
|
428
|
-
pane_count: number;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
export interface AgentStartResult {
|
|
432
|
-
name: string;
|
|
433
|
-
paneId: string;
|
|
434
|
-
argv: string[];
|
|
435
|
-
/** Session path reported by herdr (F1) — absent for non-pi kinds (F7). */
|
|
436
|
-
sessionPath?: string;
|
|
437
|
-
agentStatus: string;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
export interface ProcessInfo {
|
|
441
|
-
foregroundProcesses: Array<{
|
|
442
|
-
argv: string[];
|
|
443
|
-
cmdline: string;
|
|
444
|
-
pid: number;
|
|
445
|
-
name: string;
|
|
446
|
-
cwd?: string;
|
|
447
|
-
}>;
|
|
448
|
-
shellPid?: number;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/** Injectable runner so the client is unit-testable without a live herdr. */
|
|
452
|
-
export type CommandRunner = (
|
|
453
|
-
args: string[],
|
|
454
|
-
opts?: { timeoutMs?: number; env?: Record<string, string | undefined> },
|
|
455
|
-
) => Promise<{ stdout: string; stderr: string; code: number }>;
|
|
456
|
-
|
|
457
|
-
export interface HerdrClient {
|
|
458
|
-
// ── panes ──
|
|
459
|
-
paneSplit(opts: {
|
|
460
|
-
target?: string;
|
|
461
|
-
current?: boolean;
|
|
462
|
-
direction: "right" | "down";
|
|
463
|
-
cwd?: string;
|
|
464
|
-
env?: Record<string, string>;
|
|
465
|
-
focus?: boolean;
|
|
466
|
-
}): Promise<HerdrResult<PaneInfo>>;
|
|
467
|
-
paneClose(paneId: string): Promise<HerdrResult<void>>;
|
|
468
|
-
paneRead(
|
|
469
|
-
paneId: string,
|
|
470
|
-
opts?: { source?: ReadSource; lines?: number },
|
|
471
|
-
): Promise<HerdrResult<string>>;
|
|
472
|
-
paneList(): Promise<HerdrResult<PaneInfo[]>>;
|
|
473
|
-
paneGet(paneId: string): Promise<HerdrResult<PaneInfo>>;
|
|
474
|
-
paneProcessInfo(paneId: string): Promise<HerdrResult<ProcessInfo>>;
|
|
475
|
-
paneReportMetadata(opts: {
|
|
476
|
-
paneId: string;
|
|
477
|
-
source: string;
|
|
478
|
-
displayAgent?: string;
|
|
479
|
-
title?: string;
|
|
480
|
-
tokens?: Record<string, string>;
|
|
481
|
-
/** `idle=waiting 2 subagents` — labels a herdr agent_status from this source. */
|
|
482
|
-
stateLabel?: { status: string; text: string };
|
|
483
|
-
/** Auto-expiry so a crashed reporter leaves no stale label. */
|
|
484
|
-
ttlMs?: number;
|
|
485
|
-
/** Drop all state-labels set by this source. */
|
|
486
|
-
clearStateLabels?: boolean;
|
|
487
|
-
}): Promise<HerdrResult<void>>;
|
|
488
|
-
|
|
489
|
-
// ── tabs ──
|
|
490
|
-
tabCreate(opts: {
|
|
491
|
-
cwd?: string;
|
|
492
|
-
label?: string;
|
|
493
|
-
focus?: boolean;
|
|
494
|
-
/** Environment for the tab's root pane process (lineage propagation). */
|
|
495
|
-
env?: Record<string, string>;
|
|
496
|
-
/** Pin the new tab to this Space (`herdr tab create --workspace`). */
|
|
497
|
-
workspaceId?: string;
|
|
498
|
-
}): Promise<HerdrResult<{ tab: TabInfo; rootPaneId: string }>>;
|
|
499
|
-
tabClose(tabId: string): Promise<HerdrResult<void>>;
|
|
500
|
-
tabRename(tabId: string, label: string): Promise<HerdrResult<void>>;
|
|
501
|
-
tabList(workspaceId?: string): Promise<HerdrResult<TabInfo[]>>;
|
|
502
|
-
|
|
503
|
-
// ── agents ──
|
|
504
|
-
agentStart(opts: {
|
|
505
|
-
name: string;
|
|
506
|
-
kind: AgentKind;
|
|
507
|
-
paneId: string;
|
|
508
|
-
args?: string[];
|
|
509
|
-
timeoutMs?: number;
|
|
510
|
-
}): Promise<HerdrResult<AgentStartResult>>;
|
|
511
|
-
agentPrompt(
|
|
512
|
-
target: string,
|
|
513
|
-
text: string,
|
|
514
|
-
opts?: { wait?: boolean; timeoutMs?: number },
|
|
515
|
-
): Promise<HerdrResult<AgentInfo>>;
|
|
516
|
-
agentGet(target: string): Promise<HerdrResult<AgentInfo>>;
|
|
517
|
-
agentList(): Promise<HerdrResult<AgentInfo[]>>;
|
|
518
|
-
agentSendKeys(target: string, ...keys: string[]): Promise<HerdrResult<void>>;
|
|
519
|
-
agentWait(
|
|
520
|
-
target: string,
|
|
521
|
-
opts?: { until?: string[]; timeoutMs?: number },
|
|
522
|
-
): Promise<HerdrResult<void>>;
|
|
523
|
-
|
|
524
|
-
// ── meta ──
|
|
525
|
-
version(): Promise<HerdrResult<string>>;
|
|
526
|
-
available(): Promise<boolean>;
|
|
527
|
-
|
|
528
|
-
// ── integrations ──
|
|
529
|
-
/** One integration's status, or null when herdr cannot report it. */
|
|
530
|
-
integrationStatus(target: string): Promise<HerdrResult<string | null>>;
|
|
531
|
-
/** Install an integration hook (idempotent on reinstall). */
|
|
532
|
-
integrationInstall(target: string): Promise<HerdrResult<string>>;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
export type ReadSource =
|
|
536
|
-
| "visible"
|
|
537
|
-
| "recent"
|
|
538
|
-
| "recent-unwrapped"
|
|
539
|
-
| "detection";
|
|
540
|
-
|
|
541
367
|
// =============================================================================
|
|
542
368
|
// Session parsing (design §10)
|
|
543
369
|
// =============================================================================
|
|
@@ -566,10 +392,21 @@ export interface ParsedSession {
|
|
|
566
392
|
/** True when the last user prompt has no assistant reply (F29 abort signal). */
|
|
567
393
|
lastTurnMissing: boolean;
|
|
568
394
|
tornLines: number;
|
|
395
|
+
/**
|
|
396
|
+
* Cumulative model-generation wall time in ms: each completed assistant
|
|
397
|
+
* message timestamp minus the preceding user or toolResult timestamp when
|
|
398
|
+
* both are known. Tool/idle gaps are excluded. Approximate — session JSONL
|
|
399
|
+
* records finished assistant messages, not mid-stream token boundaries.
|
|
400
|
+
*/
|
|
401
|
+
generationMs: number;
|
|
569
402
|
}
|
|
570
403
|
|
|
571
404
|
export interface TurnRecord {
|
|
572
405
|
userText: string;
|
|
406
|
+
/** Unix ms timestamp of the user message that opened this turn, when written. */
|
|
407
|
+
startedAt?: number;
|
|
408
|
+
/** Unix ms timestamp of the last assistant message in this turn, when written. */
|
|
409
|
+
endedAt?: number;
|
|
573
410
|
assistants: Array<{
|
|
574
411
|
stopReason: StopReason | null;
|
|
575
412
|
/**
|
|
@@ -606,7 +443,6 @@ export interface TurnRecord {
|
|
|
606
443
|
*/
|
|
607
444
|
export interface Handle {
|
|
608
445
|
name: string;
|
|
609
|
-
paneId: string | null;
|
|
610
446
|
sessionFile: string;
|
|
611
447
|
runId: string;
|
|
612
448
|
agent: string;
|
|
@@ -637,15 +473,64 @@ export interface PresetConfig {
|
|
|
637
473
|
thinking?: string | false;
|
|
638
474
|
}
|
|
639
475
|
|
|
476
|
+
/** @deprecated Transitional herdr.* settings retained until v3. */
|
|
640
477
|
export interface HerdrSettings {
|
|
641
|
-
|
|
478
|
+
/** @deprecated Parsed for transition compatibility; ignored by RPC v2. */
|
|
479
|
+
defaultPlacement?: "split-down" | "split-right" | "new-tab";
|
|
480
|
+
/** @deprecated Legacy concurrency hint; ignored by RPC v2. */
|
|
642
481
|
maxConcurrentAgents?: number;
|
|
482
|
+
/** @deprecated Legacy Herdr launch retry setting. */
|
|
643
483
|
startRetries?: number;
|
|
484
|
+
/** @deprecated Legacy Herdr launch retry setting. */
|
|
644
485
|
startRetryBackoffMs?: number;
|
|
486
|
+
/** @deprecated Legacy session retention setting. */
|
|
645
487
|
sessionRetentionDays?: number;
|
|
488
|
+
/** @deprecated Legacy session retention setting. */
|
|
646
489
|
sessionRetentionMaxBytesPerRun?: number;
|
|
647
490
|
}
|
|
648
491
|
|
|
492
|
+
export interface LegionPhaseTimeouts {
|
|
493
|
+
planning?: number;
|
|
494
|
+
implementing?: number;
|
|
495
|
+
reviewing?: number;
|
|
496
|
+
verifying?: number;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** Settings for the v2 legion tree, incrementally consumed by its milestones. */
|
|
500
|
+
export interface LegionSettings {
|
|
501
|
+
maxDepth?: number;
|
|
502
|
+
maxChildrenPerNode?: number;
|
|
503
|
+
maxActiveNodes?: number;
|
|
504
|
+
phaseTimeouts?: LegionPhaseTimeouts;
|
|
505
|
+
verifyCommandTimeoutMs?: number;
|
|
506
|
+
/** Post-create provisioning for child worktrees: "npm-install" or a shell command. */
|
|
507
|
+
worktreeBootstrap?: string;
|
|
508
|
+
worktreeBootstrapTimeoutMs?: number;
|
|
509
|
+
mailRatePer5Min?: number;
|
|
510
|
+
usagePollMs?: number;
|
|
511
|
+
orphanStaleMs?: number;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** Defaults from the frozen v2 contract §11.4. */
|
|
515
|
+
export const DEFAULT_LEGION_SETTINGS = {
|
|
516
|
+
maxDepth: 4,
|
|
517
|
+
maxChildrenPerNode: 8,
|
|
518
|
+
maxActiveNodes: 30,
|
|
519
|
+
phaseTimeouts: {
|
|
520
|
+
planning: 900_000,
|
|
521
|
+
implementing: 3_600_000,
|
|
522
|
+
reviewing: 1_800_000,
|
|
523
|
+
verifying: 900_000,
|
|
524
|
+
},
|
|
525
|
+
verifyCommandTimeoutMs: 600_000,
|
|
526
|
+
// Provisioning is opt-in: no default mode, a generous install budget.
|
|
527
|
+
worktreeBootstrap: "",
|
|
528
|
+
worktreeBootstrapTimeoutMs: 600_000,
|
|
529
|
+
mailRatePer5Min: 10,
|
|
530
|
+
usagePollMs: 30_000,
|
|
531
|
+
orphanStaleMs: 90_000,
|
|
532
|
+
} as const satisfies Required<LegionSettings>;
|
|
533
|
+
|
|
649
534
|
export type AgentOverride = Partial<AgentConfig> & { disabled?: boolean };
|
|
650
535
|
|
|
651
536
|
export type TeamMember =
|
|
@@ -657,6 +542,22 @@ export interface TeamConfig {
|
|
|
657
542
|
members: TeamMember[];
|
|
658
543
|
}
|
|
659
544
|
|
|
545
|
+
/**
|
|
546
|
+
* Main-session scout guard (issue #17).
|
|
547
|
+
*
|
|
548
|
+
* After `threshold` consecutive read-like tool calls with no subagent or
|
|
549
|
+
* vexillatio dispatch, the parent session gets one follow-up reminder and
|
|
550
|
+
* then waits `cooldownMs`. Defaults: enabled, threshold 8, cooldown 120000.
|
|
551
|
+
* `/scout-guard` and `/no-scout-guard` override `enabled` for this session.
|
|
552
|
+
*/
|
|
553
|
+
export interface ScoutGuardSettings {
|
|
554
|
+
enabled?: boolean;
|
|
555
|
+
/** Positive integer. Default 8. */
|
|
556
|
+
threshold?: number;
|
|
557
|
+
/** Integer >= 0. Default 120000. */
|
|
558
|
+
cooldownMs?: number;
|
|
559
|
+
}
|
|
560
|
+
|
|
660
561
|
export interface SubagentsSettings {
|
|
661
562
|
/**
|
|
662
563
|
* Master switch (default true). `false` stops new launches, system-prompt
|
|
@@ -711,11 +612,15 @@ export interface SubagentsSettings {
|
|
|
711
612
|
disableBuiltins?: boolean;
|
|
712
613
|
disableThinking?: boolean;
|
|
713
614
|
maxSubagentSpawnsPerSession?: number;
|
|
615
|
+
/** @deprecated Transitional herdr.* keys are parsed and ignored by RPC v2. */
|
|
714
616
|
herdr?: HerdrSettings;
|
|
617
|
+
legion?: LegionSettings;
|
|
715
618
|
/** Completion-notice merge mode. Default "smart". */
|
|
716
619
|
joinMode?: "each" | "smart";
|
|
717
620
|
/** smart-mode group flush window (ms). Default 10000. */
|
|
718
621
|
joinFlushMs?: number;
|
|
622
|
+
/** Main-session recon reminder. Absent fields use scout-guard defaults. */
|
|
623
|
+
scoutGuard?: ScoutGuardSettings;
|
|
719
624
|
}
|
|
720
625
|
|
|
721
626
|
/**
|
|
@@ -766,6 +671,7 @@ export class SubagentError extends Error {
|
|
|
766
671
|
export const ErrorCodes = {
|
|
767
672
|
HERDR_UNAVAILABLE: "HERDR_UNAVAILABLE",
|
|
768
673
|
DISABLED: "DISABLED",
|
|
674
|
+
/** @deprecated Legacy Herdr client error retained for API compatibility. */
|
|
769
675
|
PANE_BUSY: "PANE_BUSY",
|
|
770
676
|
START_FAILED: "START_FAILED",
|
|
771
677
|
START_TIMEOUT: "START_TIMEOUT",
|
|
@@ -777,6 +683,7 @@ export const ErrorCodes = {
|
|
|
777
683
|
NOT_FOUND: "NOT_FOUND",
|
|
778
684
|
RETIRE_FAILED: "RETIRE_FAILED",
|
|
779
685
|
INVALID_PARAMS: "INVALID_PARAMS",
|
|
686
|
+
INSUFFICIENT_MEMORY: "INSUFFICIENT_MEMORY",
|
|
780
687
|
} as const;
|
|
781
688
|
|
|
782
689
|
// =============================================================================
|
|
@@ -784,28 +691,20 @@ export const ErrorCodes = {
|
|
|
784
691
|
// =============================================================================
|
|
785
692
|
|
|
786
693
|
export const DEFAULTS = {
|
|
787
|
-
/**
|
|
694
|
+
/** @deprecated Legacy Herdr start-retry settings retained by the archived backend. */
|
|
788
695
|
startRetries: 40,
|
|
789
696
|
startRetryBackoffMs: 150,
|
|
790
697
|
/** F8: agent wait covers a turn; generous default. */
|
|
791
698
|
turnTimeoutMs: 900_000,
|
|
792
699
|
/** jsonl quiet window before declaring a turn settled. */
|
|
793
700
|
settleQuietMs: 2_500,
|
|
701
|
+
/** @deprecated Legacy Herdr concurrency hint retained during migration. */
|
|
794
702
|
maxConcurrentAgents: 6,
|
|
703
|
+
/** @deprecated Legacy Herdr session retention setting. */
|
|
795
704
|
sessionRetentionDays: 7,
|
|
796
|
-
/**
|
|
705
|
+
/** @deprecated Legacy Herdr probe settings; retained for the archived backend. */
|
|
797
706
|
startTimeoutMs: 45_000,
|
|
798
707
|
binaryProbeTimeoutMs: 3_000,
|
|
799
|
-
/**
|
|
800
|
-
* Default wall clock for ONE herdr CLI invocation (agent get, pane list,
|
|
801
|
-
* tab create, …). Calls that need longer pass their own timeoutMs and are
|
|
802
|
-
* NOT capped by this.
|
|
803
|
-
*
|
|
804
|
-
* Measured (hw 2026-10-09): a fork storm on the host wedged the herdr CLI
|
|
805
|
-
* mid-invocation; with no cap, ONE stuck `agent get` froze the whole
|
|
806
|
-
* collect/wait chain forever — the parent's `wait`/`collect` tool calls
|
|
807
|
-
* never returned (55+ min, no toolResult). Every short control call now
|
|
808
|
-
* fails fast with a timeout error the poll loops already tolerate.
|
|
809
|
-
*/
|
|
708
|
+
/** @deprecated Legacy Herdr command timeout retained for the archived backend. */
|
|
810
709
|
commandTimeoutMs: 15_000,
|
|
811
710
|
} as const;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child process memory controls.
|
|
3
|
+
*
|
|
4
|
+
* A) V8 flags for child pi processes, delivered via NODE_OPTIONS:
|
|
5
|
+
* `--max-semi-space-size=1` (smaller young generation) and a soft
|
|
6
|
+
* `--max-old-space-size` cap. The host's original NODE_OPTIONS is carried in
|
|
7
|
+
* `PI_LEGION_HOST_NODE_OPTIONS` so the child restores it at extension load
|
|
8
|
+
* and the flags never leak into bash commands the child runs (tsc /
|
|
9
|
+
* npm test inheriting a low heap cap would crash).
|
|
10
|
+
*
|
|
11
|
+
* B) Memory gate: refuse a spawn when available memory (minus in-flight
|
|
12
|
+
* reservations) is below a floor, instead of letting the host OOM.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execFileSync } from "node:child_process";
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as os from "node:os";
|
|
18
|
+
|
|
19
|
+
export const CHILD_MAX_OLD_SPACE_ENV = "PI_LEGION_CHILD_MAX_OLD_SPACE_MB";
|
|
20
|
+
export const CHILD_SEMI_SPACE_ENV = "PI_LEGION_CHILD_SEMI_SPACE_MB";
|
|
21
|
+
export const HOST_NODE_OPTIONS_ENV = "PI_LEGION_HOST_NODE_OPTIONS";
|
|
22
|
+
export const MIN_FREE_MEM_ENV = "PI_LEGION_MIN_FREE_MEM_MB";
|
|
23
|
+
export const CHILD_RESERVE_ENV = "PI_LEGION_CHILD_RESERVE_MB";
|
|
24
|
+
|
|
25
|
+
/** Measured: an idle child is ~100 MB PSS, a long scout session peaks ~160 MB RSS. */
|
|
26
|
+
export const DEFAULT_CHILD_MAX_OLD_SPACE_MB = 1024;
|
|
27
|
+
export const DEFAULT_CHILD_SEMI_SPACE_MB = 1;
|
|
28
|
+
/** Floor of free memory that must remain after reserving the new child. */
|
|
29
|
+
export const DEFAULT_MIN_FREE_MEM_MB = 1024;
|
|
30
|
+
/** Per-child reservation for spawns that are still starting up. */
|
|
31
|
+
export const DEFAULT_CHILD_RESERVE_MB = 160;
|
|
32
|
+
|
|
33
|
+
function envMb(env: NodeJS.ProcessEnv, key: string, fallback: number): number {
|
|
34
|
+
const raw = env[key]?.trim();
|
|
35
|
+
if (!raw) return fallback;
|
|
36
|
+
const n = Number(raw);
|
|
37
|
+
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Child NODE_OPTIONS: host options + memory flags (each disabled by 0). */
|
|
41
|
+
export function childNodeOptions(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
42
|
+
const host = env.NODE_OPTIONS?.trim() ?? "";
|
|
43
|
+
const flags: string[] = [];
|
|
44
|
+
const semi = envMb(env, CHILD_SEMI_SPACE_ENV, DEFAULT_CHILD_SEMI_SPACE_MB);
|
|
45
|
+
const old = envMb(env, CHILD_MAX_OLD_SPACE_ENV, DEFAULT_CHILD_MAX_OLD_SPACE_MB);
|
|
46
|
+
if (semi > 0 && !/--max-semi-space-size/.test(host)) flags.push(`--max-semi-space-size=${semi}`);
|
|
47
|
+
if (old > 0 && !/--max-old-space-size/.test(host)) flags.push(`--max-old-space-size=${old}`);
|
|
48
|
+
const merged = [host, ...flags].filter(Boolean).join(" ");
|
|
49
|
+
return merged || undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Env entries the supervisor adds to every child pi process. */
|
|
53
|
+
export function childMemoryEnv(env: NodeJS.ProcessEnv = process.env): Record<string, string | undefined> {
|
|
54
|
+
const nodeOptions = childNodeOptions(env);
|
|
55
|
+
if (nodeOptions === (env.NODE_OPTIONS?.trim() || undefined)) return {};
|
|
56
|
+
return { NODE_OPTIONS: nodeOptions, [HOST_NODE_OPTIONS_ENV]: env.NODE_OPTIONS ?? "" };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Called once in the child at extension load: put the host's NODE_OPTIONS back
|
|
61
|
+
* so tool subprocesses (bash → tsc / npm test) do not inherit the child caps.
|
|
62
|
+
*/
|
|
63
|
+
export function restoreHostNodeOptions(env: NodeJS.ProcessEnv = process.env): void {
|
|
64
|
+
if (!(HOST_NODE_OPTIONS_ENV in env)) return;
|
|
65
|
+
const host = env[HOST_NODE_OPTIONS_ENV] ?? "";
|
|
66
|
+
if (host) env.NODE_OPTIONS = host;
|
|
67
|
+
else delete env.NODE_OPTIONS;
|
|
68
|
+
delete env[HOST_NODE_OPTIONS_ENV];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── B) memory gate ────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/** Available memory in MB: cgroup limit, /proc/meminfo, macOS vm_stat, os.freemem. */
|
|
74
|
+
export function availableMemoryMb(): number {
|
|
75
|
+
const candidates: number[] = [];
|
|
76
|
+
try {
|
|
77
|
+
const m = /MemAvailable:\s+(\d+)\s+kB/.exec(fs.readFileSync("/proc/meminfo", "utf8"));
|
|
78
|
+
if (m) candidates.push(Number(m[1]) / 1024);
|
|
79
|
+
} catch { /* not linux */ }
|
|
80
|
+
try {
|
|
81
|
+
const max = fs.readFileSync("/sys/fs/cgroup/memory.max", "utf8").trim();
|
|
82
|
+
if (max !== "max") {
|
|
83
|
+
const cur = Number(fs.readFileSync("/sys/fs/cgroup/memory.current", "utf8").trim());
|
|
84
|
+
const limit = Number(max);
|
|
85
|
+
if (Number.isFinite(limit) && Number.isFinite(cur)) candidates.push((limit - cur) / (1024 * 1024));
|
|
86
|
+
}
|
|
87
|
+
} catch { /* no cgroup v2 limit */ }
|
|
88
|
+
if (candidates.length === 0 && process.platform === "darwin") {
|
|
89
|
+
try {
|
|
90
|
+
const out = execFileSync("vm_stat", { encoding: "utf8", timeout: 2_000 });
|
|
91
|
+
const page = Number(/page size of (\d+)/.exec(out)?.[1] ?? 4096);
|
|
92
|
+
const pages = (k: string) => Number(new RegExp(`Pages ${k}:\\s+(\\d+)`).exec(out)?.[1] ?? 0);
|
|
93
|
+
candidates.push(((pages("free") + pages("inactive") + pages("speculative")) * page) / (1024 * 1024));
|
|
94
|
+
} catch { /* fall through */ }
|
|
95
|
+
}
|
|
96
|
+
if (candidates.length === 0) candidates.push(os.freemem() / (1024 * 1024));
|
|
97
|
+
return Math.floor(Math.max(0, Math.min(...candidates)));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface MemoryGateConfig {
|
|
101
|
+
minFreeMb: number;
|
|
102
|
+
reserveMb: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function resolveMemoryGateConfig(env: NodeJS.ProcessEnv = process.env): MemoryGateConfig {
|
|
106
|
+
return {
|
|
107
|
+
minFreeMb: envMb(env, MIN_FREE_MEM_ENV, DEFAULT_MIN_FREE_MEM_MB),
|
|
108
|
+
reserveMb: envMb(env, CHILD_RESERVE_ENV, DEFAULT_CHILD_RESERVE_MB),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface MemoryGateVerdict {
|
|
113
|
+
ok: boolean;
|
|
114
|
+
availableMb: number;
|
|
115
|
+
requiredMb: number;
|
|
116
|
+
message?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Refuse when `available - reserve × (inFlight + 1) < minFree`. `inFlight`
|
|
121
|
+
* counts this supervisor's children still starting (their memory is not yet
|
|
122
|
+
* visible in MemAvailable), so a parallel burst cannot all pass one reading.
|
|
123
|
+
*/
|
|
124
|
+
export function checkMemoryGate(input: { name: string; inFlight: number; config?: MemoryGateConfig; available?: () => number }): MemoryGateVerdict {
|
|
125
|
+
const config = input.config ?? resolveMemoryGateConfig();
|
|
126
|
+
if (config.minFreeMb <= 0) return { ok: true, availableMb: -1, requiredMb: 0 };
|
|
127
|
+
const availableMb = (input.available ?? availableMemoryMb)();
|
|
128
|
+
const requiredMb = config.minFreeMb + config.reserveMb * (input.inFlight + 1);
|
|
129
|
+
if (availableMb >= requiredMb) return { ok: true, availableMb, requiredMb };
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
availableMb,
|
|
133
|
+
requiredMb,
|
|
134
|
+
message:
|
|
135
|
+
`insufficient memory to start subagent ${input.name}: ${availableMb} MB available, ` +
|
|
136
|
+
`${requiredMb} MB required (${MIN_FREE_MEM_ENV}=${config.minFreeMb} + ${config.reserveMb} MB per starting child × ${input.inFlight + 1}). ` +
|
|
137
|
+
`Wait for running subagents to finish (or stop some with /legion stop) before retrying; ` +
|
|
138
|
+
`set ${MIN_FREE_MEM_ENV}=0 to disable this gate.`,
|
|
139
|
+
};
|
|
140
|
+
}
|