@jwilger/pi-development-system 0.48.0 → 0.51.1

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.
@@ -1,5 +1,5 @@
1
1
  // @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
2
- import { lstatSync, readdirSync, readFileSync, type Dirent, type Stats } from "node:fs";
2
+ import { type Dirent, lstatSync, readdirSync, readFileSync, type Stats } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { delimiter, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
5
5
  import { parseDocument } from "yaml";
@@ -17,8 +17,8 @@ export interface DiscoverImportOptions {
17
17
  cwd: string;
18
18
  agentDir: string;
19
19
  includeProject: boolean;
20
- homeDir?: string;
21
- extraAgentDirs?: string;
20
+ homeDir?: string | undefined;
21
+ extraAgentDirs?: string | undefined;
22
22
  }
23
23
 
24
24
  const EXTRA_AGENT_DIRS_ENV = "PI_SUBAGENT_EXTRA_AGENT_DIRS";
@@ -227,7 +227,7 @@ function walk(
227
227
  continue;
228
228
  }
229
229
  if (isSkillMarkdownName(entry.name)) continue;
230
- if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.endsWith(".chain.md")) {
230
+ if (!(entry.isFile() && entry.name.endsWith(".md")) || entry.name.endsWith(".chain.md")) {
231
231
  continue;
232
232
  }
233
233
  if (isExcluded(child, excludes) || isManagerOwned(child, managerDirs)) continue;
@@ -270,7 +270,7 @@ function considerFile(
270
270
  const rawName = fields.name?.trim() ?? "";
271
271
  const name = rawName || (permissive ? filenameName(resolved) : "");
272
272
  const description = fields.description ?? (permissive ? "" : undefined);
273
- if (!name || description === undefined || (!permissive && !description.trim())) return;
273
+ if (!name || description === undefined || !(permissive || description.trim())) return;
274
274
  const packageName = fields.packageName?.trim() ?? "";
275
275
  candidates.push({
276
276
  id: resolved,
@@ -303,7 +303,7 @@ function looseDisplayFields(content: string): DisplayFields | undefined {
303
303
  /* Ambiguous display metadata is not selectable. */
304
304
  }
305
305
  }
306
- if (!fields.name || !fields.description) return undefined;
306
+ if (!(fields.name && fields.description)) return undefined;
307
307
  return { name: fields.name, description: fields.description, packageName: fields.package };
308
308
  }
309
309
 
@@ -1,4 +1,3 @@
1
- // @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
2
1
  import { resolveCandidate } from "../../core/models.ts";
3
2
 
4
3
  const MODEL_IDENTITY = /^[^\s/]+\/[^\s]+$/;
@@ -15,15 +14,12 @@ export function getModelPreferences(type: {
15
14
  }): string[] | undefined {
16
15
  const hasModels = type.models !== undefined;
17
16
  const hasModel = type.model !== undefined;
18
- if (hasModels && hasModel)
19
- throw new Error("Specify either models or model, not both");
17
+ if (hasModels && hasModel) throw new Error("Specify either models or model, not both");
20
18
  if (hasModels) {
21
19
  if (!Array.isArray(type.models))
22
20
  throw new Error("models must be an array of provider/model-id strings");
23
21
  if (type.models.length === 0)
24
- throw new Error(
25
- "models must not be empty; omit models to inherit the parent/default model",
26
- );
22
+ throw new Error("models must not be empty; omit models to inherit the parent/default model");
27
23
  const models = type.models.map((value, index) =>
28
24
  validateModelPreference(value, `models[${index}]`),
29
25
  );
@@ -40,11 +36,11 @@ export function modelIdentity(model: { provider: string; id: string }): string {
40
36
  }
41
37
 
42
38
  export class ModelPreferenceError extends Error {
43
- constructor(
44
- message: string,
45
- readonly scopedModelFiltering: boolean,
46
- ) {
39
+ readonly scopedModelFiltering: boolean;
40
+
41
+ constructor(message: string, scopedModelFiltering: boolean) {
47
42
  super(message);
43
+ this.scopedModelFiltering = scopedModelFiltering;
48
44
  }
49
45
 
50
46
  get summary(): string {
@@ -1,4 +1,5 @@
1
- // @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
1
+ // biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
2
+ // biome-ignore-all lint/suspicious/noShadow: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
2
3
  import { randomUUID } from "node:crypto";
3
4
  import {
4
5
  closeSync,
@@ -23,11 +24,7 @@ export type WidgetMode = (typeof WIDGET_MODES)[number];
23
24
  const TOOL_FILTERING_MODES = ["allowed", "all-except-blocked", "all"] as const;
24
25
  export type ToolFilteringMode = (typeof TOOL_FILTERING_MODES)[number];
25
26
 
26
- const MODEL_SELECTION_MODES = [
27
- "pick-first-available",
28
- "pick-first-scoped",
29
- "use-current",
30
- ] as const;
27
+ const MODEL_SELECTION_MODES = ["pick-first-available", "pick-first-scoped", "use-current"] as const;
31
28
  export type ModelSelectionMode = (typeof MODEL_SELECTION_MODES)[number];
32
29
 
33
30
  export interface ManagerSettings {
@@ -55,7 +52,15 @@ export const DEFAULT_MANAGER_SETTINGS: ManagerSettings = {
55
52
  };
56
53
 
57
54
  const KEYS = [
58
- "maxLevels", "maxConcurrent", "maxThreads", "modelSelection", "subagentMode", "toolFiltering", "widgetMode", "nerdFontIcons", "finalRecap",
55
+ "maxLevels",
56
+ "maxConcurrent",
57
+ "maxThreads",
58
+ "modelSelection",
59
+ "subagentMode",
60
+ "toolFiltering",
61
+ "widgetMode",
62
+ "nerdFontIcons",
63
+ "finalRecap",
59
64
  ] as const;
60
65
  const MAX_LEVELS = 32;
61
66
 
@@ -65,9 +70,7 @@ function errorCode(error: unknown): string | undefined {
65
70
  : undefined;
66
71
  }
67
72
 
68
- function lstatIfPresent(
69
- path: string,
70
- ): ReturnType<typeof lstatSync> | undefined {
73
+ function lstatIfPresent(path: string): ReturnType<typeof lstatSync> | undefined {
71
74
  try {
72
75
  return lstatSync(path);
73
76
  } catch (error) {
@@ -76,9 +79,7 @@ function lstatIfPresent(
76
79
  }
77
80
  }
78
81
 
79
- function assertNotSymlinkPath(
80
- path: string,
81
- ): ReturnType<typeof lstatSync> | undefined {
82
+ function assertNotSymlinkPath(path: string): ReturnType<typeof lstatSync> | undefined {
82
83
  const stat = lstatIfPresent(path);
83
84
  if (stat?.isSymbolicLink()) throw new Error(`Unsafe symlink path: ${path}`);
84
85
  return stat;
@@ -107,8 +108,8 @@ function parseSettings(content: string): Partial<ManagerSettings> {
107
108
  try {
108
109
  parsed = JSON.parse(content);
109
110
  } catch (error) {
110
- const detail = error instanceof Error ? error.message : String(error);
111
- throw new Error(`Invalid JSON: ${detail}`);
111
+ const reason = error instanceof Error ? error.message : String(error);
112
+ throw new Error(`Invalid JSON: ${reason}`, { cause: error });
112
113
  }
113
114
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
114
115
  throw new Error("Settings must be a JSON object");
@@ -123,7 +124,9 @@ function parseSettings(content: string): Partial<ManagerSettings> {
123
124
  if (typeof record.scopedModelFiltering !== "boolean")
124
125
  throw new Error("scopedModelFiltering must be a boolean");
125
126
  if (!Object.hasOwn(record, "modelSelection"))
126
- layer.modelSelection = record.scopedModelFiltering ? "pick-first-scoped" : "pick-first-available";
127
+ layer.modelSelection = record.scopedModelFiltering
128
+ ? "pick-first-scoped"
129
+ : "pick-first-available";
127
130
  }
128
131
  for (const key of KEYS) {
129
132
  if (!Object.hasOwn(record, key)) continue;
@@ -186,8 +189,7 @@ function readLayerContent(entry: SettingsLayer): string | undefined {
186
189
  }
187
190
  const stat = assertNotSymlinkPath(entry.filePath);
188
191
  if (!stat) return undefined;
189
- if (!stat.isFile())
190
- throw new Error(`Settings must be a regular file: ${entry.filePath}`);
192
+ if (!stat.isFile()) throw new Error(`Settings must be a regular file: ${entry.filePath}`);
191
193
  return readFileSync(entry.filePath, "utf8");
192
194
  }
193
195
 
@@ -214,9 +216,13 @@ export function loadManagerSaveScope(options: {
214
216
  if (content !== undefined) {
215
217
  const state: unknown = JSON.parse(content);
216
218
  if (
217
- state && typeof state === "object" && !Array.isArray(state) &&
218
- "saveScope" in state && state.saveScope === "user"
219
- ) return "user";
219
+ state &&
220
+ typeof state === "object" &&
221
+ !Array.isArray(state) &&
222
+ "saveScope" in state &&
223
+ state.saveScope === "user"
224
+ )
225
+ return "user";
220
226
  }
221
227
  } catch {
222
228
  // Missing, invalid or unsafe UI state must not prevent opening settings.
@@ -243,8 +249,7 @@ export function loadManagerSettings(options: {
243
249
  const diagnostics: string[] = [];
244
250
  const settings: ManagerSettings = { ...DEFAULT_MANAGER_SETTINGS };
245
251
  const layers = [layer(options.agentDir, ["subagent-manager"])];
246
- if (options.includeProject)
247
- layers.push(layer(options.cwd, [".pi", "agent", "subagent-manager"]));
252
+ if (options.includeProject) layers.push(layer(options.cwd, [".pi", "agent", "subagent-manager"]));
248
253
  for (const entry of layers) {
249
254
  try {
250
255
  const parsed = readLayer(entry);
@@ -257,13 +262,6 @@ export function loadManagerSettings(options: {
257
262
  }
258
263
 
259
264
  function serializedSettings(settings: ManagerSettings): string {
260
- if (
261
- settings === null ||
262
- typeof settings !== "object" ||
263
- Array.isArray(settings)
264
- ) {
265
- throw new Error("Settings must be a JSON object");
266
- }
267
265
  for (const key of Object.keys(settings)) {
268
266
  if (!isSupportedKey(key)) throw new Error(`Unknown settings key: ${key}`);
269
267
  }
@@ -298,17 +296,20 @@ function ensureOwnedDirectory(directory: string): void {
298
296
  }
299
297
 
300
298
  function ensureScopeDirectories(directories: string[]): void {
301
- assertExistingDirectory(directories[0]);
302
- for (let index = 1; index < directories.length; index++) {
303
- assertExistingDirectory(directories[index - 1]);
304
- ensureOwnedDirectory(directories[index]);
299
+ const [root, ...rest] = directories;
300
+ if (root === undefined) return;
301
+ assertExistingDirectory(root);
302
+ let parent = root;
303
+ for (const directory of rest) {
304
+ assertExistingDirectory(parent);
305
+ ensureOwnedDirectory(directory);
306
+ parent = directory;
305
307
  }
306
308
  }
307
309
 
308
310
  function assertRegularFileOrAbsent(filePath: string): void {
309
311
  const stat = assertNotSymlinkPath(filePath);
310
- if (stat && !stat.isFile())
311
- throw new Error(`Settings must be a regular file: ${filePath}`);
312
+ if (stat && !stat.isFile()) throw new Error(`Settings must be a regular file: ${filePath}`);
312
313
  }
313
314
 
314
315
  function exclusiveCreateFlags(): number {
@@ -333,8 +334,7 @@ function writeAtomically(entry: SettingsLayer, content: string): void {
333
334
  for (const directory of entry.directories) assertExistingDirectory(directory);
334
335
  assertRegularFileOrAbsent(filePath);
335
336
  const tempStat = assertNotSymlinkPath(temporary);
336
- if (!tempStat?.isFile())
337
- throw new Error(`Settings must be a regular file: ${temporary}`);
337
+ if (!tempStat?.isFile()) throw new Error(`Settings must be a regular file: ${temporary}`);
338
338
  renameSync(temporary, filePath);
339
339
  } finally {
340
340
  if (created) {
@@ -1,4 +1,3 @@
1
- // @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
2
1
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
2
  import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
4
3
 
@@ -11,22 +10,24 @@ export interface ResolvedAgentSettings {
11
10
  export interface AgentType {
12
11
  name: string;
13
12
  description: string;
14
- models?: string[];
13
+ models?: string[] | undefined;
15
14
  /** @deprecated Use ordered models instead. */
16
- model?: string;
15
+ model?: string | undefined;
17
16
  /** Advisory display names. Not provider/model pins and never used for selection. */
18
- modelSuggestions?: string[];
19
- thinkingLevel?: ThinkingLevel;
17
+ modelSuggestions?: string[] | undefined;
18
+ thinkingLevel?: ThinkingLevel | undefined;
20
19
  /** devsys: per-spawn pin set by the coordinator via agent_spawn; beats preferences and inheritance. */
21
- spawnOverrides?: { model?: string; thinkingLevel?: ThinkingLevel };
20
+ spawnOverrides?:
21
+ | { model?: string | undefined; thinkingLevel?: ThinkingLevel | undefined }
22
+ | undefined;
22
23
  /** Pi semantic token for the type pill background and task path foreground. */
23
- color?: string;
24
+ color?: string | undefined | undefined;
24
25
  /** Optional literal Nerd Font glyph; displayed only when the labs setting is enabled. */
25
- icon?: string;
26
- tools?: { allow?: string[]; block?: string[] };
26
+ icon?: string | undefined | undefined;
27
+ tools?: { allow?: string[] | undefined; block?: string[] | undefined } | undefined;
27
28
  systemPrompt: string;
28
- filePath?: string;
29
- source?: "bundled" | "user" | "project";
29
+ filePath?: string | undefined;
30
+ source?: "bundled" | "user" | "project" | undefined;
30
31
  /**
31
32
  * How this definition is customized relative to its base.
32
33
  * - fork: a full `<name>.md` copy (owns the system prompt).
@@ -34,15 +35,17 @@ export interface AgentType {
34
35
  * (no Markdown body; unset fields follow the base).
35
36
  * Absent for pure bundled definitions and brand-new drafts.
36
37
  */
37
- customization?: {
38
- kind: "fork" | "override";
39
- scope: "user" | "project";
40
- filePath: string;
41
- };
38
+ customization?:
39
+ | {
40
+ kind: "fork" | "override";
41
+ scope: "user" | "project";
42
+ filePath: string;
43
+ }
44
+ | undefined;
42
45
  /** Source of the base definition under the customization, if any. */
43
- baseSource?: "bundled" | "user" | "project";
46
+ baseSource?: "bundled" | "user" | "project" | undefined;
44
47
  /** File path of the base definition under the customization, if any. */
45
- baseFilePath?: string;
48
+ baseFilePath?: string | undefined;
46
49
  }
47
50
  type ThreadState = "starting" | "running" | "paused" | "completed" | "failed" | "stopped";
48
51
  export interface ThreadView {
@@ -50,33 +53,33 @@ export interface ThreadView {
50
53
  parent: string | null;
51
54
  owner: string;
52
55
  type: string;
53
- color?: string;
54
- icon?: string;
56
+ color?: string | undefined;
57
+ icon?: string | undefined;
55
58
  state: ThreadState;
56
59
  task: string;
57
60
  status: string;
58
- output?: string;
59
- error?: string;
61
+ output?: string | undefined;
62
+ error?: string | undefined;
60
63
  createdAt: number;
61
64
  updatedAt: number;
62
- sessionFile?: string;
65
+ sessionFile?: string | undefined;
63
66
  /** Frozen active time. Renderer adds `Date.now() - startedAt` while a run is active. */
64
67
  elapsedMs?: number;
65
68
  /** Current run start. Live only; omitted from saved views so reloads do not count offline time. */
66
- startedAt?: number;
69
+ startedAt?: number | undefined;
67
70
  /** Cumulative input tokens, including cache read/write. Live views include the in-progress message. */
68
71
  inputTokens?: number;
69
72
  /** Cumulative output tokens. Live views include the in-progress message. */
70
73
  outputTokens?: number;
71
74
  /** devsys: per-spawn model/thinkingLevel pin requested through agent_spawn, when any. */
72
- pinned?: { model?: string; thinkingLevel?: ThinkingLevel };
75
+ pinned?: { model?: string | undefined; thinkingLevel?: ThinkingLevel | undefined };
73
76
  }
74
77
  type DriverEvent =
75
78
  | { kind: "activity" | "error"; text: string }
76
79
  | {
77
80
  kind: "checkpoint";
78
81
  text: string;
79
- sessionFile?: string;
82
+ sessionFile?: string | undefined;
80
83
  sessionLeafId?: string | null;
81
84
  }
82
85
  | {
@@ -106,7 +109,7 @@ export interface TranscriptSnapshot {
106
109
  tools: readonly TranscriptToolState[];
107
110
  inheritedCount: number;
108
111
  thread?: ThreadView;
109
- error?: string;
112
+ error?: string | undefined;
110
113
  }
111
114
  export interface TranscriptObservation {
112
115
  snapshot: TranscriptSnapshot;
@@ -123,7 +126,7 @@ export interface AgentDriver {
123
126
  abort(): Promise<void>;
124
127
  dispose(): void;
125
128
  sendUpdate(content: string): Promise<void> | void;
126
- sessionFile?: string;
129
+ sessionFile?: string | undefined;
127
130
  sessionLeafId?: string | null;
128
131
  }
129
132
  export interface DriverOptions {
@@ -134,7 +137,7 @@ export interface DriverOptions {
134
137
  onEvent(event: DriverEvent): void;
135
138
  shouldPause(): boolean;
136
139
  parentPath: string | null;
137
- sessionFile?: string;
140
+ sessionFile?: string | undefined;
138
141
  /** Retained transcript of a nested lexical parent. Absent for /root and independent roots. */
139
142
  parentSessionFile?: string;
140
143
  sessionLeafId?: string | null;
@@ -145,15 +148,15 @@ export interface SavedThreadView {
145
148
  parent: string | null;
146
149
  owner: string;
147
150
  type: string;
148
- color?: string;
149
- icon?: string;
151
+ color?: string | undefined;
152
+ icon?: string | undefined;
150
153
  state: ThreadState;
151
154
  task: string;
152
155
  status: string;
153
- output?: string;
154
- error?: string;
156
+ output?: string | undefined;
157
+ error?: string | undefined;
155
158
  createdAt: number;
156
- sessionFile?: string;
159
+ sessionFile?: string | undefined;
157
160
  sessionLeafId?: string | null;
158
161
  /** Frozen cumulative active time. `startedAt` is intentionally not persisted. */
159
162
  elapsedMs?: number;
@@ -5,11 +5,11 @@ import {
5
5
  type KeybindingsManager,
6
6
  } from "@earendil-works/pi-coding-agent";
7
7
  import {
8
+ type EditorTheme,
8
9
  getKeybindings,
9
10
  Key,
10
- matchesKey,
11
- type EditorTheme,
12
11
  type Keybinding,
12
+ matchesKey,
13
13
  type TUI,
14
14
  } from "@earendil-works/pi-tui";
15
15
 
@@ -87,8 +87,11 @@ export class AgentNavigationEditor extends CustomEditor {
87
87
 
88
88
  override handleInput(data: string): void {
89
89
  const outer = this.dispatchDepth === 0;
90
- const direction = this.ordinaryArrow(data, "left") ? "left"
91
- : this.ordinaryArrow(data, "right") ? "right" : undefined;
90
+ const direction = this.ordinaryArrow(data, "left")
91
+ ? "left"
92
+ : this.ordinaryArrow(data, "right")
93
+ ? "right"
94
+ : undefined;
92
95
  if (outer && (!direction || (this.pending && this.pending !== direction))) this.inputEpoch++;
93
96
  const previousPaste = this.pastePending;
94
97
  const wasPaste = previousPaste || data.includes("\x1b[200~");
@@ -100,10 +103,12 @@ export class AgentNavigationEditor extends CustomEditor {
100
103
  const cursor = this.getCursor();
101
104
  const text = this.getText();
102
105
  const lines = text.split("\n");
103
- const atBoundary = direction === "left"
104
- ? cursor.line === 0 && cursor.col === 0
105
- : direction === "right" && cursor.line === lines.length - 1 &&
106
- cursor.col === lines.at(-1)!.length;
106
+ const atBoundary =
107
+ direction === "left"
108
+ ? cursor.line === 0 && cursor.col === 0
109
+ : direction === "right" &&
110
+ cursor.line === lines.length - 1 &&
111
+ cursor.col === lines.at(-1)!.length;
107
112
  const eligible =
108
113
  outer &&
109
114
  direction !== undefined &&
@@ -170,9 +175,10 @@ export class AgentNavigationEditor extends CustomEditor {
170
175
  generation: unknown,
171
176
  direction: "left" | "right",
172
177
  ): void {
173
- const allowed = () => direction === "left"
174
- ? this.navigation.canOpen()
175
- : !!this.navigation.collapseWidget && (this.navigation.canCollapse?.() ?? false);
178
+ const allowed = () =>
179
+ direction === "left"
180
+ ? this.navigation.canOpen()
181
+ : !!this.navigation.collapseWidget && (this.navigation.canCollapse?.() ?? false);
176
182
  if (this.pending || !allowed()) return;
177
183
  this.pending = direction;
178
184
  const epoch = this.inputEpoch;
@@ -195,8 +201,9 @@ export class AgentNavigationEditor extends CustomEditor {
195
201
  try {
196
202
  // Call within the checked microtask: an extra Promise.then would leave
197
203
  // a gap in which root ownership could change after validation.
198
- void Promise.resolve(direction === "left"
199
- ? this.navigation.openTree() : this.navigation.collapseWidget!())
204
+ void Promise.resolve(
205
+ direction === "left" ? this.navigation.openTree() : this.navigation.collapseWidget!(),
206
+ )
200
207
  .catch((error: unknown) => this.navigation.onError?.(error))
201
208
  .finally(() => {
202
209
  this.pending = undefined;