@jwilger/pi-development-system 0.50.0 → 0.52.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/package.json +2 -2
- package/skills/strict-lints/SKILL.md +2 -1
- package/src/core/spawn-overrides.ts +2 -2
- package/src/core/test-runner.ts +8 -2
- package/src/review/digest.ts +7 -1
- package/src/subagents/VENDORED.md +1 -1
- package/src/subagents/orch/mailbox.ts +11 -7
- package/src/subagents/orch/manager.ts +7 -1
- package/src/subagents/orch/runtime.ts +38 -33
- package/src/subagents/orch/tools.ts +12 -10
- package/src/subagents/prefs/agent-import.ts +15 -15
- package/src/subagents/prefs/config.ts +85 -185
- package/src/subagents/prefs/models.ts +6 -10
- package/src/subagents/prefs/settings.ts +38 -38
- package/src/subagents/types.ts +22 -19
- package/src/subagents/ui/dialog.ts +99 -84
- package/src/subagents/ui/import-picker.ts +28 -11
- package/src/subagents/ui/model-picker.ts +26 -17
- package/src/subagents/ui/settings-ui.ts +16 -34
- package/src/subagents/ui/status-ui.ts +140 -51
- package/src/subagents/ui/ui.ts +162 -265
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jwilger/pi-development-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "A pi extension package representing a seasoned approach to software development using a full AI SDLC.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"test:jev": "node --experimental-transform-types --no-warnings --test \"test/live/*.live.ts\"",
|
|
60
60
|
"check:jev": "node scripts/run-jev-fixtures.ts",
|
|
61
61
|
"typecheck": "tsc --noEmit",
|
|
62
|
-
"lint": "biome check .",
|
|
62
|
+
"lint": "biome check --error-on-warnings .",
|
|
63
63
|
"lint:fix": "biome check --write .",
|
|
64
64
|
"check:build-gate": "node scripts/build-gate.ts",
|
|
65
65
|
"check:version": "node scripts/check-version.ts",
|
|
@@ -39,7 +39,8 @@ A warning is an error that has not been triaged yet. Before every commit:
|
|
|
39
39
|
working; deny in CI.
|
|
40
40
|
- Vendored code under `src/subagents` is on a ratchet: `test/subagents/nocheck-files.ts`
|
|
41
41
|
lists the files still skipping type checks and may only shrink. A file you touch there
|
|
42
|
-
is made clean and dropped from the list
|
|
42
|
+
is made type-clean and lint-clean, dropped from the list, and removed from the
|
|
43
|
+
`!src/subagents/...` exclusions in `biome.json`.
|
|
43
44
|
|
|
44
45
|
## What the extension does
|
|
45
46
|
|
|
@@ -6,8 +6,8 @@ export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
|
6
6
|
|
|
7
7
|
/** Per-spawn choices the coordinator makes; they beat agent-type preferences and inheritance. */
|
|
8
8
|
export type SpawnOverrides = {
|
|
9
|
-
readonly model?: string;
|
|
10
|
-
readonly thinkingLevel?: ThinkingLevel;
|
|
9
|
+
readonly model?: string | undefined;
|
|
10
|
+
readonly thinkingLevel?: ThinkingLevel | undefined;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
export type Settings = {
|
package/src/core/test-runner.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { basename, isAssignment, segments, splitLines, WRAPPERS } from "./git-in
|
|
|
2
2
|
import { redactSecrets } from "./redact.ts";
|
|
3
3
|
|
|
4
4
|
const PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn", "bun"]);
|
|
5
|
-
const DIRECT_RUNNERS = new Set(["vitest", "jest", "pytest", "mocha", "ava"]);
|
|
5
|
+
const DIRECT_RUNNERS = new Set(["vitest", "jest", "pytest", "mocha", "ava", "tap"]);
|
|
6
6
|
const EXEC_PREFIX = new Set(["npx", "bunx", "pnpx"]);
|
|
7
7
|
const SUMMARY_LINES = 3;
|
|
8
8
|
const SUMMARY_MAX = 300;
|
|
@@ -63,6 +63,12 @@ export type ResultLike = {
|
|
|
63
63
|
|
|
64
64
|
/** A pipe or sequence makes the shell report the last command's status, not the runner's. */
|
|
65
65
|
const NO_TEST_SCRIPT = /Missing script: "?test/;
|
|
66
|
+
/** What npm prints around a missing script; any other line means something else ran or failed. */
|
|
67
|
+
const NPM_MISSING_SCRIPT_NOISE =
|
|
68
|
+
/^(?:npm (?:error|ERR!)\s*)?(?:$|.*Missing script.*|.*To see a list of scripts.*|.*A complete log of this run.*|.*Did you mean.*|.*npm run.*|Command exited with code \d+)$/;
|
|
69
|
+
const onlyMissingScript = (text: string): boolean =>
|
|
70
|
+
NO_TEST_SCRIPT.test(text) &&
|
|
71
|
+
text.split("\n").every((l) => NPM_MISSING_SCRIPT_NOISE.test(l.trim()));
|
|
66
72
|
const MASKS_STATUS = /[|;\n]/;
|
|
67
73
|
const FAILURE_MARKERS =
|
|
68
74
|
/^(?:# |ℹ )fail [1-9]|test result: FAILED|\b[1-9]\d* (?:failed|failing)\b|^FAILED\b|^FAIL\b|\bnot ok\b/m;
|
|
@@ -74,7 +80,7 @@ const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
|
74
80
|
export function exitCodeOf(result: ResultLike): number {
|
|
75
81
|
// `npm test` in a package without a test script exits 1 but ran no tests, so it is not a RED;
|
|
76
82
|
// but with --workspaces one package may lack the script while another fails for real.
|
|
77
|
-
if (
|
|
83
|
+
if (onlyMissingScript(result.text) && !FAILURE_MARKERS.test(result.text)) return 0;
|
|
78
84
|
const reported = reportedExit(result);
|
|
79
85
|
if (reported !== 0) return reported;
|
|
80
86
|
// `npm test | tail` exits 0 whatever the tests did, even in the structured status; trust the output.
|
package/src/review/digest.ts
CHANGED
|
@@ -102,7 +102,8 @@ export async function snapshotDiff(
|
|
|
102
102
|
const hashed = await hashUntracked(exec, cwd, untracked);
|
|
103
103
|
if (!hashed.ok) return hashed;
|
|
104
104
|
const files = { ...tracked.value, ...hashed.value };
|
|
105
|
-
|
|
105
|
+
// Code-unit order, not locale order: the digest must not depend on the runtime's collation.
|
|
106
|
+
const names = Object.keys(files).sort(byCodeUnit);
|
|
106
107
|
const untrackedStat = [
|
|
107
108
|
...untracked.map((p) => ` ${p} (untracked)`),
|
|
108
109
|
...nested.map((p) => ` ${p} (nested repository, not reviewable)`),
|
|
@@ -118,6 +119,11 @@ export async function snapshotDiff(
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
const byCodeUnit = (a: string, b: string): number => {
|
|
123
|
+
if (a === b) return 0;
|
|
124
|
+
return a < b ? -1 : 1;
|
|
125
|
+
};
|
|
126
|
+
|
|
121
127
|
/** Content digests (`new:<blob>`) of untracked files; symlinks are hashed by target, not followed. */
|
|
122
128
|
async function hashUntracked(
|
|
123
129
|
exec: Exec,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
## Local modifications (keep this list current)
|
|
11
11
|
|
|
12
|
-
1.
|
|
12
|
+
1. Ratchet. Upstream compiles under looser options than this repo's strict `tsconfig.json` (`exactOptionalPropertyTypes`, `erasableSyntaxOnly`). Files not yet retyped keep `// @ts-nocheck` and are excluded from biome in `biome.json`; both lists must match `test/subagents/nocheck-files.ts`, which may only shrink (`test/subagents/ratchet.test.ts`). Any file we touch is retyped, linted at the repo's strict biome rules and removed from the list. Retyped files that keep a rule violation carry a file-level `biome-ignore-all` with a reason.
|
|
13
13
|
2. Relative `.js` import specifiers were rewritten to `.ts` (upstream relies on a TypeScript-aware loader; this repo's tests run under Node's native type stripping).
|
|
14
14
|
3. `prefs/config.ts`: the bundled-agents directory is `../../../agents/` (was `../../agents/`) because the sources moved one level deeper.
|
|
15
15
|
4. Per-spawn pins (I5.2): `types.ts` `AgentType.spawnOverrides` and `ThreadService.spawn` args gain `model`/`thinkingLevel`; `orch/tools.ts` `agent_spawn` schema exposes both; `orch/manager.ts` `spawn` validates them with `src/core/spawn-overrides.ts` and stores them on the cloned type (so saved threads keep them); `orch/runtime.ts` `resolveInitialSettings` skips the type's preference list when a model is pinned (via `modelSource` in `src/core/spawn-overrides.ts`, so resumed threads keep their model), applies the pin last for fresh spawns, and `createDriver` drops model preferences/scope policy when a model is pinned; `ThreadView.pinned` (`types.ts`) is filled in `orch/manager.ts` `view()` so `agent_status` reports the pins. A pin therefore bypasses `/scoped-models`.
|
|
@@ -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 { randomUUID } from "node:crypto";
|
|
3
2
|
import type { SessionEntry, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
4
3
|
|
|
@@ -94,10 +93,12 @@ const normalizeLegacyQueue = (
|
|
|
94
93
|
const steering = value.steering;
|
|
95
94
|
const followUp = value.followUp;
|
|
96
95
|
if (
|
|
97
|
-
!
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
!(
|
|
97
|
+
Array.isArray(steering) &&
|
|
98
|
+
steering.every((text) => typeof text === "string") &&
|
|
99
|
+
Array.isArray(followUp) &&
|
|
100
|
+
followUp.every((text) => typeof text === "string")
|
|
101
|
+
)
|
|
101
102
|
) {
|
|
102
103
|
return undefined;
|
|
103
104
|
}
|
|
@@ -137,7 +138,10 @@ export class DurableMailbox {
|
|
|
137
138
|
private readonly consumed = new Set<string>();
|
|
138
139
|
private readonly enqueued = new Set<string>();
|
|
139
140
|
|
|
140
|
-
|
|
141
|
+
private readonly sessionManager: SessionManager;
|
|
142
|
+
|
|
143
|
+
constructor(sessionManager: SessionManager) {
|
|
144
|
+
this.sessionManager = sessionManager;
|
|
141
145
|
this.migrateLegacyQueue();
|
|
142
146
|
this.restore();
|
|
143
147
|
}
|
|
@@ -166,7 +170,7 @@ export class DurableMailbox {
|
|
|
166
170
|
|
|
167
171
|
private consumeAppended(messageEntryId: string, markerSource: unknown): void {
|
|
168
172
|
const id = getMailboxMarker(markerSource)?.id;
|
|
169
|
-
if (!id
|
|
173
|
+
if (!(id && this.accepted.has(id)) || this.consumed.has(id)) return;
|
|
170
174
|
this.sessionManager.appendCustomEntry(CONSUMED_TYPE, { id, messageEntryId });
|
|
171
175
|
this.consumed.add(id);
|
|
172
176
|
this.enqueued.delete(id);
|
|
@@ -788,10 +788,16 @@ export class ThreadManager {
|
|
|
788
788
|
while (this.records.size >= limit) {
|
|
789
789
|
const paths = [...this.records.keys()];
|
|
790
790
|
const victim = paths.find((path) => {
|
|
791
|
-
const
|
|
791
|
+
const held = this.records.get(path);
|
|
792
|
+
const state = held?.view.state;
|
|
792
793
|
const finished = state === "completed" || state === "failed" || state === "stopped";
|
|
794
|
+
// A driver still being created would land on a dropped record and never be disposed.
|
|
795
|
+
const opening =
|
|
796
|
+
held?.driver === undefined &&
|
|
797
|
+
(held?.initializing !== undefined || held?.contextReady !== undefined);
|
|
793
798
|
return (
|
|
794
799
|
finished &&
|
|
800
|
+
!opening &&
|
|
795
801
|
!isDescendant(spawning, path) &&
|
|
796
802
|
!paths.some((other) => isDescendant(other, path))
|
|
797
803
|
);
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
//
|
|
1
|
+
// biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: the flagged functions (driver construction, settings resolution) are long sequences over pi sessions; only the pin and resume paths are covered by test/subagents/spawn-pins.test.ts, so splitting them would move untested behaviour (see src/subagents/VENDORED.md)
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, realpath } from "node:fs/promises";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
4
6
|
import { clampThinkingLevel } from "@earendil-works/pi-ai/compat";
|
|
5
|
-
import { existsSync } from "node:fs";
|
|
6
|
-
import { mkdir, readFile, realpath } from "node:fs/promises";
|
|
7
7
|
import {
|
|
8
8
|
createAgentSession,
|
|
9
9
|
createCodemodeExtension,
|
|
10
10
|
createToolSearchExtension,
|
|
11
11
|
DefaultResourceLoader,
|
|
12
|
+
type ExtensionContext,
|
|
12
13
|
getAgentDir,
|
|
13
14
|
ModelRuntime,
|
|
14
15
|
SessionManager,
|
|
15
16
|
SettingsManager,
|
|
16
|
-
type ExtensionContext,
|
|
17
17
|
} from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import {
|
|
19
|
-
BOOTSTRAP_MESSAGE,
|
|
20
|
-
buildBootstrapMessage,
|
|
21
|
-
buildQueuedUserMessage,
|
|
22
|
-
buildUpdateDetails,
|
|
23
|
-
DurableMailbox,
|
|
24
|
-
} from "./mailbox.ts";
|
|
25
|
-
import { selectTools } from "../prefs/config.ts";
|
|
26
|
-
import { TranscriptChannel } from "./transcript.ts";
|
|
27
|
-
import type { InheritedToolSource } from "./inherited-tools.ts";
|
|
28
|
-
import type { ModelSelectionMode, ToolFilteringMode } from "../prefs/settings.ts";
|
|
29
18
|
import { applySpawnOverrides, modelSource } from "../../core/spawn-overrides.ts";
|
|
19
|
+
import { selectTools } from "../prefs/config.ts";
|
|
30
20
|
import {
|
|
31
|
-
modelIdentity,
|
|
32
21
|
getModelPreferences,
|
|
33
|
-
selectPreferredModel,
|
|
34
22
|
ModelPreferenceError,
|
|
23
|
+
modelIdentity,
|
|
24
|
+
selectPreferredModel,
|
|
35
25
|
} from "../prefs/models.ts";
|
|
26
|
+
import type { ModelSelectionMode, ToolFilteringMode } from "../prefs/settings.ts";
|
|
36
27
|
import {
|
|
37
|
-
THINKING_LEVELS,
|
|
38
28
|
type AgentType,
|
|
39
|
-
type ResolvedAgentSettings,
|
|
40
29
|
type DriverFactory,
|
|
41
30
|
type DriverOptions,
|
|
31
|
+
type ResolvedAgentSettings,
|
|
32
|
+
THINKING_LEVELS,
|
|
42
33
|
type ThinkingLevel,
|
|
43
34
|
} from "../types.ts";
|
|
35
|
+
import type { InheritedToolSource } from "./inherited-tools.ts";
|
|
36
|
+
import {
|
|
37
|
+
BOOTSTRAP_MESSAGE,
|
|
38
|
+
buildBootstrapMessage,
|
|
39
|
+
buildQueuedUserMessage,
|
|
40
|
+
buildUpdateDetails,
|
|
41
|
+
DurableMailbox,
|
|
42
|
+
} from "./mailbox.ts";
|
|
43
|
+
import { TranscriptChannel } from "./transcript.ts";
|
|
44
44
|
|
|
45
45
|
const BUILTINS = ["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"];
|
|
46
46
|
/** Persisted before transcript messages so older-leaf restores still carry ownership. */
|
|
@@ -157,7 +157,7 @@ export function createDriverFactory(
|
|
|
157
157
|
type: AgentType,
|
|
158
158
|
parentPath?: string | null,
|
|
159
159
|
restored?: ReturnType<SessionManager["buildSessionContext"]>,
|
|
160
|
-
): { provider?: string; id?: string; thinkingLevel: ThinkingLevel };
|
|
160
|
+
): { provider?: string | undefined; id?: string | undefined; thinkingLevel: ThinkingLevel };
|
|
161
161
|
} {
|
|
162
162
|
// Keep resolved settings even after disposal: descendants inherit settings, not the caller's history.
|
|
163
163
|
const resolved = new Map<string, { provider: string; id: string; thinking: ThinkingLevel }>();
|
|
@@ -226,9 +226,9 @@ export function createDriverFactory(
|
|
|
226
226
|
const candidates = filtering
|
|
227
227
|
? availableScopedModels(normalizeScopedModels(ctx.scopedModels))
|
|
228
228
|
: availableModelCandidates();
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
));
|
|
229
|
+
const preferred = selectTypeModelPreference(type, candidates, filtering);
|
|
230
|
+
if (preferred === undefined) throw new Error("No preferred model is available");
|
|
231
|
+
({ provider, id } = parseModelIdentity(preferred));
|
|
232
232
|
} else if (source === "restored" && restored?.model) {
|
|
233
233
|
provider = restored.model.provider;
|
|
234
234
|
id = restored.model.modelId;
|
|
@@ -252,7 +252,7 @@ export function createDriverFactory(
|
|
|
252
252
|
const { provider, id, thinkingLevel } = resolveInitialSettings(type, parentPath);
|
|
253
253
|
const model =
|
|
254
254
|
normalizeScopedModels(ctx.scopedModels).find(
|
|
255
|
-
(
|
|
255
|
+
(scoped) => scoped.model.provider === provider && scoped.model.id === id,
|
|
256
256
|
)?.model ?? (provider && id ? ctx.modelRegistry.find(provider, id) : undefined);
|
|
257
257
|
if (!model)
|
|
258
258
|
throw new Error(
|
|
@@ -381,14 +381,15 @@ export function createDriverFactory(
|
|
|
381
381
|
modelId: string | undefined,
|
|
382
382
|
scopedModels: readonly ScopedModel[],
|
|
383
383
|
) => {
|
|
384
|
-
if (!providerId
|
|
384
|
+
if (!(providerId && modelId))
|
|
385
385
|
throw new Error(
|
|
386
386
|
`Subagent model ${providerId ?? "(unset)"}/${modelId ?? "(unset)"} is unavailable; select a physical model in the root or agent type`,
|
|
387
387
|
);
|
|
388
388
|
await mirrorRuntimeAuth(providerId);
|
|
389
389
|
const sourceModel =
|
|
390
|
-
scopedModels.find(
|
|
391
|
-
|
|
390
|
+
scopedModels.find(
|
|
391
|
+
(scoped) => scoped.model.provider === providerId && scoped.model.id === modelId,
|
|
392
|
+
)?.model ?? ctx.modelRegistry.find(providerId, modelId);
|
|
392
393
|
if (sourceModel?.api === "pi-virtual") {
|
|
393
394
|
throw new Error(
|
|
394
395
|
`Virtual model ${providerId}/${modelId} cannot be reproduced through the public registry API. Set this agent type's model to a physical provider/model-id.`,
|
|
@@ -414,13 +415,16 @@ export function createDriverFactory(
|
|
|
414
415
|
const rootActive = new Set(inherited.activeNames);
|
|
415
416
|
// Preserve deferred/codemode exposure rather than flooding model declarations with MCP tools.
|
|
416
417
|
const activeToolNames = toolNames.filter(
|
|
417
|
-
(name) =>
|
|
418
|
+
(name) =>
|
|
419
|
+
localNames.has(name) ||
|
|
420
|
+
rootActive.has(name) ||
|
|
421
|
+
(options.type.tools?.allow?.includes(name) && toolFiltering === "allowed"),
|
|
418
422
|
);
|
|
419
423
|
const customTools = [...options.tools, ...externalTools].filter(
|
|
420
424
|
(tool) => allowed.has(tool.name) && tool.name !== "codemode" && tool.name !== "tool_search",
|
|
421
425
|
);
|
|
422
426
|
const settingsManager = SettingsManager.inMemory({ cacheWarming: "off" });
|
|
423
|
-
let parkQueue = () =>
|
|
427
|
+
let parkQueue: () => void = () => undefined;
|
|
424
428
|
const pauseBoundary = () => {
|
|
425
429
|
if (!options.shouldPause()) return undefined;
|
|
426
430
|
parkQueue();
|
|
@@ -558,7 +562,7 @@ export function createDriverFactory(
|
|
|
558
562
|
throw error;
|
|
559
563
|
}
|
|
560
564
|
const streamFunction = session.agent.streamFunction;
|
|
561
|
-
session.agent.streamFunction =
|
|
565
|
+
session.agent.streamFunction = (requestModel, context, streamOptions) => {
|
|
562
566
|
assertScopedRequestAuthorized(
|
|
563
567
|
modelIdentity(requestModel),
|
|
564
568
|
normalizeScopedModels(getRootContext().scopedModels),
|
|
@@ -579,7 +583,7 @@ export function createDriverFactory(
|
|
|
579
583
|
sessionManager.appendMessage(buildBootstrapMessage());
|
|
580
584
|
emit("activity", BOOTSTRAP_MESSAGE);
|
|
581
585
|
};
|
|
582
|
-
const queueAccepted =
|
|
586
|
+
const queueAccepted = (kind: "steer" | "followUp", content: string): Promise<void> => {
|
|
583
587
|
assertCurrentModelAuthorized();
|
|
584
588
|
ensurePersistedSession();
|
|
585
589
|
const accepted = mailbox.accept(kind, content);
|
|
@@ -587,6 +591,7 @@ export function createDriverFactory(
|
|
|
587
591
|
if (kind === "followUp") session.agent.followUp(buildQueuedUserMessage(accepted));
|
|
588
592
|
else session.agent.steer(buildQueuedUserMessage(accepted));
|
|
589
593
|
mailbox.markEnqueued(accepted.id);
|
|
594
|
+
return Promise.resolve();
|
|
590
595
|
};
|
|
591
596
|
const resumePending = async () => {
|
|
592
597
|
for (const accepted of mailbox.replayablePending()) {
|
|
@@ -641,7 +646,7 @@ export function createDriverFactory(
|
|
|
641
646
|
const usage = message.usage;
|
|
642
647
|
const input = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
643
648
|
const output = usage.output;
|
|
644
|
-
if (!Number.isFinite(input)
|
|
649
|
+
if (!(Number.isFinite(input) && Number.isFinite(output))) return;
|
|
645
650
|
return { input, output, reported: input + output + usage.totalTokens > 0 };
|
|
646
651
|
};
|
|
647
652
|
|
|
@@ -650,7 +655,7 @@ export function createDriverFactory(
|
|
|
650
655
|
.find((entry) => entry.type === "custom" && entry.customType === THREAD_OWNERSHIP_TYPE);
|
|
651
656
|
const inheritedMetadata =
|
|
652
657
|
ownership?.type === "custom"
|
|
653
|
-
? (ownership.data as { inheritedCount?: unknown })
|
|
658
|
+
? (ownership.data as { inheritedCount?: unknown }).inheritedCount
|
|
654
659
|
: undefined;
|
|
655
660
|
const inheritedCount =
|
|
656
661
|
typeof inheritedMetadata === "number" &&
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
// @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
|
|
2
|
-
import { Type } from "typebox";
|
|
3
1
|
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import
|
|
2
|
+
import { Type } from "typebox";
|
|
5
3
|
import { ModelPreferenceError } from "../prefs/models.ts";
|
|
6
4
|
import type { AgentType, ResolvedAgentSettings, ThreadView } from "../types.ts";
|
|
5
|
+
import type { ThreadManager } from "./manager.ts";
|
|
7
6
|
|
|
8
7
|
const path = Type.String({ description: "Absolute agent path, or name relative to the caller" });
|
|
9
8
|
const text = Type.String({ minLength: 1 });
|
|
@@ -30,8 +29,10 @@ export function agentTools(
|
|
|
30
29
|
const make = (
|
|
31
30
|
name: string,
|
|
32
31
|
description: string,
|
|
32
|
+
// biome-ignore-start lint/suspicious/noExplicitAny: the host validates params against the TypeBox schema before execute runs, and each handler reads only fields that schema declares
|
|
33
33
|
parameters: any,
|
|
34
34
|
execute: (params: any, signal?: AbortSignal) => Promise<any> | any,
|
|
35
|
+
// biome-ignore-end lint/suspicious/noExplicitAny: end of the schema-validated boundary
|
|
35
36
|
): ToolDefinition => ({
|
|
36
37
|
name,
|
|
37
38
|
label: name,
|
|
@@ -53,12 +54,7 @@ export function agentTools(
|
|
|
53
54
|
return {
|
|
54
55
|
name,
|
|
55
56
|
description,
|
|
56
|
-
error:
|
|
57
|
-
error instanceof ModelPreferenceError
|
|
58
|
-
? error.summary
|
|
59
|
-
: error instanceof Error
|
|
60
|
-
? error.message
|
|
61
|
-
: String(error),
|
|
57
|
+
error: describeError(error),
|
|
62
58
|
};
|
|
63
59
|
}
|
|
64
60
|
});
|
|
@@ -99,7 +95,8 @@ export function agentTools(
|
|
|
99
95
|
),
|
|
100
96
|
thinkingLevel: Type.Optional(
|
|
101
97
|
Type.String({
|
|
102
|
-
description:
|
|
98
|
+
description:
|
|
99
|
+
"Pin this child's thinking level: off, minimal, low, medium, high, xhigh or max.",
|
|
103
100
|
}),
|
|
104
101
|
),
|
|
105
102
|
}),
|
|
@@ -175,3 +172,8 @@ export function agentTools(
|
|
|
175
172
|
),
|
|
176
173
|
];
|
|
177
174
|
}
|
|
175
|
+
|
|
176
|
+
function describeError(error: unknown): string {
|
|
177
|
+
if (error instanceof ModelPreferenceError) return error.summary;
|
|
178
|
+
return error instanceof Error ? error.message : String(error);
|
|
179
|
+
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
import { constants, lstatSync, mkdirSync, openSync, closeSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { closeSync, constants, lstatSync, mkdirSync, openSync, writeFileSync } from "node:fs";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
5
4
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { dialogText } from "../ui/dialog.ts";
|
|
6
|
+
import { selectImportAgents } from "../ui/import-picker.ts";
|
|
6
7
|
import { AGENT_COLORS, type ConfigStore } from "./config.ts";
|
|
7
8
|
import { discoverImportCandidates, type ImportCandidate } from "./import-discovery.ts";
|
|
8
|
-
import { selectImportAgents } from "../ui/import-picker.ts";
|
|
9
9
|
import { MIGRATION_INSTRUCTIONS } from "./import-instructions.ts";
|
|
10
|
-
import { dialogText } from "../ui/dialog.ts";
|
|
11
10
|
|
|
12
11
|
export const IMPORT_REQUEST_PREFIX =
|
|
13
12
|
"Import ONLY the agents I selected in the pi-subagent-manager migration picker.";
|
|
@@ -70,7 +69,8 @@ function importWasOffered(agentDir: string): boolean {
|
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
export function markImportOffered(agentDir: string): void {
|
|
73
|
-
const directory = stateDirectory(agentDir, true)
|
|
72
|
+
const directory = stateDirectory(agentDir, true);
|
|
73
|
+
if (directory === undefined) throw new Error("Import-state directory could not be created");
|
|
74
74
|
const file = join(directory, OFFER_MARKER);
|
|
75
75
|
const stat = statIfPresent(file);
|
|
76
76
|
if (stat) {
|
|
@@ -94,11 +94,11 @@ function buildImportPrompt(options: {
|
|
|
94
94
|
agentDir: string;
|
|
95
95
|
cwd: string;
|
|
96
96
|
includeProject: boolean;
|
|
97
|
-
existingTypes: { name: string; source?: string; filePath?: string }[];
|
|
97
|
+
existingTypes: { name: string; source?: string | undefined; filePath?: string | undefined }[];
|
|
98
98
|
scopedModels: readonly string[];
|
|
99
|
-
scopedModelDetails?: readonly { identity: string; api: string; virtual: boolean }[];
|
|
100
|
-
parentModel?: string;
|
|
101
|
-
parentModelApi?: string;
|
|
99
|
+
scopedModelDetails?: readonly { identity: string; api: string; virtual: boolean }[] | undefined;
|
|
100
|
+
parentModel?: string | undefined;
|
|
101
|
+
parentModelApi?: string | undefined;
|
|
102
102
|
}): string {
|
|
103
103
|
return [
|
|
104
104
|
IMPORT_REQUEST_PREFIX,
|
|
@@ -159,10 +159,10 @@ export async function offerAgentImport(
|
|
|
159
159
|
cwd: ctx.cwd,
|
|
160
160
|
agentDir: options.agentDir,
|
|
161
161
|
includeProject: ctx.isProjectTrusted(),
|
|
162
|
-
homeDir: options.homeDir,
|
|
163
|
-
extraAgentDirs: options.extraAgentDirs,
|
|
162
|
+
...(options.homeDir === undefined ? {} : { homeDir: options.homeDir }),
|
|
163
|
+
...(options.extraAgentDirs === undefined ? {} : { extraAgentDirs: options.extraAgentDirs }),
|
|
164
164
|
});
|
|
165
|
-
if (found.diagnostics.length)
|
|
165
|
+
if (found.diagnostics.length > 0)
|
|
166
166
|
ctx.ui.notify(dialogText(found.diagnostics.join("\n")), "warning");
|
|
167
167
|
const remember = () => {
|
|
168
168
|
try {
|
|
@@ -174,9 +174,9 @@ export async function offerAgentImport(
|
|
|
174
174
|
);
|
|
175
175
|
}
|
|
176
176
|
};
|
|
177
|
-
if (
|
|
177
|
+
if (found.candidates.length === 0) {
|
|
178
178
|
// Retry an incomplete scan next startup, rather than permanently hiding unreadable definitions.
|
|
179
|
-
if (
|
|
179
|
+
if (found.diagnostics.length === 0) remember();
|
|
180
180
|
if (!firstRun)
|
|
181
181
|
ctx.ui.notify(
|
|
182
182
|
"No external agent definitions found. Package-provided defaults are not scanned.",
|
|
@@ -208,7 +208,7 @@ export async function offerAgentImport(
|
|
|
208
208
|
}
|
|
209
209
|
const selected = new Set(ids);
|
|
210
210
|
const candidates = found.candidates.filter((candidate) => selected.has(candidate.id));
|
|
211
|
-
if (
|
|
211
|
+
if (candidates.length === 0) return false;
|
|
212
212
|
options.store.reload();
|
|
213
213
|
const prompt = buildImportPrompt({
|
|
214
214
|
candidates,
|