@ian-pascoe/pi-minimal-subagents 0.6.4 → 0.6.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ian-pascoe/pi-minimal-subagents",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "private": false,
5
5
  "description": "Persistent nested subagents with bounded delegation for Pi",
6
6
  "keywords": [
@@ -1,4 +1,3 @@
1
- import type { JsonValue } from "@earendil-works/pi-ai";
2
1
  import type { SettingsManager } from "@earendil-works/pi-coding-agent";
3
2
  import { type Static, Type } from "typebox";
4
3
  import { Value } from "typebox/value";
@@ -7,16 +6,18 @@ import { DEFAULT_MAX_SUBAGENT_DEPTH, THINKING_LEVELS } from "./minimal-subagents
7
6
  const MODEL_ROLE_NAME_MAX_LENGTH = 64;
8
7
  const MODEL_ROLE_HINT_MAX_LENGTH = 500;
9
8
 
10
- const JsonValueSchema = Type.Unsafe<JsonValue>({});
11
9
  const SettingsDocumentSchema = Type.Object({
12
- minimalSubagents: Type.Optional(JsonValueSchema),
10
+ minimalSubagents: Type.Optional(Type.Unknown()),
13
11
  });
14
12
  const MinimalSubagentsSettingsSchema = Type.Object({
15
- enabled: Type.Optional(JsonValueSchema),
16
- maxSubagentDepth: Type.Optional(JsonValueSchema),
17
- modelRoles: Type.Optional(JsonValueSchema),
13
+ enabled: Type.Optional(Type.Unknown()),
14
+ maxSubagentDepth: Type.Optional(Type.Unknown()),
15
+ modelRoles: Type.Optional(Type.Unknown()),
16
+ });
17
+ const ModelRoleObjectSchema = Type.Object({
18
+ model: Type.Optional(Type.Unknown()),
19
+ hint: Type.Optional(Type.Unknown()),
18
20
  });
19
- const JsonObjectSchema = Type.Record(Type.String(), JsonValueSchema);
20
21
  const EnabledSettingSchema = Type.Boolean();
21
22
  const PositiveSafeIntegerSchema = Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
22
23
  const MaxSubagentDepthSettingSchema = Type.Union([PositiveSafeIntegerSchema, Type.Null()]);
@@ -28,7 +29,7 @@ const ExpandedModelRoleSchema = Type.Object(
28
29
  },
29
30
  { additionalProperties: false },
30
31
  );
31
- const ModelRoleEntriesSchema = Type.Record(Type.String(), JsonValueSchema);
32
+ const ModelRoleEntriesSchema = Type.Record(Type.String(), Type.Unknown());
32
33
  const ModelRolesSettingSchema = Type.Union([ModelRoleEntriesSchema, Type.Null()]);
33
34
 
34
35
  type ModelRoleThinkingLevel = (typeof THINKING_LEVELS)[number];
@@ -61,7 +62,7 @@ export interface ResolvedMinimalSubagentsConfig {
61
62
  }
62
63
 
63
64
  interface MinimalSubagentsSettingsDocument {
64
- minimalSubagents?: JsonValue;
65
+ minimalSubagents?: unknown;
65
66
  }
66
67
 
