pi-pstack-adapter 0.7.0 → 0.9.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/CHANGELOG.md +20 -0
- package/README.md +80 -1
- package/VERSION +1 -1
- package/adapter-skills/setup.md +71 -10
- package/docs/compatibility.md +173 -16
- package/index.ts +246 -20
- package/package.json +1 -1
- package/src/certified.ts +3 -3
- package/src/config.ts +190 -11
- package/src/filter.ts +150 -0
- package/src/gate.ts +139 -0
- package/src/generate.ts +74 -12
- package/src/pools.ts +104 -0
- package/src/registry.ts +39 -9
- package/src/status.ts +198 -18
- package/src/vocabulary.ts +123 -0
package/index.ts
CHANGED
|
@@ -7,7 +7,17 @@ import { Type } from "typebox";
|
|
|
7
7
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { registerDelegationActivation } from "./src/activation.ts";
|
|
9
9
|
import { registerAgents, unregisteredAgentDiagnostics, type RuntimeRegistration } from "./src/agents.ts";
|
|
10
|
-
import { DEFAULT_DELEGATION } from "./src/delegation.ts";
|
|
10
|
+
import { DEFAULT_DELEGATION, SUBAGENT_TOOL, T3_DELEGATE_TOOL } from "./src/delegation.ts";
|
|
11
|
+
import {
|
|
12
|
+
FILTER_ENTRY_TYPE,
|
|
13
|
+
type ModelFilter,
|
|
14
|
+
activeFilter,
|
|
15
|
+
describeOrigin,
|
|
16
|
+
filterOf,
|
|
17
|
+
parseModelFilter,
|
|
18
|
+
sessionFilterFromEntries,
|
|
19
|
+
} from "./src/filter.ts";
|
|
20
|
+
import { gateDelegation } from "./src/gate.ts";
|
|
11
21
|
import { checkPrerequisites, detectCapabilities, type ToolDescriptor } from "./src/capabilities.ts";
|
|
12
22
|
import {
|
|
13
23
|
type AdapterConfig,
|
|
@@ -21,6 +31,8 @@ import {
|
|
|
21
31
|
joinModelSpec,
|
|
22
32
|
mergeConfig,
|
|
23
33
|
planModelAssignment,
|
|
34
|
+
planPanelAssignment,
|
|
35
|
+
planPoolAssignment,
|
|
24
36
|
readConfigFile,
|
|
25
37
|
trustWarning,
|
|
26
38
|
updateConfigFile,
|
|
@@ -30,8 +42,16 @@ import { safeEcho } from "./src/echo.ts";
|
|
|
30
42
|
import { MODE_ENTRY_TYPE, modeFromEntries, modeInstruction } from "./src/mode.ts";
|
|
31
43
|
import { CONFIG_FILE_NAME, agentDir, cacheRoot } from "./src/paths.ts";
|
|
32
44
|
import { bashToolContext, probeExecutables } from "./src/executables.ts";
|
|
33
|
-
import { entryFor } from "./src/registry.ts";
|
|
34
|
-
import {
|
|
45
|
+
import { SKILL_PANELS, entryFor, isPanelRole } from "./src/registry.ts";
|
|
46
|
+
import {
|
|
47
|
+
platformSupport,
|
|
48
|
+
renderCheck,
|
|
49
|
+
renderModelPolicy,
|
|
50
|
+
renderPanels,
|
|
51
|
+
renderStatus,
|
|
52
|
+
shellDiagnostics,
|
|
53
|
+
type ShellState,
|
|
54
|
+
} from "./src/status.ts";
|
|
35
55
|
import { BootstrapRequiredError, resolveRefToCommit, resolveSource } from "./src/upstream.ts";
|
|
36
56
|
import { normalizeRepo } from "./src/config.ts";
|
|
37
57
|
import type { CapabilityStatus, Diagnostic, GeneratedAgent, GenerationResult, ResolvedSource } from "./src/types.ts";
|
|
@@ -55,6 +75,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
55
75
|
let agentDiagnostics: readonly Diagnostic[] = [];
|
|
56
76
|
let unregisteredAgents: readonly GeneratedAgent[] = [];
|
|
57
77
|
let modeActive = false;
|
|
78
|
+
/** This session's own model filter, restored from the active branch. It wins over a saved one. */
|
|
79
|
+
let sessionFilter: ModelFilter | undefined;
|
|
80
|
+
let savedFilter: AdapterConfig["modelFilter"];
|
|
81
|
+
/** Why the config files could not be read, so the saved filter is unknown. */
|
|
82
|
+
let configError: string | undefined;
|
|
58
83
|
const commandNamespace = startupNamespace();
|
|
59
84
|
|
|
60
85
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -62,9 +87,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
62
87
|
state = undefined;
|
|
63
88
|
startupError = undefined;
|
|
64
89
|
modeActive = modeFromEntries(ctx.sessionManager.getBranch() as never[]);
|
|
90
|
+
sessionFilter = sessionFilterFromEntries(ctx.sessionManager.getBranch() as never[]);
|
|
91
|
+
savedFilter = undefined;
|
|
92
|
+
configError = undefined;
|
|
65
93
|
|
|
94
|
+
let config: AdapterConfig;
|
|
66
95
|
try {
|
|
67
|
-
|
|
96
|
+
config = loadConfig(ctx);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
startupError = error as Error;
|
|
99
|
+
try {
|
|
100
|
+
savedFilter = loadConfig(ctx, true).modelFilter;
|
|
101
|
+
} catch (filterError) {
|
|
102
|
+
configError = (filterError as Error).message;
|
|
103
|
+
}
|
|
104
|
+
if (ctx.hasUI) ctx.ui.notify(`pstack adapter: ${(error as Error).message}`, "error");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
savedFilter = config.modelFilter;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
state = await bringUpUpstream(config, ctx);
|
|
68
111
|
} catch (error) {
|
|
69
112
|
startupError = error as Error;
|
|
70
113
|
if (ctx.hasUI) ctx.ui.notify(`pstack adapter: ${(error as Error).message}`, "error");
|
|
@@ -101,6 +144,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
101
144
|
}
|
|
102
145
|
});
|
|
103
146
|
|
|
147
|
+
// `/tree` moves the active branch, and with it the session filter that branch recorded.
|
|
148
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
149
|
+
sessionFilter = sessionFilterFromEntries(ctx.sessionManager.getBranch() as never[]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// The model filter applies to every delegation in this session, pstack or not, because a
|
|
153
|
+
// delegation tool call names no pstack role. A blocked call fails; nothing retries it.
|
|
154
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
155
|
+
if (event.toolName !== T3_DELEGATE_TOOL && event.toolName !== SUBAGENT_TOOL) return;
|
|
156
|
+
if (sessionFilter === undefined && configError !== undefined) {
|
|
157
|
+
return {
|
|
158
|
+
block: true,
|
|
159
|
+
reason: `Blocked by the pstack adapter: its config could not be read, so it cannot tell whether a saved model filter applies. ${configError} Fix the config and reload, or ask the user whether to set an explicit session filter with \`set_filter\`.`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const decision = gateDelegation(event.toolName, event.input, {
|
|
163
|
+
filter: filterOf(activeFilter(sessionFilter, savedFilter)),
|
|
164
|
+
parentModel: parentModelOf(ctx),
|
|
165
|
+
resolveModel: (model) => resolveRegistryModel(ctx, model),
|
|
166
|
+
});
|
|
167
|
+
if (decision.kind === "block") return { block: true, reason: decision.reason };
|
|
168
|
+
});
|
|
169
|
+
|
|
104
170
|
pi.on("resources_discover", async () => {
|
|
105
171
|
if (!state) return {};
|
|
106
172
|
return { skillPaths: [state.generation.skillsDir] };
|
|
@@ -126,7 +192,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
126
192
|
name: "pstack_adapter",
|
|
127
193
|
label: "pstack adapter",
|
|
128
194
|
description:
|
|
129
|
-
"Inspect and configure the pstack adapter. Use `status` for the full report, `check` before starting a pstack workflow, `models` to list real Pi model ids, `set_model` to assign a pstack role, and `set_source` to select another upstream revision.",
|
|
195
|
+
"Inspect and configure the pstack adapter. Use `status` for the full report, `check` before starting a pstack workflow, `models` to list real Pi model ids, `set_model` to assign a pstack role or a pool of models, `set_filter` to restrict which models delegation may use, and `set_source` to select another upstream revision.",
|
|
130
196
|
parameters: Type.Object({
|
|
131
197
|
action: Type.Union(
|
|
132
198
|
[
|
|
@@ -134,13 +200,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
134
200
|
Type.Literal("check"),
|
|
135
201
|
Type.Literal("models"),
|
|
136
202
|
Type.Literal("set_model"),
|
|
203
|
+
Type.Literal("set_filter"),
|
|
137
204
|
Type.Literal("set_source"),
|
|
138
205
|
],
|
|
139
206
|
{ description: "Operation to run" },
|
|
140
207
|
),
|
|
141
208
|
skill: Type.Optional(Type.String({ description: "Skill name for `check`, with or without the namespace prefix" })),
|
|
142
209
|
role: Type.Optional(Type.String({ description: "pstack model role for `set_model`" })),
|
|
143
|
-
model: Type.Optional(
|
|
210
|
+
model: Type.Optional(
|
|
211
|
+
Type.Union([Type.String(), Type.Array(Type.String())], {
|
|
212
|
+
description:
|
|
213
|
+
"`provider/model` id, or `inherit` to clear the role. A list of ids gives an ordinary role a pool: each delegation picks one. For a panel role such as `arena-runners`, a string of entries separated by commas, one seat each.",
|
|
214
|
+
}),
|
|
215
|
+
),
|
|
144
216
|
thinking: Type.Optional(
|
|
145
217
|
Type.String({
|
|
146
218
|
description: `Thinking level for \`set_model\`: ${THINKING_LEVELS.join(", ")}. Omit to pass no level, which also clears a level set earlier.`,
|
|
@@ -149,9 +221,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
149
221
|
repo: Type.Optional(Type.String({ description: "Upstream repository URL for `set_source`" })),
|
|
150
222
|
ref: Type.Optional(Type.String({ description: "Branch, tag, or commit for `set_source`" })),
|
|
151
223
|
localPath: Type.Optional(Type.String({ description: "Local pstack checkout for `set_source`" })),
|
|
224
|
+
include: Type.Optional(
|
|
225
|
+
Type.Array(Type.String(), {
|
|
226
|
+
description: "`set_filter`: patterns a delegation model must match, such as `gpt-*`. `*` is the only wildcard; a pattern with `/` matches `provider/model`.",
|
|
227
|
+
}),
|
|
228
|
+
),
|
|
229
|
+
exclude: Type.Optional(Type.Array(Type.String(), { description: "`set_filter`: patterns no delegation model may match, such as `claude-*`. Exclude wins." })),
|
|
230
|
+
clear: Type.Optional(Type.Boolean({ description: "`set_filter`: remove the filter at `scope` instead of setting one." })),
|
|
152
231
|
scope: Type.Optional(
|
|
153
|
-
Type.Union([Type.Literal("user"), Type.Literal("project")], {
|
|
154
|
-
description: "
|
|
232
|
+
Type.Union([Type.Literal("session"), Type.Literal("user"), Type.Literal("project")], {
|
|
233
|
+
description: "`set_filter` defaults to session, this session only. `set_model` and `set_source` write a config file: user (default) or project.",
|
|
155
234
|
}),
|
|
156
235
|
),
|
|
157
236
|
}),
|
|
@@ -205,15 +284,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
205
284
|
action: string;
|
|
206
285
|
skill?: string;
|
|
207
286
|
role?: string;
|
|
208
|
-
model?: string;
|
|
287
|
+
model?: string | string[];
|
|
209
288
|
thinking?: string;
|
|
210
289
|
repo?: string;
|
|
211
290
|
ref?: string;
|
|
212
291
|
localPath?: string;
|
|
213
|
-
|
|
292
|
+
include?: string[];
|
|
293
|
+
exclude?: string[];
|
|
294
|
+
clear?: boolean;
|
|
295
|
+
scope?: "session" | "user" | "project";
|
|
214
296
|
},
|
|
215
297
|
ctx: ExtensionContext,
|
|
216
298
|
): Promise<string> {
|
|
299
|
+
// A restriction must be settable even when the upstream checkout did not resolve.
|
|
300
|
+
if (params.action === "set_filter") return setFilter(params, params.scope ?? "session", ctx);
|
|
217
301
|
if (startupError) return `The pstack adapter did not start: ${startupError.message}`;
|
|
218
302
|
if (!state) return "The pstack adapter has not resolved its upstream checkout yet.";
|
|
219
303
|
|
|
@@ -233,6 +317,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
233
317
|
modeActive,
|
|
234
318
|
cacheRoot: state.cacheRoot,
|
|
235
319
|
bootstrapped: state.bootstrapped,
|
|
320
|
+
filter: activeFilter(sessionFilter, savedFilter),
|
|
321
|
+
parentModel: parentModelOf(ctx),
|
|
236
322
|
runtimeDiagnostics: [
|
|
237
323
|
...agentDiagnostics,
|
|
238
324
|
// Judged now, because T3's tools can connect after the agents were registered.
|
|
@@ -245,10 +331,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
245
331
|
return checkSkill(params.skill ?? "", ctx);
|
|
246
332
|
case "models":
|
|
247
333
|
return renderModels(ctx);
|
|
248
|
-
case "set_model":
|
|
334
|
+
case "set_model": {
|
|
335
|
+
if (params.scope === "session") return SESSION_SCOPE_ONLY_FILTER;
|
|
249
336
|
return setModel(params.role, params.model, params.thinking, params.scope ?? "user", ctx);
|
|
250
|
-
|
|
337
|
+
}
|
|
338
|
+
case "set_source": {
|
|
339
|
+
if (params.scope === "session") return SESSION_SCOPE_ONLY_FILTER;
|
|
251
340
|
return setSource(params, params.scope ?? "user", ctx);
|
|
341
|
+
}
|
|
252
342
|
default:
|
|
253
343
|
return `Unknown action ${params.action}.`;
|
|
254
344
|
}
|
|
@@ -275,11 +365,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
275
365
|
const shell: ShellState = { ...resolved, runError: executables.shellFailure };
|
|
276
366
|
const shellProblems = shellDiagnostics(shell).map((diagnostic) => `${diagnostic.message} ${diagnostic.action ?? ""}`.trim());
|
|
277
367
|
const current = capabilities();
|
|
368
|
+
const panels = Object.hasOwn(SKILL_PANELS, skill.upstreamName) ? SKILL_PANELS[skill.upstreamName] : undefined;
|
|
369
|
+
const origin = activeFilter(sessionFilter, savedFilter);
|
|
370
|
+
const parentModel = parentModelOf(ctx);
|
|
278
371
|
return renderCheck(
|
|
279
372
|
skill,
|
|
280
373
|
entryFor(skill.upstreamName, skill.generatedName.slice(prefix.length)),
|
|
281
374
|
checkPrerequisites(skill.capabilities, current, skill.executables, executables.has, shellProblems),
|
|
282
375
|
current.find((capability) => capability.id === "delegation"),
|
|
376
|
+
panels ? renderPanels(state.config, panels, { filter: filterOf(origin), parentModel }) : undefined,
|
|
377
|
+
renderModelPolicy(state.config, origin, parentModel),
|
|
283
378
|
);
|
|
284
379
|
}
|
|
285
380
|
|
|
@@ -293,17 +388,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
293
388
|
|
|
294
389
|
function setModel(
|
|
295
390
|
role: string | undefined,
|
|
296
|
-
model: string | undefined,
|
|
391
|
+
model: string | string[] | undefined,
|
|
297
392
|
thinking: string | undefined,
|
|
298
393
|
scope: "user" | "project",
|
|
299
394
|
ctx: ExtensionContext,
|
|
300
395
|
): string {
|
|
301
396
|
if (!role) return "set_model needs a `role`.";
|
|
302
|
-
if (!model) return "set_model needs a `model`, or `inherit` to clear the role.";
|
|
397
|
+
if (!model || model.length === 0) return "set_model needs a `model`, or `inherit` to clear the role.";
|
|
303
398
|
if (!isValidRole(role)) return invalidRoleMessage(role);
|
|
304
399
|
const available = ctx.modelRegistry
|
|
305
400
|
.getAvailable()
|
|
306
401
|
.map((candidate) => ({ id: `${candidate.provider}/${candidate.id}`, reasoning: candidate.reasoning }));
|
|
402
|
+
if (Array.isArray(model)) {
|
|
403
|
+
if (isPanelRole(role)) {
|
|
404
|
+
return `Panel \`${role}\` takes one string of entries separated by commas, one seat each, not a list. A list is a pool, which only ordinary roles have.`;
|
|
405
|
+
}
|
|
406
|
+
return setPool(role, model, thinking, available, scope, ctx);
|
|
407
|
+
}
|
|
408
|
+
if (isPanelRole(role)) return setPanel(role, model, thinking, available, scope, ctx);
|
|
307
409
|
const plan = planModelAssignment(model, thinking, available);
|
|
308
410
|
if (plan.kind === "rejected") return plan.message;
|
|
309
411
|
const path = configPath(scope, ctx);
|
|
@@ -312,10 +414,117 @@ export default function (pi: ExtensionAPI) {
|
|
|
312
414
|
if (plan.kind === "inherit") delete models[role];
|
|
313
415
|
else models[role] = joinModelSpec(plan.spec);
|
|
314
416
|
updateConfigFile(path, { models });
|
|
315
|
-
|
|
417
|
+
const note = refreshModels(scope, ctx);
|
|
418
|
+
if (plan.kind === "inherit") return `Role \`${role}\` now inherits the parent session model and thinking. Written to ${path}.${note}`;
|
|
316
419
|
return plan.spec.thinking === undefined
|
|
317
|
-
? `Role \`${role}\` now uses \`${plan.spec.model}\` at the model's default thinking. Written to ${path}
|
|
318
|
-
: `Role \`${role}\` now uses \`${plan.spec.model}\` with thinking \`${plan.spec.thinking}\`. Written to ${path}
|
|
420
|
+
? `Role \`${role}\` now uses \`${plan.spec.model}\` at the model's default thinking. Written to ${path}.${note}`
|
|
421
|
+
: `Role \`${role}\` now uses \`${plan.spec.model}\` with thinking \`${plan.spec.thinking}\`. Written to ${path}.${note}`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Status and `check` read roles from `state`, so a written role takes effect now. Only `models`
|
|
426
|
+
* moves: the source and generation stay as they were until the next start. Returns a note for
|
|
427
|
+
* the reply when the session view does not match what was written.
|
|
428
|
+
*/
|
|
429
|
+
function refreshModels(scope: "user" | "project", ctx: ExtensionContext): string {
|
|
430
|
+
const untrusted =
|
|
431
|
+
scope === "project" && !ctx.isProjectTrusted()
|
|
432
|
+
? " This project is not trusted, so Pi ignores the file until you trust it, and the status report still shows the earlier value."
|
|
433
|
+
: "";
|
|
434
|
+
try {
|
|
435
|
+
const fresh = loadConfig(ctx);
|
|
436
|
+
savedFilter = fresh.modelFilter;
|
|
437
|
+
configError = undefined;
|
|
438
|
+
if (state) state = { ...state, config: { ...state.config, models: fresh.models, modelFilter: fresh.modelFilter } };
|
|
439
|
+
} catch (error) {
|
|
440
|
+
try {
|
|
441
|
+
savedFilter = loadConfig(ctx, true).modelFilter;
|
|
442
|
+
configError = undefined;
|
|
443
|
+
} catch (filterError) {
|
|
444
|
+
configError = (filterError as Error).message;
|
|
445
|
+
}
|
|
446
|
+
return `${untrusted} The write succeeded, but the model role view was not refreshed: ${(error as Error).message}`;
|
|
447
|
+
}
|
|
448
|
+
return untrusted;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function setPool(
|
|
452
|
+
role: string,
|
|
453
|
+
entries: readonly string[],
|
|
454
|
+
thinking: string | undefined,
|
|
455
|
+
available: readonly { id: string; reasoning?: boolean }[],
|
|
456
|
+
scope: "user" | "project",
|
|
457
|
+
ctx: ExtensionContext,
|
|
458
|
+
): string {
|
|
459
|
+
const plan = planPoolAssignment(entries, thinking, available);
|
|
460
|
+
if (plan.kind === "rejected") return plan.message;
|
|
461
|
+
const path = configPath(scope, ctx);
|
|
462
|
+
const stored = readConfigFile(path)?.values.models;
|
|
463
|
+
const models = typeof stored === "object" && stored !== null && !Array.isArray(stored) ? { ...stored } : {};
|
|
464
|
+
models[role] = plan.value;
|
|
465
|
+
updateConfigFile(path, { models });
|
|
466
|
+
const note = refreshModels(scope, ctx);
|
|
467
|
+
const listed = (typeof plan.value === "string" ? [plan.value] : plan.value).map((entry) => `\`${entry}\``).join(", ");
|
|
468
|
+
return typeof plan.value === "string"
|
|
469
|
+
? `Role \`${role}\` now uses ${listed}. A pool of one is written as a plain model. Written to ${path}.${note}`
|
|
470
|
+
: `Role \`${role}\` now has a pool of ${plan.value.length} models: ${listed}. Each delegation picks one model the filter allows. Written to ${path}.${note}`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function setFilter(
|
|
474
|
+
params: { include?: string[]; exclude?: string[]; clear?: boolean },
|
|
475
|
+
scope: "session" | "user" | "project",
|
|
476
|
+
ctx: ExtensionContext,
|
|
477
|
+
): string {
|
|
478
|
+
const setting = params.include !== undefined || params.exclude !== undefined;
|
|
479
|
+
if (params.clear && setting) return "set_filter takes either `clear: true` or `include` and `exclude`, not both.";
|
|
480
|
+
if (!params.clear && !setting) {
|
|
481
|
+
return "set_filter needs `include`, `exclude`, or `clear: true`. Empty lists set an unrestricted filter at that scope.";
|
|
482
|
+
}
|
|
483
|
+
let filter: ModelFilter | undefined;
|
|
484
|
+
if (setting) {
|
|
485
|
+
const raw: Record<string, unknown> = {};
|
|
486
|
+
if (params.include !== undefined) raw.include = params.include;
|
|
487
|
+
if (params.exclude !== undefined) raw.exclude = params.exclude;
|
|
488
|
+
const parsed = parseModelFilter(raw);
|
|
489
|
+
if ("problem" in parsed) return parsed.problem;
|
|
490
|
+
filter = parsed.filter;
|
|
491
|
+
}
|
|
492
|
+
if (scope === "session") {
|
|
493
|
+
sessionFilter = filter;
|
|
494
|
+
pi.appendEntry(FILTER_ENTRY_TYPE, filter ? { filter } : { cleared: true });
|
|
495
|
+
const origin = activeFilter(sessionFilter, savedFilter);
|
|
496
|
+
return `${filter ? "This session's model filter is set" : "This session's model filter is cleared"}. Filter in force: ${describeOrigin(origin)}. It applies to every \`delegate_task\` and \`subagent\` call in this session, and is not saved to a config file.`;
|
|
497
|
+
}
|
|
498
|
+
const path = configPath(scope, ctx);
|
|
499
|
+
updateConfigFile(path, { modelFilter: filter });
|
|
500
|
+
const note = refreshModels(scope, ctx);
|
|
501
|
+
const origin = activeFilter(sessionFilter, savedFilter);
|
|
502
|
+
const shadowed =
|
|
503
|
+
sessionFilter !== undefined
|
|
504
|
+
? " This session's own filter still wins over the saved one. Clear it with `set_filter`, `clear: true`, and `scope: \"session\"` to use the saved filter here."
|
|
505
|
+
: "";
|
|
506
|
+
return `${filter ? "Saved model filter" : "Removed the saved model filter"} in ${path}.${note}${shadowed} Filter in force: ${describeOrigin(origin)}.`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function setPanel(
|
|
510
|
+
role: string,
|
|
511
|
+
model: string,
|
|
512
|
+
thinking: string | undefined,
|
|
513
|
+
available: readonly { id: string; reasoning?: boolean }[],
|
|
514
|
+
scope: "user" | "project",
|
|
515
|
+
ctx: ExtensionContext,
|
|
516
|
+
): string {
|
|
517
|
+
const plan = planPanelAssignment(model, thinking, available);
|
|
518
|
+
if (plan.kind === "rejected") return plan.message;
|
|
519
|
+
const path = configPath(scope, ctx);
|
|
520
|
+
const stored = readConfigFile(path)?.values.models;
|
|
521
|
+
const models = typeof stored === "object" && stored !== null && !Array.isArray(stored) ? { ...stored } : {};
|
|
522
|
+
if (plan.kind === "inherit") delete models[role];
|
|
523
|
+
else models[role] = plan.value;
|
|
524
|
+
updateConfigFile(path, { models });
|
|
525
|
+
const note = refreshModels(scope, ctx);
|
|
526
|
+
if (plan.kind === "inherit") return `Panel \`${role}\` is unset again, so it uses \`judgment\`, then \`general\`. Written to ${path}.${note}`;
|
|
527
|
+
return `Panel \`${role}\` now runs one seat each on ${plan.value.split(" ").map((entry) => `\`${entry}\``).join(", ")}. Written to ${path}.${note}`;
|
|
319
528
|
}
|
|
320
529
|
|
|
321
530
|
async function setSource(
|
|
@@ -393,7 +602,24 @@ function startupNamespace(): string {
|
|
|
393
602
|
}
|
|
394
603
|
}
|
|
395
604
|
|
|
396
|
-
|
|
605
|
+
const SESSION_SCOPE_ONLY_FILTER =
|
|
606
|
+
"Only `set_filter` takes `scope: \"session\"`. `set_model` and `set_source` write a config file, so pass `scope: \"user\"` or `scope: \"project\"`.";
|
|
607
|
+
|
|
608
|
+
function parentModelOf(ctx: ExtensionContext): string | undefined {
|
|
609
|
+
return ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** A requested model as its registry `provider/id`. A bare id counts only when exactly one model has it. */
|
|
613
|
+
function resolveRegistryModel(ctx: ExtensionContext, model: string): string | undefined {
|
|
614
|
+
const ids = ctx.modelRegistry.getAvailable().map((candidate) => ({ full: `${candidate.provider}/${candidate.id}`, id: candidate.id }));
|
|
615
|
+
const exact = ids.find((candidate) => candidate.full === model);
|
|
616
|
+
if (exact) return exact.full;
|
|
617
|
+
if (model.includes("/")) return undefined;
|
|
618
|
+
const bare = ids.filter((candidate) => candidate.id === model);
|
|
619
|
+
return bare.length === 1 ? bare[0]?.full : undefined;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function loadConfig(ctx: ExtensionContext, filterOnly = false): AdapterConfig {
|
|
397
623
|
const layers: ConfigLayer[] = [];
|
|
398
624
|
const global = readConfigFile(userConfigPath());
|
|
399
625
|
if (global) layers.push(global);
|
|
@@ -403,8 +629,8 @@ function loadConfig(ctx: ExtensionContext): AdapterConfig {
|
|
|
403
629
|
// without desynchronizing `/<ns>-status` from the generated `<ns>-*` skills.
|
|
404
630
|
if (project) layers.push({ ...project, values: { ...project.values, namespace: undefined } });
|
|
405
631
|
}
|
|
406
|
-
layers.push(environmentLayer(process.env));
|
|
407
|
-
return mergeConfig(layers);
|
|
632
|
+
layers.push(environmentLayer(filterOnly ? { ...process.env, PISTACK_MODELS: undefined } : process.env));
|
|
633
|
+
return mergeConfig(filterOnly ? layers.map((layer) => ({ ...layer, values: { modelFilter: layer.values.modelFilter } })) : layers);
|
|
408
634
|
}
|
|
409
635
|
|
|
410
636
|
function configPath(scope: "user" | "project", ctx: ExtensionContext): string {
|
package/package.json
CHANGED
package/src/certified.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
export const CERTIFIED = {
|
|
6
6
|
repo: "https://github.com/cursor/plugins",
|
|
7
7
|
ref: "main",
|
|
8
|
-
commit: "
|
|
8
|
+
commit: "c742dd61706fbacc1b95f1a20b7b988cf096ce6c",
|
|
9
9
|
pluginPath: "pstack",
|
|
10
|
-
pstackVersion: "0.15.
|
|
11
|
-
skillCount:
|
|
10
|
+
pstackVersion: "0.15.36",
|
|
11
|
+
skillCount: 58,
|
|
12
12
|
agentCount: 2,
|
|
13
13
|
} as const;
|