@mccune1224/pi-pstack 0.1.0 → 0.2.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/README.md +52 -100
- package/agents/comment-sicko.md +1 -1
- package/agents/poteto-agent.md +2 -2
- package/docs/compare.md +122 -0
- package/docs/guide/01-setup.md +37 -12
- package/docs/guide/02-poteto-mode.md +1 -1
- package/docs/guide/03-understand.md +0 -8
- package/docs/guide/05-build-and-clean.md +1 -1
- package/docs/guide/06-verify-and-ship.md +1 -1
- package/docs/guide/07-overnight.md +2 -2
- package/docs/guide/08-principles.md +4 -2
- package/docs/guide/10-recipes-and-pitfalls.md +2 -2
- package/docs/guide/README.md +2 -2
- package/extensions/compaction.test.ts +132 -0
- package/extensions/compaction.ts +183 -0
- package/extensions/context-guard.test.ts +53 -0
- package/extensions/context-guard.ts +65 -0
- package/extensions/doctor.test.ts +85 -0
- package/extensions/doctor.ts +100 -0
- package/extensions/index.ts +289 -26
- package/extensions/settings.test.ts +71 -16
- package/extensions/settings.ts +57 -9
- package/extensions/skill-budget.test.ts +110 -0
- package/extensions/skill-budget.ts +101 -0
- package/package.json +9 -4
- package/scripts/check-pi-isms.mjs +106 -0
- package/scripts/lib/pi-delta-rules.mjs +110 -0
- package/scripts/lib/pi-delta-rules.test.mjs +100 -0
- package/scripts/lib/upstream-vendor.mjs +32 -0
- package/scripts/pi-deltas.json +754 -0
- package/scripts/sync-upstream.mjs +117 -0
- package/scripts/verify-guards.mjs +132 -0
- package/scripts/verify-sync.mjs +206 -0
- package/skills/architect/SKILL.md +10 -10
- package/skills/architect/references/rationale-template.md +2 -2
- package/skills/architect/references/runner-prompt.md +7 -7
- package/skills/arena/SKILL.md +11 -11
- package/skills/automate-me/SKILL.md +21 -26
- package/skills/blast-radius/SKILL.md +5 -5
- package/skills/create-verification-skill/SKILL.md +5 -5
- package/skills/figure-it-out/SKILL.md +11 -13
- package/skills/how/SKILL.md +30 -104
- package/skills/how/references/explainer-prompt.md +6 -6
- package/skills/how/references/explorer-prompt.md +1 -1
- package/skills/interrogate/SKILL.md +13 -14
- package/skills/interrogate/references/code-quality-review.md +1 -1
- package/skills/interrogate/references/lead-judgment.md +1 -1
- package/skills/interrogate/references/rubric.md +2 -2
- package/skills/maintain-verification-skill/SKILL.md +5 -5
- package/skills/no-comments/SKILL.md +3 -3
- package/skills/poteto-mode/SKILL.md +9 -8
- package/skills/poteto-mode/playbooks/authoring-a-skill.md +4 -4
- package/skills/poteto-mode/playbooks/autonomous-run.md +4 -4
- package/skills/poteto-mode/playbooks/autopilot-full.md +9 -9
- package/skills/poteto-mode/playbooks/autopilot-stack.md +8 -8
- package/skills/poteto-mode/playbooks/babysit.md +17 -17
- package/skills/poteto-mode/playbooks/bug-fix.md +6 -6
- package/skills/poteto-mode/playbooks/eval.md +6 -8
- package/skills/poteto-mode/playbooks/feature.md +7 -7
- package/skills/poteto-mode/playbooks/hillclimb.md +10 -10
- package/skills/poteto-mode/playbooks/investigation.md +3 -3
- package/skills/poteto-mode/playbooks/multi-phase-plan.md +26 -25
- package/skills/poteto-mode/playbooks/opening-a-pr.md +17 -13
- package/skills/poteto-mode/playbooks/orchestrate.md +37 -39
- package/skills/poteto-mode/playbooks/pause-safely.md +4 -4
- package/skills/poteto-mode/playbooks/perf-issue.md +9 -9
- package/skills/poteto-mode/playbooks/prototype.md +5 -5
- package/skills/poteto-mode/playbooks/refactoring.md +9 -9
- package/skills/poteto-mode/playbooks/runtime-forensics.md +3 -3
- package/skills/poteto-mode/playbooks/session-pickup.md +4 -6
- package/skills/poteto-mode/playbooks/shipping.md +11 -14
- package/skills/poteto-mode/playbooks/trace-forensics.md +4 -4
- package/skills/poteto-mode/playbooks/visual-parity.md +3 -3
- package/skills/poteto-mode/playbooks/worktree-cleanup.md +4 -4
- package/skills/poteto-mode/references/bugbot-triage.md +3 -3
- package/skills/poteto-mode/scripts/worktree-audit.mjs +0 -7
- package/skills/principle-attack-the-premise/SKILL.md +23 -0
- package/skills/principle-boundary-discipline/SKILL.md +2 -2
- package/skills/principle-build-the-lever/SKILL.md +5 -5
- package/skills/principle-encode-lessons-in-structure/SKILL.md +3 -3
- package/skills/principle-experience-first/SKILL.md +5 -5
- package/skills/principle-fix-root-causes/SKILL.md +4 -4
- package/skills/principle-foundational-thinking/SKILL.md +3 -3
- package/skills/principle-guard-the-context-window/SKILL.md +2 -2
- package/skills/principle-laziness-protocol/SKILL.md +2 -2
- package/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +1 -1
- package/skills/principle-minimize-reader-load/SKILL.md +2 -2
- package/skills/principle-model-the-domain/SKILL.md +3 -3
- package/skills/principle-never-block-on-the-human/SKILL.md +4 -5
- package/skills/principle-prove-it-works/SKILL.md +3 -3
- package/skills/principle-redesign-from-first-principles/SKILL.md +3 -3
- package/skills/principle-separate-before-serializing-shared-state/SKILL.md +3 -3
- package/skills/principle-sequence-verifiable-units/SKILL.md +3 -3
- package/skills/principle-subtract-before-you-add/SKILL.md +3 -4
- package/skills/principle-test-behavior-not-implementation/SKILL.md +25 -0
- package/skills/principle-type-system-discipline/SKILL.md +5 -5
- package/skills/recall/SKILL.md +7 -7
- package/skills/reflect/SKILL.md +14 -20
- package/skills/reflect/references/divergent-reviewer.md +3 -3
- package/skills/reflect/references/judgment-reviewer.md +3 -3
- package/skills/reflect/references/synthesizer.md +2 -2
- package/skills/reflect/references/tooling-reviewer.md +4 -6
- package/skills/setup-pstack/SKILL.md +61 -49
- package/skills/show-me-your-work/SKILL.md +17 -17
- package/skills/show-me-your-work/scripts/log.mjs +2 -8
- package/skills/swarm/SKILL.md +6 -6
- package/skills/tdd/SKILL.md +2 -2
- package/skills/teach/SKILL.md +7 -7
- package/skills/technical-writing/SKILL.md +5 -8
- package/skills/typescript-best-practices/SKILL.md +6 -3
- package/skills/typescript-best-practices/references/patterns.md +35 -14
- package/skills/unslop/SKILL.md +8 -20
- package/skills/why/SKILL.md +27 -100
- package/skills/why/references/epistemics.md +6 -6
- package/skills/why/references/investigator-prompt.md +5 -5
- package/skills/why/references/source-playbook.md +1 -1
- package/skills/why/references/sources/databricks.md +7 -7
- package/skills/why/references/sources/datadog.md +5 -5
- package/skills/why/references/sources/incident-postmortem.md +2 -2
- package/skills/why/references/sources/linear.md +2 -2
- package/skills/why/references/sources/notion.md +3 -3
- package/skills/why/references/sources/sentry.md +4 -4
- package/skills/why/references/sources/slack.md +2 -2
- package/skills/why/references/synthesizer-prompt.md +3 -3
- package/skills/how/references/critic-prompt.md +0 -59
- package/skills/how/references/critique-rubric.md +0 -58
package/extensions/index.ts
CHANGED
|
@@ -1,17 +1,48 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import {
|
|
4
|
+
CONFIG_DIR_NAME,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionCommandContext,
|
|
7
|
+
type ExtensionContext,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { analyzeSkillBudget, readSkillSources } from "./skill-budget.ts";
|
|
10
|
+
import {
|
|
11
|
+
POTETO_COMPACT_INSTRUCTIONS,
|
|
12
|
+
loadConversationHelpers,
|
|
13
|
+
runPotetoCompaction,
|
|
14
|
+
} from "./compaction.ts";
|
|
15
|
+
import {
|
|
16
|
+
type DoctorCheck,
|
|
17
|
+
checkAgentFrontmatter,
|
|
18
|
+
checkModelMap,
|
|
19
|
+
renderReport,
|
|
20
|
+
} from "./doctor.ts";
|
|
21
|
+
import {
|
|
22
|
+
guardAdvice,
|
|
23
|
+
guardLevel,
|
|
24
|
+
isStronger,
|
|
25
|
+
meterLine,
|
|
26
|
+
type GuardLevel,
|
|
27
|
+
} from "./context-guard.ts";
|
|
28
|
+
import {
|
|
29
|
+
BUDGETS,
|
|
3
30
|
MODE_ENTRY_TYPE,
|
|
4
31
|
POTETO_DIRECTIVE,
|
|
5
32
|
ROLE_KEYS,
|
|
6
33
|
availableModels,
|
|
34
|
+
applyBudget,
|
|
7
35
|
buildTier,
|
|
8
36
|
lastModeEnabled,
|
|
9
37
|
loadSettings,
|
|
10
38
|
parseArgs,
|
|
11
39
|
parseModelId,
|
|
40
|
+
resolvedMapping,
|
|
12
41
|
saveSettings,
|
|
13
42
|
settingsPath,
|
|
14
43
|
summaryText,
|
|
44
|
+
budgetLabel,
|
|
45
|
+
type BudgetKey,
|
|
15
46
|
type ModeEntry,
|
|
16
47
|
type ModelMap,
|
|
17
48
|
type ModelSpec,
|
|
@@ -19,6 +50,22 @@ import {
|
|
|
19
50
|
type Scope,
|
|
20
51
|
} from "./settings.ts";
|
|
21
52
|
|
|
53
|
+
const SKILLS_DIR = join(import.meta.dirname, "..", "skills");
|
|
54
|
+
const AGENTS_DIR = join(import.meta.dirname, "..", "agents");
|
|
55
|
+
const DELTAS_PATH = join(import.meta.dirname, "..", "scripts", "pi-deltas.json");
|
|
56
|
+
|
|
57
|
+
const PACKAGE_AGENTS = ["poteto-agent.md", "comment-sicko.md"];
|
|
58
|
+
|
|
59
|
+
const PLAYBOOK_COMPLETIONS: Array<{ value: string; label: string }> = (() => {
|
|
60
|
+
try {
|
|
61
|
+
return readdirSync(join(SKILLS_DIR, "poteto-mode", "playbooks"))
|
|
62
|
+
.filter((file) => file.endsWith(".md"))
|
|
63
|
+
.map((file) => ({ value: file.replace(/\.md$/, ""), label: file.replace(/\.md$/, "") }));
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
68
|
+
|
|
22
69
|
const SETUP_COMPLETIONS = [
|
|
23
70
|
"-l",
|
|
24
71
|
"--local",
|
|
@@ -27,6 +74,10 @@ const SETUP_COMPLETIONS = [
|
|
|
27
74
|
"all-inherit",
|
|
28
75
|
"light",
|
|
29
76
|
"custom",
|
|
77
|
+
"unlimited",
|
|
78
|
+
"large",
|
|
79
|
+
"medium",
|
|
80
|
+
"small",
|
|
30
81
|
"default",
|
|
31
82
|
"scout",
|
|
32
83
|
"worker",
|
|
@@ -101,6 +152,21 @@ function pstackSkillNames(commands: Array<{ name: string; source: string }>): st
|
|
|
101
152
|
.sort();
|
|
102
153
|
}
|
|
103
154
|
|
|
155
|
+
function effectiveModelMap(cwd: string): { map: ModelMap; scope: Scope | "none"; path: string } {
|
|
156
|
+
const userPath = settingsPath(cwd, "user", CONFIG_DIR_NAME);
|
|
157
|
+
const projectPath = settingsPath(cwd, "project", CONFIG_DIR_NAME);
|
|
158
|
+
const projectMap = loadSettings(projectPath);
|
|
159
|
+
if (Object.keys(projectMap).length > 0) {
|
|
160
|
+
return { map: projectMap, scope: "project", path: projectPath };
|
|
161
|
+
}
|
|
162
|
+
const userMap = loadSettings(userPath);
|
|
163
|
+
return {
|
|
164
|
+
map: userMap,
|
|
165
|
+
scope: Object.keys(userMap).length > 0 ? "user" : "none",
|
|
166
|
+
path: userPath,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
104
170
|
function modelChoices(ctx: ExtensionCommandContext, currentValue: string) {
|
|
105
171
|
return availableModels(ctx.modelRegistry.getAvailable()).map((choice) =>
|
|
106
172
|
choice.id === currentValue ? `${choice.label} [current]` : choice.label,
|
|
@@ -124,19 +190,34 @@ async function announceAndReload(
|
|
|
124
190
|
|
|
125
191
|
async function pstackSetupHandler(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
126
192
|
const parsed = parseArgs(args);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
193
|
+
let scope: Scope | undefined = parsed.scope;
|
|
194
|
+
if (!scope) {
|
|
195
|
+
const label = ctx.hasUI
|
|
130
196
|
? await ctx.ui.select("Where to save the pstack model map?", [
|
|
131
197
|
"User (~/.pi/agent/settings.json) - recommended",
|
|
132
198
|
"Project (.pi/settings.json) - team shared",
|
|
133
199
|
])
|
|
134
|
-
: "user"
|
|
135
|
-
|
|
136
|
-
|
|
200
|
+
: "user";
|
|
201
|
+
if (!label) return;
|
|
202
|
+
scope = label.startsWith("Project") ? "project" : "user";
|
|
203
|
+
}
|
|
137
204
|
const path = settingsPath(ctx.cwd, scope, CONFIG_DIR_NAME);
|
|
138
205
|
const current = loadSettings(path);
|
|
139
206
|
|
|
207
|
+
// Fast paths (-l, a tier, or a bare role) skip the budget question, mirroring
|
|
208
|
+
// upstream's budget-first flow only when the run is fully interactive.
|
|
209
|
+
const hasFastPath = parsed.tier !== undefined || parsed.role !== undefined;
|
|
210
|
+
let budget: BudgetKey | undefined = parsed.budget;
|
|
211
|
+
if (!budget && ctx.hasUI && !hasFastPath) {
|
|
212
|
+
const pick = await ctx.ui.select(
|
|
213
|
+
`Reasoning budget for roles without their own level - now ${budgetLabel(current)}`,
|
|
214
|
+
Object.entries(BUDGETS).map(([key, spec]) => `${key} - ${spec.blurb}`),
|
|
215
|
+
);
|
|
216
|
+
if (!pick) return;
|
|
217
|
+
budget = pick.split(" ")[0] as BudgetKey;
|
|
218
|
+
}
|
|
219
|
+
const withBudget = (map: ModelMap): ModelMap => (budget ? applyBudget(map, budget) : map);
|
|
220
|
+
|
|
140
221
|
if (parsed.role) {
|
|
141
222
|
const currentValue =
|
|
142
223
|
parsed.role === "default"
|
|
@@ -157,8 +238,8 @@ async function pstackSetupHandler(args: string, ctx: ExtensionCommandContext): P
|
|
|
157
238
|
[parsed.role]: { ...current.agentOverrides?.[parsed.role], model: id },
|
|
158
239
|
};
|
|
159
240
|
}
|
|
160
|
-
saveSettings(path, map);
|
|
161
|
-
await announceAndReload(ctx, path, map, scope);
|
|
241
|
+
saveSettings(path, withBudget(map));
|
|
242
|
+
await announceAndReload(ctx, path, withBudget(map), scope);
|
|
162
243
|
return;
|
|
163
244
|
}
|
|
164
245
|
|
|
@@ -167,7 +248,7 @@ async function pstackSetupHandler(args: string, ctx: ExtensionCommandContext): P
|
|
|
167
248
|
ctx.ui.notify(summaryText(current, "none", []), "info");
|
|
168
249
|
const pick = await ctx.ui.select("Pick a tier", [
|
|
169
250
|
"All inherit (cheapest) - every role uses the parent model (Recommended)",
|
|
170
|
-
"Light tier (scout low,
|
|
251
|
+
"Light tier (scout low thinking, every model still inherits the parent)",
|
|
171
252
|
"Custom per role",
|
|
172
253
|
]);
|
|
173
254
|
if (!pick) return;
|
|
@@ -199,6 +280,7 @@ async function pstackSetupHandler(args: string, ctx: ExtensionCommandContext): P
|
|
|
199
280
|
} else {
|
|
200
281
|
map = buildTier(tier);
|
|
201
282
|
}
|
|
283
|
+
map = withBudget(map);
|
|
202
284
|
saveSettings(path, map);
|
|
203
285
|
await announceAndReload(ctx, path, map, scope);
|
|
204
286
|
}
|
|
@@ -208,16 +290,13 @@ async function pstackStatusHandler(
|
|
|
208
290
|
_args: string,
|
|
209
291
|
ctx: ExtensionCommandContext,
|
|
210
292
|
): Promise<void> {
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
const userMap = loadSettings(userPath);
|
|
214
|
-
const projectMap = loadSettings(projectPath);
|
|
215
|
-
const projectWins = Object.keys(projectMap).length > 0;
|
|
216
|
-
const effective = projectWins ? projectMap : userMap;
|
|
217
|
-
const scope = projectWins ? "project" : Object.keys(userMap).length > 0 ? "user" : "none";
|
|
293
|
+
const effective = effectiveModelMap(ctx.cwd);
|
|
294
|
+
const budget = analyzeSkillBudget(readSkillSources(SKILLS_DIR));
|
|
218
295
|
const text =
|
|
219
|
-
summaryText(effective, scope, pstackSkillNames(pi.getCommands())) +
|
|
220
|
-
`\nsettings: ${
|
|
296
|
+
summaryText(effective.map, effective.scope, pstackSkillNames(pi.getCommands())) +
|
|
297
|
+
`\nsettings: ${effective.path}` +
|
|
298
|
+
`\nprompt cost: ${budget.modelInvocable} of ${budget.total} pstack skills exposed to the model,` +
|
|
299
|
+
` ${budget.filtered} filtered by disable-model-invocation, ~${budget.estimatedTokens} tokens`;
|
|
221
300
|
const parent =
|
|
222
301
|
ctx.model && typeof ctx.model === "object"
|
|
223
302
|
? `${(ctx.model as { provider?: string }).provider}/${(ctx.model as { id?: string }).id}`
|
|
@@ -252,6 +331,106 @@ function textContent(content: unknown): string | undefined {
|
|
|
252
331
|
return parts.length > 0 ? parts.join("\n") : undefined;
|
|
253
332
|
}
|
|
254
333
|
|
|
334
|
+
function upstreamPin(): { ref: string; version: string } | undefined {
|
|
335
|
+
try {
|
|
336
|
+
const spec = JSON.parse(readFileSync(DELTAS_PATH, "utf8")) as {
|
|
337
|
+
upstream?: { ref?: string; version?: string };
|
|
338
|
+
};
|
|
339
|
+
if (!spec.upstream?.ref || !spec.upstream.version) return undefined;
|
|
340
|
+
return { ref: spec.upstream.ref, version: spec.upstream.version };
|
|
341
|
+
} catch {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function doctorChecks(pi: ExtensionAPI, ctx: ExtensionCommandContext): DoctorCheck[] {
|
|
347
|
+
const checks: DoctorCheck[] = [];
|
|
348
|
+
|
|
349
|
+
for (const scope of ["user", "project"] as const) {
|
|
350
|
+
const path = settingsPath(ctx.cwd, scope, CONFIG_DIR_NAME);
|
|
351
|
+
if (!existsSync(path)) {
|
|
352
|
+
checks.push({ name: `settings (${scope})`, status: "ok", detail: `${path} absent, nothing pinned` });
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const problems = checkModelMap(loadSettings(path), ROLE_KEYS);
|
|
356
|
+
checks.push({
|
|
357
|
+
name: `settings (${scope})`,
|
|
358
|
+
status: problems.length > 0 ? "warn" : "ok",
|
|
359
|
+
detail: problems.length > 0 ? problems.join("; ") : `${path} parses clean`,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (const file of PACKAGE_AGENTS) {
|
|
364
|
+
const path = join(AGENTS_DIR, file);
|
|
365
|
+
try {
|
|
366
|
+
const problems = checkAgentFrontmatter(file, readFileSync(path, "utf8"));
|
|
367
|
+
checks.push({
|
|
368
|
+
name: `agent ${file}`,
|
|
369
|
+
status: problems.length > 0 ? "fail" : "ok",
|
|
370
|
+
detail: problems.length > 0 ? problems.join("; ") : "frontmatter registers with pi-subagents",
|
|
371
|
+
});
|
|
372
|
+
} catch {
|
|
373
|
+
checks.push({ name: `agent ${file}`, status: "fail", detail: `${path} is missing` });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
const playbooks = readdirSync(join(SKILLS_DIR, "poteto-mode", "playbooks")).filter((f) => f.endsWith(".md"));
|
|
379
|
+
checks.push({
|
|
380
|
+
name: "skills",
|
|
381
|
+
status: existsSync(join(SKILLS_DIR, "poteto-mode", "SKILL.md")) ? "ok" : "fail",
|
|
382
|
+
detail: `poteto-mode present, ${playbooks.length} playbooks vendored under skills/`,
|
|
383
|
+
});
|
|
384
|
+
} catch {
|
|
385
|
+
checks.push({ name: "skills", status: "fail", detail: "skills/poteto-mode is missing" });
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const toolNames = new Set(pi.getAllTools().map((tool) => tool.name));
|
|
389
|
+
for (const companion of COMPANIONS) {
|
|
390
|
+
const present = toolNames.has(companion.tool);
|
|
391
|
+
checks.push({
|
|
392
|
+
name: `companion ${companion.tool}`,
|
|
393
|
+
status: present ? "ok" : companion.required ? "fail" : "warn",
|
|
394
|
+
detail: present ? "registered" : `absent: install ${companion.pkg}`,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const pin = upstreamPin();
|
|
399
|
+
checks.push({
|
|
400
|
+
name: "upstream pin",
|
|
401
|
+
status: pin ? "ok" : "warn",
|
|
402
|
+
detail: pin
|
|
403
|
+
? `pstack ${pin.version} at ${pin.ref.slice(0, 12)}; npm run verify:sync proves the port matches`
|
|
404
|
+
: "scripts/pi-deltas.json unreadable",
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const effective = effectiveModelMap(ctx.cwd);
|
|
408
|
+
const parent =
|
|
409
|
+
ctx.model && typeof ctx.model === "object"
|
|
410
|
+
? `${(ctx.model as { provider?: string }).provider ?? "?"}/${(ctx.model as { id?: string }).id ?? "?"}`
|
|
411
|
+
: "unknown";
|
|
412
|
+
checks.push({
|
|
413
|
+
name: "session",
|
|
414
|
+
status: "ok",
|
|
415
|
+
detail: `parent model ${parent}, map ${effective.scope}, ${budgetLabel(effective.map)}`,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
return checks;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function pstackDoctorHandler(
|
|
422
|
+
pi: ExtensionAPI,
|
|
423
|
+
_args: string,
|
|
424
|
+
ctx: ExtensionCommandContext,
|
|
425
|
+
): Promise<void> {
|
|
426
|
+
const report = renderReport(doctorChecks(pi, ctx));
|
|
427
|
+
if (ctx.hasUI) {
|
|
428
|
+
ctx.ui.notify(report, "info");
|
|
429
|
+
} else {
|
|
430
|
+
console.log(report);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
255
434
|
async function broHandler(
|
|
256
435
|
pi: ExtensionAPI,
|
|
257
436
|
_args: string,
|
|
@@ -274,6 +453,8 @@ async function broHandler(
|
|
|
274
453
|
|
|
275
454
|
export default function (pi: ExtensionAPI): void {
|
|
276
455
|
let potetoMode = false;
|
|
456
|
+
let guardNotified: GuardLevel = "ok";
|
|
457
|
+
let conversationHelpers: Awaited<ReturnType<typeof loadConversationHelpers>> | "unavailable" | undefined;
|
|
277
458
|
|
|
278
459
|
function sessionEntries(ctx: {
|
|
279
460
|
sessionManager: { getBranch?: () => ModeEntry[]; getEntries: () => ModeEntry[] };
|
|
@@ -283,20 +464,37 @@ export default function (pi: ExtensionAPI): void {
|
|
|
283
464
|
: ctx.sessionManager.getEntries();
|
|
284
465
|
}
|
|
285
466
|
|
|
286
|
-
function
|
|
467
|
+
function widgetLines(ctx: ExtensionContext): string[] {
|
|
468
|
+
const map = effectiveModelMap(ctx.cwd).map;
|
|
469
|
+
const roles = resolvedMapping(map, ROLE_KEYS);
|
|
470
|
+
const inherited = roles.filter((role) => role.model === "inherit").length;
|
|
471
|
+
const summary =
|
|
472
|
+
inherited === roles.length
|
|
473
|
+
? `${inherited} inherit`
|
|
474
|
+
: `${inherited}/${roles.length} inherit`;
|
|
475
|
+
return [
|
|
476
|
+
`pstack: poteto mode on | roles: ${summary}`,
|
|
477
|
+
meterLine(ctx.getContextUsage(), budgetLabel(map)),
|
|
478
|
+
];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function renderModeUi(ctx: ExtensionContext): void {
|
|
287
482
|
if (ctx.mode !== "tui") return;
|
|
288
483
|
ctx.ui.setStatus("pstack-mode", potetoMode ? "pstack: poteto mode" : undefined);
|
|
484
|
+
ctx.ui.setWidget("pstack", potetoMode ? widgetLines(ctx) : undefined, {
|
|
485
|
+
placement: "belowEditor",
|
|
486
|
+
});
|
|
289
487
|
}
|
|
290
488
|
|
|
291
|
-
function persistMode(enabled: boolean, ctx?:
|
|
489
|
+
function persistMode(enabled: boolean, ctx?: ExtensionContext): void {
|
|
292
490
|
potetoMode = enabled;
|
|
293
491
|
pi.appendEntry(MODE_ENTRY_TYPE, { enabled });
|
|
294
|
-
if (ctx)
|
|
492
|
+
if (ctx) renderModeUi(ctx);
|
|
295
493
|
}
|
|
296
494
|
|
|
297
495
|
pi.on("session_start", async (_event, ctx) => {
|
|
298
496
|
potetoMode = lastModeEnabled(sessionEntries(ctx));
|
|
299
|
-
|
|
497
|
+
renderModeUi(ctx);
|
|
300
498
|
const toolNames = new Set(pi.getAllTools().map((tool) => tool.name));
|
|
301
499
|
const missing = COMPANIONS.filter((companion) => !toolNames.has(companion.tool));
|
|
302
500
|
if (missing.length === 0) return;
|
|
@@ -319,6 +517,62 @@ export default function (pi: ExtensionAPI): void {
|
|
|
319
517
|
return { systemPrompt: `${event.systemPrompt}\n\n${POTETO_DIRECTIVE}` };
|
|
320
518
|
});
|
|
321
519
|
|
|
520
|
+
// Context guard: meter the context window while the mode is on, warn as it
|
|
521
|
+
// fills, and offer a one-key poteto compaction when it is nearly full.
|
|
522
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
523
|
+
if (!potetoMode) return;
|
|
524
|
+
renderModeUi(ctx);
|
|
525
|
+
const percent = ctx.getContextUsage()?.percent ?? null;
|
|
526
|
+
const level = guardLevel(percent);
|
|
527
|
+
if (level === "ok") {
|
|
528
|
+
guardNotified = "ok";
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (!isStronger(level, guardNotified)) return;
|
|
532
|
+
guardNotified = level;
|
|
533
|
+
const advice = guardAdvice(level, percent);
|
|
534
|
+
if (level === "compact" && ctx.hasUI) {
|
|
535
|
+
const accepted = await ctx.ui.confirm("Context nearly full", `${advice}\n\nCompact now with the poteto summary?`);
|
|
536
|
+
if (accepted) ctx.compact({ customInstructions: POTETO_COMPACT_INSTRUCTIONS });
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
ctx.ui.notify(advice, "warning");
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
543
|
+
guardNotified = "ok";
|
|
544
|
+
renderModeUi(ctx);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// Poteto compaction: while the mode is on, own the summary shape. Any
|
|
548
|
+
// failure falls back to Pi's default compaction.
|
|
549
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
550
|
+
if (!potetoMode) return;
|
|
551
|
+
if (conversationHelpers === undefined) {
|
|
552
|
+
conversationHelpers = (await loadConversationHelpers()) ?? "unavailable";
|
|
553
|
+
}
|
|
554
|
+
if (conversationHelpers === "unavailable") return;
|
|
555
|
+
const helpers = conversationHelpers;
|
|
556
|
+
const result = await runPotetoCompaction(
|
|
557
|
+
event.preparation,
|
|
558
|
+
event.customInstructions,
|
|
559
|
+
event.signal,
|
|
560
|
+
{
|
|
561
|
+
model: ctx.model,
|
|
562
|
+
complete: (model, context, options) =>
|
|
563
|
+
ctx.modelRegistry.complete(
|
|
564
|
+
model as Parameters<typeof ctx.modelRegistry.complete>[0],
|
|
565
|
+
context as Parameters<typeof ctx.modelRegistry.complete>[1],
|
|
566
|
+
options as Parameters<typeof ctx.modelRegistry.complete>[2],
|
|
567
|
+
),
|
|
568
|
+
serialize: helpers.serialize,
|
|
569
|
+
notify: (message, level) => ctx.ui.notify(message, level ?? "warning"),
|
|
570
|
+
},
|
|
571
|
+
);
|
|
572
|
+
if (!result) return;
|
|
573
|
+
return { compaction: result };
|
|
574
|
+
});
|
|
575
|
+
|
|
322
576
|
const setupCommand = {
|
|
323
577
|
description: "Configure pstack subagent models for Pi via TUI pickers (fast paths: -l scope, inherit|light|custom tier, or one role). Writes only the subagents.* keys; default is all-inherit.",
|
|
324
578
|
getArgumentCompletions: completions,
|
|
@@ -326,11 +580,16 @@ export default function (pi: ExtensionAPI): void {
|
|
|
326
580
|
};
|
|
327
581
|
pi.registerCommand("pstack-setup", setupCommand);
|
|
328
582
|
pi.registerCommand("poteto-mode", {
|
|
329
|
-
description: "Enable or disable sticky Poteto Mode. Usage: /poteto-mode [task] | /poteto-mode off",
|
|
583
|
+
description: "Enable or disable sticky Poteto Mode. Usage: /poteto-mode [task or playbook] | /poteto-mode off",
|
|
330
584
|
getArgumentCompletions: (prefix: string) => {
|
|
331
585
|
const token = prefix.trim().toLowerCase();
|
|
332
|
-
if (
|
|
333
|
-
return
|
|
586
|
+
if (token.includes(" ")) return null;
|
|
587
|
+
if (!token) return [{ value: "off", label: "off" }, ...PLAYBOOK_COMPLETIONS.slice(0, 20)];
|
|
588
|
+
const items = [
|
|
589
|
+
...("off".startsWith(token) ? [{ value: "off", label: "off" }] : []),
|
|
590
|
+
...PLAYBOOK_COMPLETIONS.filter((item) => item.value.startsWith(token)),
|
|
591
|
+
];
|
|
592
|
+
return items.length > 0 ? items : null;
|
|
334
593
|
},
|
|
335
594
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
336
595
|
const raw = args.trim();
|
|
@@ -351,6 +610,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
351
610
|
description: "Show the effective pstack model map (resolved through defaults), the winning settings file, and loaded pstack skills.",
|
|
352
611
|
handler: (_args, ctx) => pstackStatusHandler(pi, _args, ctx),
|
|
353
612
|
});
|
|
613
|
+
pi.registerCommand("pstack-doctor", {
|
|
614
|
+
description: "Check the pstack install: settings validity, package agents, vendored playbooks, companion tools, and the upstream pin.",
|
|
615
|
+
handler: (_args, ctx) => pstackDoctorHandler(pi, _args, ctx),
|
|
616
|
+
});
|
|
354
617
|
pi.registerCommand("bro", {
|
|
355
618
|
description: "Restate the last assistant reply in plain human language, no jargon.",
|
|
356
619
|
handler: (_args, ctx) => broHandler(pi, _args, ctx),
|
|
@@ -5,6 +5,9 @@ import { join } from "node:path";
|
|
|
5
5
|
import { test } from "node:test";
|
|
6
6
|
import {
|
|
7
7
|
POTETO_DIRECTIVE,
|
|
8
|
+
applyBudget,
|
|
9
|
+
budgetLabel,
|
|
10
|
+
budgetOf,
|
|
8
11
|
buildTier,
|
|
9
12
|
lastModeEnabled,
|
|
10
13
|
loadSettings,
|
|
@@ -12,16 +15,53 @@ import {
|
|
|
12
15
|
parseModelId,
|
|
13
16
|
resolvedMapping,
|
|
14
17
|
saveSettings,
|
|
18
|
+
summaryText,
|
|
15
19
|
type ModeEntry,
|
|
16
20
|
} from "./settings.ts";
|
|
17
21
|
|
|
18
22
|
test("parseArgs maps scope, tier, and role tokens", () => {
|
|
19
|
-
assert.deepEqual(parseArgs("-l inherit"), { scope: "project", tier: "all-inherit", role: undefined });
|
|
20
|
-
assert.deepEqual(parseArgs("scout"), { scope: undefined, tier: undefined, role: "scout" });
|
|
21
|
-
assert.deepEqual(parseArgs(""), { scope: undefined, tier: undefined, role: undefined });
|
|
22
|
-
assert.deepEqual(parseArgs("--local light"), { scope: "project", tier: "light", role: undefined });
|
|
23
|
-
assert.deepEqual(parseArgs("custom poteto-agent"), { scope: undefined, tier: "custom", role: "poteto-agent" });
|
|
24
|
-
assert.deepEqual(parseArgs("nonsense-token"), { scope: undefined, tier: undefined, role: undefined });
|
|
23
|
+
assert.deepEqual(parseArgs("-l inherit"), { scope: "project", tier: "all-inherit", role: undefined, budget: undefined });
|
|
24
|
+
assert.deepEqual(parseArgs("scout"), { scope: undefined, tier: undefined, role: "scout", budget: undefined });
|
|
25
|
+
assert.deepEqual(parseArgs(""), { scope: undefined, tier: undefined, role: undefined, budget: undefined });
|
|
26
|
+
assert.deepEqual(parseArgs("--local light"), { scope: "project", tier: "light", role: undefined, budget: undefined });
|
|
27
|
+
assert.deepEqual(parseArgs("custom poteto-agent"), { scope: undefined, tier: "custom", role: "poteto-agent", budget: undefined });
|
|
28
|
+
assert.deepEqual(parseArgs("nonsense-token"), { scope: undefined, tier: undefined, role: undefined, budget: undefined });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("parseArgs maps budget tokens, alone and combined", () => {
|
|
32
|
+
assert.equal(parseArgs("small").budget, "small");
|
|
33
|
+
assert.equal(parseArgs("unlimited").budget, "unlimited");
|
|
34
|
+
assert.deepEqual(parseArgs("-l large"), { scope: "project", tier: undefined, role: undefined, budget: "large" });
|
|
35
|
+
assert.equal(parseArgs("medium scout").role, "scout");
|
|
36
|
+
assert.equal(parseArgs("medium scout").budget, "medium");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("applyBudget sets and clears defaultThinking without mutating its input", () => {
|
|
40
|
+
const base = { defaultModel: "inherit", defaultThinking: "high" as const };
|
|
41
|
+
const raised = applyBudget(base, "large");
|
|
42
|
+
assert.equal(raised.defaultThinking, "xhigh");
|
|
43
|
+
assert.equal(base.defaultThinking, "high", "input map is untouched");
|
|
44
|
+
const cleared = applyBudget(base, "unlimited");
|
|
45
|
+
assert.equal(cleared.defaultThinking, undefined);
|
|
46
|
+
assert.ok("defaultThinking" in cleared === false, "unlimited removes the key instead of writing null");
|
|
47
|
+
const lowered = applyBudget({}, "small");
|
|
48
|
+
assert.equal(lowered.defaultThinking, "medium");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("budgetOf reads the recorded budget back off the map", () => {
|
|
52
|
+
assert.equal(budgetOf({}), "unlimited");
|
|
53
|
+
assert.equal(budgetOf({ defaultThinking: "xhigh" }), "large");
|
|
54
|
+
assert.equal(budgetOf({ defaultThinking: "max" }), "large");
|
|
55
|
+
assert.equal(budgetOf({ defaultThinking: "high" }), "medium");
|
|
56
|
+
assert.equal(budgetOf({ defaultThinking: "medium" }), "small");
|
|
57
|
+
assert.equal(budgetOf({ defaultThinking: "low" }), "unlimited", "a hand-written level below the ladder reads as unlimited");
|
|
58
|
+
assert.equal(budgetLabel({ defaultThinking: "high" }), "medium (high reasoning)");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("summaryText names the budget on the header line", () => {
|
|
62
|
+
const text = summaryText(applyBudget(buildTier("all-inherit"), "small"), "user", []);
|
|
63
|
+
assert.ok(text.includes("budget: small"), text);
|
|
64
|
+
assert.ok(text.startsWith("pstack models (user), budget: small"), text);
|
|
25
65
|
});
|
|
26
66
|
|
|
27
67
|
test("parseModelId strips labels to ids", () => {
|
|
@@ -37,25 +77,40 @@ test("buildTier is pure and idempotent", () => {
|
|
|
37
77
|
assert.equal(JSON.stringify(buildTier("all-inherit")), JSON.stringify(all));
|
|
38
78
|
|
|
39
79
|
const light = buildTier("light");
|
|
40
|
-
assert.equal(light.defaultModel, "opencode-go/hy3");
|
|
41
80
|
assert.equal(light.defaultThinking, "high");
|
|
42
|
-
assert.
|
|
43
|
-
|
|
44
|
-
|
|
81
|
+
assert.deepEqual(light.agentOverrides, {
|
|
82
|
+
scout: { thinking: "low" },
|
|
83
|
+
oracle: { thinking: "high" },
|
|
84
|
+
"poteto-agent": { thinking: "high" },
|
|
85
|
+
"comment-sicko": { thinking: "high" },
|
|
86
|
+
});
|
|
87
|
+
assert.equal(JSON.stringify(buildTier("light")), JSON.stringify(light));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("light tier writes no model slug other than inherit", () => {
|
|
91
|
+
const json = JSON.stringify(buildTier("light"));
|
|
92
|
+
assert.deepEqual(json.match(/[a-z0-9-]+\/[a-z0-9.-]+/gi), null, "no provider/model slug is pinned");
|
|
93
|
+
assert.ok(json.includes('"defaultModel":"inherit"'), "every role inherits the parent model");
|
|
45
94
|
});
|
|
46
95
|
|
|
47
96
|
test("resolvedMapping falls back default -> inherit and includes extras", () => {
|
|
48
97
|
const light = buildTier("light");
|
|
49
98
|
const mapped = resolvedMapping(light, ["default", "scout", "oracle", "poteto-agent"]);
|
|
50
|
-
assert.deepEqual(mapped
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
99
|
+
assert.deepEqual(mapped, [
|
|
100
|
+
{ role: "default", model: "inherit", extra: undefined },
|
|
101
|
+
{ role: "scout", model: "inherit", extra: "thinking:low" },
|
|
102
|
+
{ role: "oracle", model: "inherit", extra: "thinking:high" },
|
|
103
|
+
{ role: "poteto-agent", model: "inherit", extra: "thinking:high" },
|
|
55
104
|
]);
|
|
56
|
-
assert.ok(mapped.find((row) => row.role === "oracle")?.extra?.includes("fallback:"));
|
|
57
105
|
const empty = resolvedMapping({}, ["default", "worker"]);
|
|
58
106
|
assert.deepEqual(empty.map((row) => row.model), ["inherit", "inherit"]);
|
|
107
|
+
const handWrittenFallback = resolvedMapping(
|
|
108
|
+
{ agentOverrides: { oracle: { model: "inherit", fallbackModels: ["demo/one"] } } },
|
|
109
|
+
["oracle"],
|
|
110
|
+
);
|
|
111
|
+
assert.deepEqual(handWrittenFallback, [
|
|
112
|
+
{ role: "oracle", model: "inherit", extra: "fallback:demo/one" },
|
|
113
|
+
]);
|
|
59
114
|
});
|
|
60
115
|
|
|
61
116
|
test("saveSettings merges into existing settings, never clobbers", () => {
|
package/extensions/settings.ts
CHANGED
|
@@ -36,6 +36,52 @@ export interface ModelMap {
|
|
|
36
36
|
export type Tier = "all-inherit" | "light" | "custom";
|
|
37
37
|
export type Scope = "user" | "project";
|
|
38
38
|
|
|
39
|
+
export type BudgetKey = "unlimited" | "large" | "medium" | "small";
|
|
40
|
+
|
|
41
|
+
export interface BudgetSpec {
|
|
42
|
+
thinking: ThinkingLevel | undefined;
|
|
43
|
+
blurb: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const BUDGETS: Record<BudgetKey, BudgetSpec> = {
|
|
47
|
+
unlimited: { thinking: undefined, blurb: "keep each role's own setting" },
|
|
48
|
+
large: { thinking: "xhigh", blurb: "xhigh reasoning" },
|
|
49
|
+
medium: { thinking: "high", blurb: "high reasoning" },
|
|
50
|
+
small: { thinking: "medium", blurb: "medium reasoning" },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const BUDGET_KEYS = new Set<string>(Object.keys(BUDGETS));
|
|
54
|
+
|
|
55
|
+
export function applyBudget(map: ModelMap, budget: BudgetKey): ModelMap {
|
|
56
|
+
const next: ModelMap = { ...map };
|
|
57
|
+
const thinking = BUDGETS[budget].thinking;
|
|
58
|
+
if (thinking) {
|
|
59
|
+
next.defaultThinking = thinking;
|
|
60
|
+
} else {
|
|
61
|
+
delete next.defaultThinking;
|
|
62
|
+
}
|
|
63
|
+
return next;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function budgetOf(map: ModelMap): BudgetKey {
|
|
67
|
+
switch (map.defaultThinking) {
|
|
68
|
+
case "xhigh":
|
|
69
|
+
case "max":
|
|
70
|
+
return "large";
|
|
71
|
+
case "high":
|
|
72
|
+
return "medium";
|
|
73
|
+
case "medium":
|
|
74
|
+
return "small";
|
|
75
|
+
default:
|
|
76
|
+
return "unlimited";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function budgetLabel(map: ModelMap): string {
|
|
81
|
+
const key = budgetOf(map);
|
|
82
|
+
return `${key} (${BUDGETS[key].blurb})`;
|
|
83
|
+
}
|
|
84
|
+
|
|
39
85
|
export const ROLE_KEYS: RoleKey[] = [
|
|
40
86
|
"default",
|
|
41
87
|
"scout",
|
|
@@ -74,13 +120,14 @@ export interface ParsedArgs {
|
|
|
74
120
|
scope: Scope | undefined;
|
|
75
121
|
tier?: Tier;
|
|
76
122
|
role?: RoleKey;
|
|
123
|
+
budget?: BudgetKey;
|
|
77
124
|
}
|
|
78
125
|
|
|
79
126
|
const ROLE_SET = new Set<string>(ROLE_KEYS);
|
|
80
127
|
|
|
81
128
|
export function parseArgs(raw: string): ParsedArgs {
|
|
82
129
|
const tokens = raw.trim().split(/\s+/).filter(Boolean);
|
|
83
|
-
const out: ParsedArgs = { scope: undefined, tier: undefined, role: undefined };
|
|
130
|
+
const out: ParsedArgs = { scope: undefined, tier: undefined, role: undefined, budget: undefined };
|
|
84
131
|
for (const token of tokens) {
|
|
85
132
|
if (token === "-l" || token === "--local" || token === "local") {
|
|
86
133
|
out.scope = "project";
|
|
@@ -90,6 +137,8 @@ export function parseArgs(raw: string): ParsedArgs {
|
|
|
90
137
|
out.tier = "light";
|
|
91
138
|
} else if (token === "custom") {
|
|
92
139
|
out.tier = "custom";
|
|
140
|
+
} else if (BUDGET_KEYS.has(token)) {
|
|
141
|
+
out.budget = token as BudgetKey;
|
|
93
142
|
} else if (ROLE_SET.has(token)) {
|
|
94
143
|
out.role = token as RoleKey;
|
|
95
144
|
}
|
|
@@ -183,15 +232,13 @@ export function buildTier(tier: "all-inherit" | "light"): ModelMap {
|
|
|
183
232
|
};
|
|
184
233
|
}
|
|
185
234
|
return {
|
|
186
|
-
defaultModel: "
|
|
235
|
+
defaultModel: "inherit",
|
|
187
236
|
defaultThinking: "high",
|
|
188
237
|
agentOverrides: {
|
|
189
|
-
scout: {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
"poteto-agent": { model: "inherit", thinking: "high" },
|
|
194
|
-
"comment-sicko": { model: "inherit", thinking: "high" },
|
|
238
|
+
scout: { thinking: "low" },
|
|
239
|
+
oracle: { thinking: "high" },
|
|
240
|
+
"poteto-agent": { thinking: "high" },
|
|
241
|
+
"comment-sicko": { thinking: "high" },
|
|
195
242
|
},
|
|
196
243
|
};
|
|
197
244
|
}
|
|
@@ -226,7 +273,8 @@ export function summaryText(
|
|
|
226
273
|
loadedPstackSkills: string[],
|
|
227
274
|
): string {
|
|
228
275
|
const scopeName = scope === "project" ? "project" : scope === "user" ? "user" : "none saved";
|
|
229
|
-
const
|
|
276
|
+
const budget = budgetOf(map);
|
|
277
|
+
const lines = [`pstack models (${scopeName}), budget: ${budget} (${BUDGETS[budget].blurb}):`];
|
|
230
278
|
for (const role of resolvedMapping(map, ROLE_KEYS)) {
|
|
231
279
|
lines.push(
|
|
232
280
|
` ${role.role} -> ${role.model}${role.extra ? ` (${role.extra})` : ""}`,
|