67
68
  interface MinimalSubagentsConfigInput {
@@ -102,7 +103,7 @@ type ModelRoleWireValue =
102
103
  | { kind: "delete" }
103
104
  | { kind: "shorthand"; model: string }
104
105
  | { kind: "expanded"; fields: Static<typeof ExpandedModelRoleSchema> }
105
- | { kind: "malformed-expanded"; fields: Record<string, JsonValue> }
106
+ | { kind: "malformed-expanded"; fields: Static<typeof ModelRoleObjectSchema> }
106
107
  | { kind: "invalid" };
107
108
 
108
109
  type ModelRolesWireValue =
@@ -110,15 +111,17 @@ type ModelRolesWireValue =
110
111
  | { kind: "entries"; entries: ReadonlyMap<string, ModelRoleWireValue> }
111
112
  | { kind: "invalid" };
112
113
 
113
- function parseMaxSubagentDepthWireValue(value: JsonValue): MaxSubagentDepthWireValue {
114
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Authored settings remain unparsed until the depth schema below validates their value.
115
+ function parseMaxSubagentDepthWireValue(value: unknown): MaxSubagentDepthWireValue {
114
116
  if (!Value.Check(MaxSubagentDepthSettingSchema, value)) return { kind: "invalid" };
115
117
  return value === null ? { kind: "reset" } : { kind: "depth", value };
116
118
  }
117
119
 
118
- function parseModelRoleWireValue(value: JsonValue): ModelRoleWireValue {
120
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Role entries may contain arbitrary settings data; schemas classify them before model/hint validation.
121
+ function parseModelRoleWireValue(value: unknown): ModelRoleWireValue {
119
122
  if (value === null) return { kind: "delete" };
120
123
  if (Value.Check(ShorthandModelRoleSchema, value)) return { kind: "shorthand", model: value };
121
- if (Value.Check(JsonObjectSchema, value)) {
124
+ if (Value.Check(ModelRoleObjectSchema, value)) {
122
125
  return Value.Check(ExpandedModelRoleSchema, value)
123
126
  ? { kind: "expanded", fields: value }
124
127
  : { kind: "malformed-expanded", fields: value };
@@ -132,7 +135,8 @@ function isExpandedModelRoleWireValue(
132
135
  return value.kind === "expanded" || value.kind === "malformed-expanded";
133
136
  }
134
137
 
135
- function parseModelRolesWireValue(value: JsonValue): ModelRolesWireValue {
138
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Validate the authored role collection before parsing each untrusted entry.
139
+ function parseModelRolesWireValue(value: unknown): ModelRolesWireValue {
136
140
  if (!Value.Check(ModelRolesSettingSchema, value)) return { kind: "invalid" };
137
141
  if (value === null) return { kind: "reset" };
138
142
  return {
@@ -65,7 +65,10 @@ import {
65
65
  MinimalSubagentsStatusPanelController,
66
66
  type MinimalSubagentsStatusAccess,
67
67
  } from "./minimal-subagents-status-panel.js";
68
- import { createCoordinatorToolDefinitions } from "./minimal-subagents-tools.js";
68
+ import {
69
+ createCoordinatorToolDefinitions,
70
+ type CoordinatorToolOperations,
71
+ } from "./minimal-subagents-tools.js";
69
72
  import {
70
73
  renderMinimalSubagentsMessage,
71
74
  renderMinimalSubagentsResult,
@@ -393,6 +396,15 @@ const productionLifecycleEffects: MinimalSubagentsLifecycleEffects = {
393
396
  /** Own coordinator, UI, and prepared-fork state for one root Pi session lifecycle. */
394
397
  export class MinimalSubagentsLifecycleController {
395
398
  private coordinator: MinimalSubagentsCoordinator | undefined;
399
+ private readonly coordinatorOperations: CoordinatorToolOperations = {
400
+ spawn: (...args) => this.requireCoordinator().spawn(...args),
401
+ inspectStatus: (...args) => this.requireCoordinator().inspectStatus(...args),
402
+ sendAgentMessage: (...args) => this.requireCoordinator().sendAgentMessage(...args),
403
+ wait: (...args) => this.requireCoordinator().wait(...args),
404
+ status: (...args) => this.requireCoordinator().status(...args),
405
+ cancel: (...args) => this.requireCoordinator().cancel(...args),
406
+ delete: (...args) => this.requireCoordinator().delete(...args),
407
+ };
396
408
  private uiController: MinimalSubagentsUiController | undefined;
397
409
  private statusPanelController: MinimalSubagentsStatusPanelController | undefined;
398
410
  private accessSession: ActiveSubagentAccessSession | undefined;
@@ -406,8 +418,18 @@ export class MinimalSubagentsLifecycleController {
406
418
  private readonly effects: MinimalSubagentsLifecycleEffects,
407
419
  ) {}
408
420
 
409
- /** Register renderers and the six Pi lifecycle event handlers owned by Minimal Subagents. */
421
+ /** Register stable tools, renderers, and the six Pi lifecycle event handlers. */
410
422
  register(): void {
423
+ const rootTools = createCoordinatorToolDefinitions({
424
+ coordinator: this.coordinatorOperations,
425
+ callerId: "root",
426
+ allowFanoutTools: true,
427
+ schemas: createCoordinatorToolSchemas([]),
428
+ captureCaller: (context) => rootCallerSnapshot(this.pi, context),
429
+ onActivity: () => this.uiController?.refresh(),
430
+ onAttention: (message) => this.accessSession?.context.ui.notify(message, "error"),
431
+ });
432
+ for (const tool of rootTools) this.pi.registerTool(tool);
411
433
  this.pi.registerMessageRenderer("minimal-subagents.message", renderMinimalSubagentsMessage);
412
434
  this.pi.registerMessageRenderer("minimal-subagents.result", renderMinimalSubagentsResult);
413
435
  this.pi.registerCommand("subagents", {
@@ -573,7 +595,7 @@ export class MinimalSubagentsLifecycleController {
573
595
  this.uiController.refresh();
574
596
 
575
597
  const rootTools = createCoordinatorToolDefinitions({
576
- coordinator: activeCoordinator,
598
+ coordinator: this.coordinatorOperations,
577
599
  callerId: "root",
578
600
  allowFanoutTools: true,
579
601
  modelRoles: minimalSubagentsConfig.modelRoles,
@@ -605,6 +627,13 @@ export class MinimalSubagentsLifecycleController {
605
627
  }
606
628
  }
607
629
 
630
+ private requireCoordinator(): MinimalSubagentsCoordinator {
631
+ if (!this.coordinator) {
632
+ throw new Error("Minimal subagents lifecycle: coordinator is not initialized");
633
+ }
634
+ return this.coordinator;
635
+ }
636
+
608
637
  private async prepareSessionFork(
609
638
  event: SessionBeforeForkEvent,
610
639
  context: ExtensionContext,
@@ -2,7 +2,7 @@ import type { ForkSnapshot } from "./minimal-subagents-types.js";
2
2
  import { canonicalPath } from "./minimal-subagents-paths.js";
3
3
 
4
4
  declare global {
5
- // eslint-disable-next-line no-var -- A process-global handoff must be visible to replacement extension instances.
5
+ // A process-global handoff must be visible to replacement extension instances.
6
6
  var minimalSubagentsForkSnapshots: Map<string, ForkSnapshot> | undefined;
7
7
  }
8
8
 
@@ -1,9 +1,5 @@
1
- import type { JsonValue } from "@earendil-works/pi-ai";
2
1
  import { Type, type Static } from "typebox";
3
2
 
4
- /** Establishes Pi's recursive JSON owner type before Registry envelope parsing. */
5
- export const RegistryJsonValueWireSchema = Type.Unsafe<JsonValue>({});
6
-
7
3
  const NonnegativeNumberSchema = Type.Number({ minimum: 0 });
8
4
  const NonEmptyStringSchema = Type.String({ minLength: 1 });
9
5
  const TurnStatusSchema = Type.Union([
@@ -28,7 +28,6 @@ import {
28
28
  RegistryDeliveryTurnEventWireSchema,
29
29
  RegistryEnvelopeWireSchema,
30
30
  RegistryEventDiscriminantWireSchema,
31
- RegistryJsonValueWireSchema,
32
31
  RegistryLooseEnvelopeWireSchema,
33
32
  RegistryMessageRecordedEventWireSchema,
34
33
  RegistryRootProbeWireSchema,
@@ -846,7 +845,8 @@ function validParsedEvent(event: RegistryEventV2): ParsedRegistryEvent {
846
845
  type RegistryParseInput = JsonValue | RegistryEventV2;
847
846
 
848
847
  function parseRegistryEventRecord(
849
- value: RegistryParseInput,
848
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Registry entry data is unparsed; ownership is checked first, then envelope and event schemas validate every consumed field.
849
+ value: unknown,
850
850
  rootSessionId: string,
851
851
  ): ParsedRegistryEvent {
852
852
  if (Value.Check(RegistryRootProbeWireSchema, value) && value.root_session_id !== rootSessionId) {
@@ -1191,10 +1191,6 @@ export function replayRegistryEntries(
1191
1191
  const diagnostics: RegistryReplayDiagnostic[] = [];
1192
1192
  entries.forEach((entry, entryIndex) => {
1193
1193
  if (entry.type !== "custom" || entry.customType !== REGISTRY_ENTRY_TYPE) return;
1194
- if (!Value.Check(RegistryJsonValueWireSchema, entry.data)) {
1195
- reportDiagnostic(diagnostics, entryIndex, "invalid-envelope", "record must be JSON");
1196
- return;
1197
- }
1198
1194
  const parsed = parseRegistryEventRecord(entry.data, rootSessionId);
1199
1195
  if (parsed.kind === "foreign-root") return;
1200
1196
  if (parsed.kind === "event") {
@@ -1,8 +1,4 @@
1
- import type {
2
- AgentToolResult,
3
- MessageRenderer,
4
- ToolDefinition,
5
- } from "@earendil-works/pi-coding-agent";
1
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
6
2
  import { Type, type Static } from "typebox";
7
3
  import { Value } from "typebox/value";
8
4
  import { COORDINATOR_TOOL_NAMES } from "./minimal-subagents-capabilities.js";
@@ -339,8 +335,6 @@ export type RenderStatusAgent = Static<typeof RenderStatusAgentSchema>;
339
335
  /** Raw tool arguments supplied by Pi's tool-rendering interface. */
340
336
  export type CoordinatorToolCallInput = Parameters<NonNullable<ToolDefinition["renderCall"]>>[0];
341
337
 
342
- type CoordinatorMessageInput = Parameters<MessageRenderer>[0];
343
-
344
338
  /** Parsed tool-call arguments tagged by their coordinator tool name. */
345
339
  export type ParsedCoordinatorToolCall =
346
340
  | { toolName: "subagent"; args: SpawnCallArguments }
@@ -383,7 +377,8 @@ export function parseCoordinatorToolCall(
383
377
  /** Parse one historical tool result exactly once at the transcript rendering boundary. */
384
378
  export function parseCoordinatorToolResult(
385
379
  toolName: CoordinatorToolName,
386
- details: AgentToolResult<unknown>["details"],
380
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Historical tool details are unparsed until the selected per-tool schema checks them below.
381
+ details: unknown,
387
382
  ): ParsedCoordinatorToolResult | undefined {
388
383
  switch (toolName) {
389
384
  case "subagent":
@@ -417,7 +412,8 @@ export type CoordinatorMessageRenderDetails = Static<typeof CoordinatorMessageRe
417
412
 
418
413
  /** Parse optional custom-message details while tolerating legacy field names. */
419
414
  export function parseCoordinatorMessageDetails(
420
- details: CoordinatorMessageInput["details"],
415
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Historical custom-message metadata is validated by its owning schema below.
416
+ details: unknown,
421
417
  ): CoordinatorMessageRenderDetails | undefined {
422
418
  return Value.Check(CoordinatorMessageRenderDetailsSchema, details) ? details : undefined;
423
419
  }
@@ -4,8 +4,6 @@ import { randomUUID } from "node:crypto";
4
4
  import { mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
5
5
  import { basename, dirname, join, resolve } from "node:path";
6
6
  import lockfile from "proper-lockfile";
7
- import { Type } from "typebox";
8
- import { Value } from "typebox/value";
9
7
 
10
8
  /** Identifies the standard Pi settings file changed by a Subagent Access command. */
11
9
  export type MinimalSubagentsSettingsScope = "global" | "project";
@@ -64,8 +62,10 @@ interface ExistingSettingsDocument {
64
62
  readonly mode: number;
65
63
  }
66
64
 
67
- const JsonValueSchema = Type.Unsafe<JsonValue>({});
68
- const SettingsJsonObjectSchema = Type.Record(Type.String(), JsonValueSchema);
65
+ function isSettingsJsonObject(value: JsonValue | undefined): value is SettingsJsonObject {
66
+ // oxlint-disable-next-line anti-slop/no-runtime-typeof -- JSON.parse already established JSON data; distinguish object roots and settings blocks from primitives and arrays.
67
+ return value !== null && typeof value === "object" && !Array.isArray(value);
68
+ }
69
69
  const SETTINGS_LOCK_RETRY_DELAY_MS = 20;
70
70
  const SETTINGS_LOCK_RETRIES = 100;
71
71
  const NEW_SETTINGS_FILE_MODE = 0o600;
@@ -92,7 +92,8 @@ function parseSettingsDocument(
92
92
  scope: MinimalSubagentsSettingsScope,
93
93
  path: string,
94
94
  ): ParsedSettingsDocument {
95
- let parsed: unknown;
95
+ // JSON.parse is the provenance for this JSON type; the object shape is checked below.
96
+ let parsed: JsonValue;
96
97
  try {
97
98
  parsed = JSON.parse(stripUtf8Bom(content));
98
99
  } catch (cause) {
@@ -109,11 +110,11 @@ function parseSettingsDocument(
109
110
  };
110
111
  }
111
112
 
112
- if (!Value.Check(SettingsJsonObjectSchema, parsed)) {
113
+ if (!isSettingsJsonObject(parsed)) {
113
114
  return { ok: false, error: settingsContractError(scope, path, "expected an object root") };
114
115
  }
115
116
  const minimalSubagents = parsed.minimalSubagents;
116
- if (minimalSubagents !== undefined && !Value.Check(SettingsJsonObjectSchema, minimalSubagents)) {
117
+ if (minimalSubagents !== undefined && !isSettingsJsonObject(minimalSubagents)) {
117
118
  return {
118
119
  ok: false,
119
120
  error: settingsContractError(
@@ -131,7 +132,7 @@ function mutateMinimalSubagentsEnabled(
131
132
  enabled: boolean | undefined,
132
133
  ): void {
133
134
  const currentMinimalSubagents = settings.minimalSubagents;
134
- const minimalSubagents = Value.Check(SettingsJsonObjectSchema, currentMinimalSubagents)
135
+ const minimalSubagents = isSettingsJsonObject(currentMinimalSubagents)
135
136
  ? currentMinimalSubagents
136
137
  : {};
137
138