@jwilger/pi-development-system 0.50.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.
- 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/index.ts +144 -60
- package/src/subagents/orch/inherited-tools.ts +3 -3
- 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 +13 -13
- package/src/subagents/prefs/config.ts +85 -185
- package/src/subagents/prefs/import-discovery.ts +6 -6
- 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/agent-navigation-editor.ts +20 -13
- package/src/subagents/ui/dialog.ts +99 -84
- package/src/subagents/ui/import-picker.ts +28 -11
- package/src/subagents/ui/live-agent-view.ts +17 -5
- 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/thread-tree.ts +3 -9
- 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.51.1",
|
|
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.*)$/;
|
|
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`.
|
package/src/subagents/index.ts
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
// @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { buildSessionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
3
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { buildSessionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { fuzzyFilter } from "@earendil-works/pi-tui";
|
|
6
|
-
import {
|
|
7
|
-
import { IMPORT_REQUEST_PREFIX, markImportOffered, offerAgentImport } from "./prefs/agent-import.ts";
|
|
6
|
+
import { createInheritedToolSource } from "./orch/inherited-tools.ts";
|
|
8
7
|
import { ThreadManager } from "./orch/manager.ts";
|
|
8
|
+
import { subagentPrompt } from "./orch/prompt.ts";
|
|
9
|
+
import { createDriverFactory } from "./orch/runtime.ts";
|
|
10
|
+
import { agentTools } from "./orch/tools.ts";
|
|
11
|
+
import {
|
|
12
|
+
IMPORT_REQUEST_PREFIX,
|
|
13
|
+
markImportOffered,
|
|
14
|
+
offerAgentImport,
|
|
15
|
+
} from "./prefs/agent-import.ts";
|
|
16
|
+
import { ConfigStore } from "./prefs/config.ts";
|
|
9
17
|
import { loadManagerSettings } from "./prefs/settings.ts";
|
|
18
|
+
import type { SavedThread, ThreadEvent } from "./types.ts";
|
|
19
|
+
import { AgentNavigationEditor } from "./ui/agent-navigation-editor.ts";
|
|
10
20
|
import { configureAgents } from "./ui/settings-ui.ts";
|
|
11
21
|
import { AgentNavigationController, showAgentStatus } from "./ui/status-ui.ts";
|
|
12
|
-
import { AgentNavigationEditor } from "./ui/agent-navigation-editor.ts";
|
|
13
|
-
import { createDriverFactory } from "./orch/runtime.ts";
|
|
14
|
-
import { createInheritedToolSource } from "./orch/inherited-tools.ts";
|
|
15
|
-
import { subagentPrompt } from "./orch/prompt.ts";
|
|
16
|
-
import { agentTools } from "./orch/tools.ts";
|
|
17
22
|
import { editAgentTypes, updateWidget } from "./ui/ui.ts";
|
|
18
|
-
import type { SavedThread, ThreadEvent } from "./types.ts";
|
|
19
23
|
|
|
20
24
|
const REGISTRY_ENTRY = "pi-subagent:registry:v1";
|
|
21
25
|
const ROOT_MAILBOX_ENTRY = "pi-subagent:root-mailbox:v1";
|
|
@@ -81,32 +85,55 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
81
85
|
if (existingFactory && existingFactory !== navigationEditorFactory) return;
|
|
82
86
|
// The host only transfers raw text, not cursor/undo/expanded-paste state.
|
|
83
87
|
if (ctx.ui.getEditorText?.()) return;
|
|
84
|
-
const history = buildSessionContext(ctx.sessionManager.getBranch()).messages.flatMap(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
const history = buildSessionContext(ctx.sessionManager.getBranch()).messages.flatMap(
|
|
89
|
+
(message) => {
|
|
90
|
+
if (message.role !== "user") return [];
|
|
91
|
+
const content = message.content;
|
|
92
|
+
return [
|
|
93
|
+
typeof content === "string"
|
|
94
|
+
? content
|
|
95
|
+
: content
|
|
96
|
+
.filter((block) => block.type === "text")
|
|
97
|
+
.map((block) => block.text)
|
|
98
|
+
.join(""),
|
|
99
|
+
];
|
|
100
|
+
},
|
|
101
|
+
);
|
|
90
102
|
navigationEditorFactory = (tui, theme, keys) => {
|
|
91
103
|
const editor = new AgentNavigationEditor(tui, theme, keys, {
|
|
92
104
|
// Initial startup hydrates after session_start; replacement editors do not.
|
|
93
105
|
initialHistory: seedHistory ? history : undefined,
|
|
94
106
|
generation: () => navigationGeneration,
|
|
95
|
-
canOpen: () =>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
canOpen: () =>
|
|
108
|
+
manager !== undefined &&
|
|
109
|
+
context?.mode === "tui" &&
|
|
110
|
+
limits.subagentMode !== "off" &&
|
|
111
|
+
!navigation.isOpen &&
|
|
112
|
+
manager
|
|
113
|
+
.list()
|
|
114
|
+
.some(
|
|
115
|
+
(thread) =>
|
|
116
|
+
thread.path !== "/root" &&
|
|
117
|
+
(thread.state === "starting" || thread.state === "running"),
|
|
118
|
+
),
|
|
119
|
+
openTree: () =>
|
|
120
|
+
manager && context
|
|
121
|
+
? navigation.open(context, manager.scope("/root"), undefined, limits.nerdFontIcons)
|
|
122
|
+
: undefined,
|
|
123
|
+
canCollapse: () =>
|
|
124
|
+
manager !== undefined &&
|
|
125
|
+
context?.mode === "tui" &&
|
|
126
|
+
limits.subagentMode !== "off" &&
|
|
127
|
+
!navigation.isOpen &&
|
|
128
|
+
!widgetCollapsed &&
|
|
129
|
+
limits.widgetMode === "full" &&
|
|
130
|
+
widgetThreads().some((thread) => thread.path !== "/root"),
|
|
104
131
|
collapseWidget: () => {
|
|
105
132
|
widgetCollapsed = true;
|
|
106
133
|
if (context) refreshWidget(context);
|
|
107
134
|
},
|
|
108
|
-
onError: (error) =>
|
|
109
|
-
error instanceof Error ? error.message : String(error), "error"),
|
|
135
|
+
onError: (error) =>
|
|
136
|
+
context?.ui.notify(error instanceof Error ? error.message : String(error), "error"),
|
|
110
137
|
});
|
|
111
138
|
navigationEditor = editor;
|
|
112
139
|
return editor;
|
|
@@ -120,7 +147,10 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
120
147
|
// Migration requires the root's file tools and must not delegate source prompts.
|
|
121
148
|
if (limits.subagentMode === "orchestration") {
|
|
122
149
|
if (!firstRun)
|
|
123
|
-
ctx.ui.notify(
|
|
150
|
+
ctx.ui.notify(
|
|
151
|
+
"Agent import requires Opportunistic mode. Change Subagent Mode in /agents settings.",
|
|
152
|
+
"info",
|
|
153
|
+
);
|
|
124
154
|
return;
|
|
125
155
|
}
|
|
126
156
|
if (importInProgress) return;
|
|
@@ -140,12 +170,15 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
140
170
|
if (!manager) throw new Error("Subagent threads are not initialized; start a Pi session first");
|
|
141
171
|
return manager;
|
|
142
172
|
};
|
|
143
|
-
const widgetThreads = () =>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
173
|
+
const widgetThreads = () =>
|
|
174
|
+
requireManager()
|
|
175
|
+
.list()
|
|
176
|
+
.filter((thread) => {
|
|
177
|
+
// Resuming a retained session makes it visible for this task, even after it settles again.
|
|
178
|
+
if (thread.state === "starting" || thread.state === "running")
|
|
179
|
+
hiddenWidgetThreads.delete(thread.path);
|
|
180
|
+
return !hiddenWidgetThreads.has(thread.path);
|
|
181
|
+
});
|
|
149
182
|
const requireContext = () => {
|
|
150
183
|
if (!context) throw new Error("No active Pi session");
|
|
151
184
|
return context;
|
|
@@ -187,44 +220,72 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
187
220
|
};
|
|
188
221
|
const pendingSummaries = (ctx: ExtensionContext) => {
|
|
189
222
|
const entries = ctx.sessionManager.getBranch();
|
|
190
|
-
const summarized = new Set(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
223
|
+
const summarized = new Set(
|
|
224
|
+
entries.flatMap((entry) => {
|
|
225
|
+
if (entry.type !== "custom_message" || entry.customType !== ROOT_SUMMARY_MESSAGE) return [];
|
|
226
|
+
const ids = (entry.details as { mailboxIds?: unknown } | undefined)?.mailboxIds;
|
|
227
|
+
return Array.isArray(ids) ? ids.filter((id): id is string => typeof id === "string") : [];
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
195
230
|
return entries.flatMap((entry) => {
|
|
196
231
|
if (entry.type !== "custom" || entry.customType !== ROOT_MAILBOX_ENTRY) return [];
|
|
197
232
|
const notification = entry.data as RootNotification | undefined;
|
|
198
233
|
return notification?.rootSessionId === ctx.sessionManager.getSessionId() &&
|
|
199
|
-
notification.finalRecap === true &&
|
|
234
|
+
notification.finalRecap === true &&
|
|
235
|
+
typeof notification.content === "string" &&
|
|
200
236
|
typeof notification.details?.mailboxId === "string" &&
|
|
201
|
-
!summarized.has(notification.details.mailboxId)
|
|
237
|
+
!summarized.has(notification.details.mailboxId)
|
|
238
|
+
? [notification]
|
|
239
|
+
: [];
|
|
202
240
|
});
|
|
203
241
|
};
|
|
204
242
|
const scheduleSummary = (ctx: ExtensionContext) => {
|
|
205
|
-
if (
|
|
206
|
-
|
|
243
|
+
if (
|
|
244
|
+
!limits.finalRecap ||
|
|
245
|
+
limits.subagentMode === "off" ||
|
|
246
|
+
summaryRunning ||
|
|
247
|
+
suspendingSummaries ||
|
|
248
|
+
summaryTimer !== undefined ||
|
|
249
|
+
ctx.isIdle() === false ||
|
|
250
|
+
!pendingSummaries(ctx).length
|
|
251
|
+
)
|
|
252
|
+
return;
|
|
207
253
|
const currentGeneration = generation;
|
|
208
254
|
// Coalesce siblings finishing together and leave the settlement boundary before
|
|
209
255
|
// starting a new turn. Recheck settings/session/idle state after the deferral.
|
|
210
256
|
summaryTimer = setTimeout(() => {
|
|
211
257
|
summaryTimer = undefined;
|
|
212
|
-
if (
|
|
213
|
-
|
|
258
|
+
if (
|
|
259
|
+
currentGeneration !== generation ||
|
|
260
|
+
context?.sessionManager !== ctx.sessionManager ||
|
|
261
|
+
!limits.finalRecap ||
|
|
262
|
+
limits.subagentMode === "off" ||
|
|
263
|
+
suspendingSummaries ||
|
|
264
|
+
ctx.isIdle() === false
|
|
265
|
+
)
|
|
266
|
+
return;
|
|
214
267
|
const notifications = pendingSummaries(ctx);
|
|
215
268
|
if (!notifications.length) return;
|
|
216
269
|
summaryRunning = true;
|
|
217
270
|
try {
|
|
218
|
-
pi.sendMessage(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
271
|
+
pi.sendMessage(
|
|
272
|
+
{
|
|
273
|
+
customType: ROOT_SUMMARY_MESSAGE,
|
|
274
|
+
content:
|
|
275
|
+
"Summarize the newly finished asynchronous subagent results for the user. " +
|
|
276
|
+
"Their results are in the preceding subagent notifications; use agent_output if more detail is needed. " +
|
|
277
|
+
"Give a concise main-thread answer covering results and any failures. " +
|
|
278
|
+
"Do not spawn or resume agents solely to summarize.\nAgents: " +
|
|
279
|
+
[...new Set(notifications.map((notification) => notification.details.path))].join(
|
|
280
|
+
", ",
|
|
281
|
+
),
|
|
282
|
+
display: false,
|
|
283
|
+
details: {
|
|
284
|
+
mailboxIds: notifications.map((notification) => notification.details.mailboxId),
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
{ triggerTurn: true },
|
|
288
|
+
);
|
|
228
289
|
} catch (error) {
|
|
229
290
|
summaryRunning = false;
|
|
230
291
|
warnDelivery(error);
|
|
@@ -278,7 +339,10 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
278
339
|
content: message,
|
|
279
340
|
// Foreground results already handled by a busy main thread do not need
|
|
280
341
|
// another turn. Completions during a summary do need a follow-up summary.
|
|
281
|
-
finalRecap:
|
|
342
|
+
finalRecap:
|
|
343
|
+
limits.finalRecap &&
|
|
344
|
+
!suspendingSummaries &&
|
|
345
|
+
event.kind === "settled" &&
|
|
282
346
|
(thread.state === "completed" || thread.state === "failed") &&
|
|
283
347
|
(requireContext().isIdle() || summaryRunning),
|
|
284
348
|
details: {
|
|
@@ -335,8 +399,14 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
335
399
|
const refreshWidget = (ctx: ExtensionContext) => {
|
|
336
400
|
if (limits.subagentMode === "off") {
|
|
337
401
|
if (ctx.hasUI) ctx.ui.setWidget("pi-subagent", undefined);
|
|
338
|
-
} else
|
|
339
|
-
|
|
402
|
+
} else
|
|
403
|
+
updateWidget(
|
|
404
|
+
ctx,
|
|
405
|
+
widgetThreads(),
|
|
406
|
+
widgetCollapsed ? "minimal" : limits.widgetMode,
|
|
407
|
+
limits.nerdFontIcons,
|
|
408
|
+
rootTurnEnded,
|
|
409
|
+
);
|
|
340
410
|
};
|
|
341
411
|
pi.on("agent_start", async (_event, ctx) => {
|
|
342
412
|
// A new task must not resurrect the previous task's settled agents. Automatic
|
|
@@ -374,7 +444,11 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
374
444
|
};
|
|
375
445
|
});
|
|
376
446
|
|
|
377
|
-
const attachSession = async (
|
|
447
|
+
const attachSession = async (
|
|
448
|
+
ctx: ExtensionContext,
|
|
449
|
+
installEditor = false,
|
|
450
|
+
seedHistory = true,
|
|
451
|
+
) => {
|
|
378
452
|
cancelSummary();
|
|
379
453
|
const token = ++generation;
|
|
380
454
|
closeNavigation(installEditor);
|
|
@@ -531,9 +605,19 @@ export default function piSubagent(pi: ExtensionAPI): void {
|
|
|
531
605
|
},
|
|
532
606
|
});
|
|
533
607
|
} else if (command === "status") {
|
|
534
|
-
await showAgentStatus(
|
|
608
|
+
await showAgentStatus(
|
|
609
|
+
ctx,
|
|
610
|
+
requireManager().scope("/root"),
|
|
611
|
+
undefined,
|
|
612
|
+
limits.nerdFontIcons,
|
|
613
|
+
);
|
|
535
614
|
} else if (command === "tree") {
|
|
536
|
-
await navigation.open(
|
|
615
|
+
await navigation.open(
|
|
616
|
+
ctx,
|
|
617
|
+
requireManager().scope("/root"),
|
|
618
|
+
rest.join(" ") || undefined,
|
|
619
|
+
limits.nerdFontIcons,
|
|
620
|
+
);
|
|
537
621
|
} else
|
|
538
622
|
ctx.ui.notify(
|
|
539
623
|
"Usage: /agents [tree [path] | status | settings | types | import | reload]",
|
|
@@ -43,7 +43,7 @@ export function createInheritedToolSource(pi: ExtensionAPI) {
|
|
|
43
43
|
for (const tool of loadout.registered) {
|
|
44
44
|
const exposure = loadout.getExposure(tool.name);
|
|
45
45
|
// Inactive direct tools and hidden tools are not available in the root either.
|
|
46
|
-
if (!callable.has(tool.name)
|
|
46
|
+
if (!(callable.has(tool.name) || active.has(tool.name))) continue;
|
|
47
47
|
if (exposure === "hidden") continue;
|
|
48
48
|
const metadata = info.get(tool.name);
|
|
49
49
|
tools.push({
|
|
@@ -64,14 +64,14 @@ export function createInheritedToolSource(pi: ExtensionAPI) {
|
|
|
64
64
|
// SDK orchestrators are instead recreated locally by runtime.
|
|
65
65
|
throw new Error(
|
|
66
66
|
`Cannot safely inherit model-only tool ${tool.name}: the SDK does not ` +
|
|
67
|
-
|
|
67
|
+
"provide a hook-preserving execution bridge. Run it in the main session.",
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
70
|
const bridge = rootContext ?? context;
|
|
71
71
|
if (!bridge) {
|
|
72
72
|
throw new Error(
|
|
73
73
|
"Inherited tool execution needs a main-session agent_* tool call first. " +
|
|
74
|
-
|
|
74
|
+
"Use agent_steer to resume this child instead of the thread dialog.",
|
|
75
75
|
);
|
|
76
76
|
}
|
|
77
77
|
const outcome = await bridge.executeTool(tool.name, args, { signal, onUpdate });
|
|
@@ -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
|
);
|