@mccune1224/pi-pstack 0.1.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.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -0
  3. package/agents/comment-sicko.md +37 -0
  4. package/agents/poteto-agent.md +16 -0
  5. package/docs/guide/01-setup.md +49 -0
  6. package/docs/guide/02-poteto-mode.md +98 -0
  7. package/docs/guide/03-understand.md +61 -0
  8. package/docs/guide/04-design.md +81 -0
  9. package/docs/guide/05-build-and-clean.md +75 -0
  10. package/docs/guide/06-verify-and-ship.md +87 -0
  11. package/docs/guide/07-overnight.md +81 -0
  12. package/docs/guide/08-principles.md +69 -0
  13. package/docs/guide/09-make-it-yours.md +67 -0
  14. package/docs/guide/10-recipes-and-pitfalls.md +94 -0
  15. package/docs/guide/README.md +30 -0
  16. package/docs/guide/images/design.jpg +0 -0
  17. package/docs/guide/images/overnight.jpg +0 -0
  18. package/docs/guide/images/recipes.jpg +0 -0
  19. package/docs/guide/images/router.jpg +0 -0
  20. package/docs/guide/images/understanding.jpg +0 -0
  21. package/docs/guide/images/verification.jpg +0 -0
  22. package/extensions/index.ts +358 -0
  23. package/extensions/settings.test.ts +110 -0
  24. package/extensions/settings.ts +240 -0
  25. package/package.json +77 -0
  26. package/skills/architect/SKILL.md +83 -0
  27. package/skills/architect/references/design-red-flags.md +33 -0
  28. package/skills/architect/references/rationale-template.md +35 -0
  29. package/skills/architect/references/runner-prompt.md +20 -0
  30. package/skills/arena/SKILL.md +71 -0
  31. package/skills/automate-me/SKILL.md +109 -0
  32. package/skills/blast-radius/SKILL.md +50 -0
  33. package/skills/bro/SKILL.md +7 -0
  34. package/skills/control-cli/SKILL.md +109 -0
  35. package/skills/control-ui/SKILL.md +109 -0
  36. package/skills/create-skill/SKILL.md +69 -0
  37. package/skills/create-verification-skill/SKILL.md +44 -0
  38. package/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
  39. package/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
  40. package/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
  41. package/skills/deslop/SKILL.md +22 -0
  42. package/skills/figure-it-out/SKILL.md +55 -0
  43. package/skills/how/SKILL.md +130 -0
  44. package/skills/how/references/critic-prompt.md +59 -0
  45. package/skills/how/references/critique-rubric.md +58 -0
  46. package/skills/how/references/explainer-prompt.md +55 -0
  47. package/skills/how/references/explorer-prompt.md +52 -0
  48. package/skills/interrogate/SKILL.md +112 -0
  49. package/skills/interrogate/references/code-quality-review.md +47 -0
  50. package/skills/interrogate/references/lead-judgment.md +58 -0
  51. package/skills/interrogate/references/reviewer-prompt.md +72 -0
  52. package/skills/interrogate/references/rubric.md +77 -0
  53. package/skills/maintain-verification-skill/SKILL.md +39 -0
  54. package/skills/no-comments/SKILL.md +24 -0
  55. package/skills/poteto-mode/SKILL.md +142 -0
  56. package/skills/poteto-mode/playbooks/authoring-a-skill.md +12 -0
  57. package/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
  58. package/skills/poteto-mode/playbooks/autopilot-full.md +13 -0
  59. package/skills/poteto-mode/playbooks/autopilot-stack.md +16 -0
  60. package/skills/poteto-mode/playbooks/babysit.md +27 -0
  61. package/skills/poteto-mode/playbooks/bug-fix.md +17 -0
  62. package/skills/poteto-mode/playbooks/eval.md +27 -0
  63. package/skills/poteto-mode/playbooks/feature.md +21 -0
  64. package/skills/poteto-mode/playbooks/hillclimb.md +21 -0
  65. package/skills/poteto-mode/playbooks/investigation.md +14 -0
  66. package/skills/poteto-mode/playbooks/multi-phase-plan.md +155 -0
  67. package/skills/poteto-mode/playbooks/opening-a-pr.md +29 -0
  68. package/skills/poteto-mode/playbooks/orchestrate.md +113 -0
  69. package/skills/poteto-mode/playbooks/pause-safely.md +10 -0
  70. package/skills/poteto-mode/playbooks/perf-issue.md +24 -0
  71. package/skills/poteto-mode/playbooks/prototype.md +14 -0
  72. package/skills/poteto-mode/playbooks/refactoring.md +16 -0
  73. package/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
  74. package/skills/poteto-mode/playbooks/session-pickup.md +13 -0
  75. package/skills/poteto-mode/playbooks/shipping.md +20 -0
  76. package/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
  77. package/skills/poteto-mode/playbooks/visual-parity.md +11 -0
  78. package/skills/poteto-mode/playbooks/worktree-cleanup.md +14 -0
  79. package/skills/poteto-mode/references/bugbot-triage.md +142 -0
  80. package/skills/poteto-mode/scripts/bootstrap.ts +62 -0
  81. package/skills/poteto-mode/scripts/bun.lock +67 -0
  82. package/skills/poteto-mode/scripts/check-plan.mjs +186 -0
  83. package/skills/poteto-mode/scripts/orch/orch.test.ts +634 -0
  84. package/skills/poteto-mode/scripts/orch/orch.ts +578 -0
  85. package/skills/poteto-mode/scripts/orch/store.ts +1607 -0
  86. package/skills/poteto-mode/scripts/package.json +16 -0
  87. package/skills/poteto-mode/scripts/watch-pr/cli.test.ts +224 -0
  88. package/skills/poteto-mode/scripts/watch-pr/cli.ts +223 -0
  89. package/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts +118 -0
  90. package/skills/poteto-mode/scripts/watch-pr/github.test.ts +306 -0
  91. package/skills/poteto-mode/scripts/watch-pr/github.ts +699 -0
  92. package/skills/poteto-mode/scripts/watch-pr/policy.test.ts +420 -0
  93. package/skills/poteto-mode/scripts/watch-pr/policy.ts +832 -0
  94. package/skills/poteto-mode/scripts/watch-pr/render.ts +169 -0
  95. package/skills/poteto-mode/scripts/watch-pr/tsconfig.json +13 -0
  96. package/skills/poteto-mode/scripts/watch-pr/types.compile.ts +93 -0
  97. package/skills/poteto-mode/scripts/watch-pr/types.ts +401 -0
  98. package/skills/poteto-mode/scripts/watch-pr/watch-pr +6 -0
  99. package/skills/poteto-mode/scripts/worktree-audit.mjs +178 -0
  100. package/skills/principle-boundary-discipline/SKILL.md +34 -0
  101. package/skills/principle-build-the-lever/SKILL.md +23 -0
  102. package/skills/principle-encode-lessons-in-structure/SKILL.md +31 -0
  103. package/skills/principle-exhaust-the-design-space/SKILL.md +21 -0
  104. package/skills/principle-experience-first/SKILL.md +19 -0
  105. package/skills/principle-fix-root-causes/SKILL.md +23 -0
  106. package/skills/principle-foundational-thinking/SKILL.md +21 -0
  107. package/skills/principle-guard-the-context-window/SKILL.md +17 -0
  108. package/skills/principle-laziness-protocol/SKILL.md +18 -0
  109. package/skills/principle-make-operations-idempotent/SKILL.md +24 -0
  110. package/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +22 -0
  111. package/skills/principle-minimize-reader-load/SKILL.md +23 -0
  112. package/skills/principle-model-the-domain/SKILL.md +26 -0
  113. package/skills/principle-never-block-on-the-human/SKILL.md +23 -0
  114. package/skills/principle-outcome-oriented-execution/SKILL.md +22 -0
  115. package/skills/principle-prove-it-works/SKILL.md +33 -0
  116. package/skills/principle-redesign-from-first-principles/SKILL.md +16 -0
  117. package/skills/principle-separate-before-serializing-shared-state/SKILL.md +16 -0
  118. package/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
  119. package/skills/principle-subtract-before-you-add/SKILL.md +22 -0
  120. package/skills/principle-type-system-discipline/SKILL.md +31 -0
  121. package/skills/recall/SKILL.md +35 -0
  122. package/skills/reflect/SKILL.md +77 -0
  123. package/skills/reflect/references/divergent-reviewer.md +43 -0
  124. package/skills/reflect/references/judgment-reviewer.md +42 -0
  125. package/skills/reflect/references/synthesizer.md +56 -0
  126. package/skills/reflect/references/tooling-reviewer.md +57 -0
  127. package/skills/setup-pstack/SKILL.md +92 -0
  128. package/skills/show-me-your-work/SKILL.md +82 -0
  129. package/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
  130. package/skills/show-me-your-work/scripts/log.mjs +30 -0
  131. package/skills/swarm/SKILL.md +46 -0
  132. package/skills/tdd/SKILL.md +44 -0
  133. package/skills/teach/SKILL.md +21 -0
  134. package/skills/technical-writing/SKILL.md +130 -0
  135. package/skills/typescript-best-practices/SKILL.md +28 -0
  136. package/skills/typescript-best-practices/references/patterns.md +292 -0
  137. package/skills/unslop/SKILL.md +80 -0
  138. package/skills/why/SKILL.md +229 -0
  139. package/skills/why/references/epistemics.md +144 -0
  140. package/skills/why/references/investigator-prompt.md +103 -0
  141. package/skills/why/references/source-playbook.md +17 -0
  142. package/skills/why/references/sources/code-archaeology.md +88 -0
  143. package/skills/why/references/sources/databricks.md +70 -0
  144. package/skills/why/references/sources/datadog.md +99 -0
  145. package/skills/why/references/sources/incident-postmortem.md +15 -0
  146. package/skills/why/references/sources/linear.md +48 -0
  147. package/skills/why/references/sources/notion.md +55 -0
  148. package/skills/why/references/sources/sentry.md +100 -0
  149. package/skills/why/references/sources/slack.md +54 -0
  150. package/skills/why/references/synthesizer-prompt.md +135 -0
@@ -0,0 +1,358 @@
1
+ import { CONFIG_DIR_NAME, type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ MODE_ENTRY_TYPE,
4
+ POTETO_DIRECTIVE,
5
+ ROLE_KEYS,
6
+ availableModels,
7
+ buildTier,
8
+ lastModeEnabled,
9
+ loadSettings,
10
+ parseArgs,
11
+ parseModelId,
12
+ saveSettings,
13
+ settingsPath,
14
+ summaryText,
15
+ type ModeEntry,
16
+ type ModelMap,
17
+ type ModelSpec,
18
+ type RoleKey,
19
+ type Scope,
20
+ } from "./settings.ts";
21
+
22
+ const SETUP_COMPLETIONS = [
23
+ "-l",
24
+ "--local",
25
+ "local",
26
+ "inherit",
27
+ "all-inherit",
28
+ "light",
29
+ "custom",
30
+ "default",
31
+ "scout",
32
+ "worker",
33
+ "reviewer",
34
+ "oracle",
35
+ "poteto-agent",
36
+ "comment-sicko",
37
+ ];
38
+
39
+ const PSTACK_SKILLS = new Set([
40
+ "poteto-mode",
41
+ "how",
42
+ "why",
43
+ "architect",
44
+ "arena",
45
+ "swarm",
46
+ "interrogate",
47
+ "reflect",
48
+ "tdd",
49
+ "unslop",
50
+ "no-comments",
51
+ "technical-writing",
52
+ "bro",
53
+ "teach",
54
+ "automate-me",
55
+ "recall",
56
+ "blast-radius",
57
+ "figure-it-out",
58
+ "show-me-your-work",
59
+ "create-verification-skill",
60
+ "maintain-verification-skill",
61
+ "typescript-best-practices",
62
+ "setup-pstack",
63
+ "deslop",
64
+ "control-cli",
65
+ "control-ui",
66
+ "create-skill",
67
+ ]);
68
+
69
+ function completions(prefix: string) {
70
+ const items = SETUP_COMPLETIONS.map((value) => ({ value, label: value }));
71
+ const filtered = items.filter((item) => item.value.startsWith(prefix));
72
+ return filtered.length > 0 ? filtered : null;
73
+ }
74
+
75
+ const COMPANIONS = [
76
+ {
77
+ tool: "subagent",
78
+ pkg: "npm:pi-subagents",
79
+ required: true,
80
+ why: "registers the subagent tool, reads this package's agents/, and backs every routed skill",
81
+ },
82
+ {
83
+ tool: "todo",
84
+ pkg: "npm:@juicesharp/rpiv-todo",
85
+ required: false,
86
+ why: "the playbooks open a todo list through it",
87
+ },
88
+ {
89
+ tool: "ask_user_question",
90
+ pkg: "npm:@juicesharp/rpiv-ask-user-question",
91
+ required: false,
92
+ why: "the skills name it when a decision needs the user",
93
+ },
94
+ ];
95
+
96
+ function pstackSkillNames(commands: Array<{ name: string; source: string }>): string[] {
97
+ return commands
98
+ .filter((command) => command.source === "skill")
99
+ .map((command) => command.name.replace(/^skill:/, "").replace(/:\d+$/, ""))
100
+ .filter((name) => PSTACK_SKILLS.has(name) || name.startsWith("principle-"))
101
+ .sort();
102
+ }
103
+
104
+ function modelChoices(ctx: ExtensionCommandContext, currentValue: string) {
105
+ return availableModels(ctx.modelRegistry.getAvailable()).map((choice) =>
106
+ choice.id === currentValue ? `${choice.label} [current]` : choice.label,
107
+ );
108
+ }
109
+
110
+ async function announceAndReload(
111
+ ctx: ExtensionCommandContext,
112
+ path: string,
113
+ map: ModelMap,
114
+ scope: Scope,
115
+ ): Promise<void> {
116
+ ctx.ui.notify(
117
+ `${summaryText(map, scope, [])}\nsettings: ${path}\nRun /reload to apply. Check /subagents-models after reload.`,
118
+ "info",
119
+ );
120
+ if (ctx.hasUI && (await ctx.ui.confirm("Reload now?", "Applies the new model map immediately."))) {
121
+ await ctx.reload();
122
+ }
123
+ }
124
+
125
+ async function pstackSetupHandler(args: string, ctx: ExtensionCommandContext): Promise<void> {
126
+ const parsed = parseArgs(args);
127
+ const scopePick =
128
+ parsed.scope ??
129
+ (ctx.hasUI
130
+ ? await ctx.ui.select("Where to save the pstack model map?", [
131
+ "User (~/.pi/agent/settings.json) - recommended",
132
+ "Project (.pi/settings.json) - team shared",
133
+ ])
134
+ : "user");
135
+ if (!scopePick) return;
136
+ const scope: Scope = scopePick.startsWith("Project") ? "project" : "user";
137
+ const path = settingsPath(ctx.cwd, scope, CONFIG_DIR_NAME);
138
+ const current = loadSettings(path);
139
+
140
+ if (parsed.role) {
141
+ const currentValue =
142
+ parsed.role === "default"
143
+ ? current.defaultModel ?? "inherit"
144
+ : current.agentOverrides?.[parsed.role]?.model ?? current.defaultModel ?? "inherit";
145
+ const pick = await ctx.ui.select(
146
+ `Choose model for '${parsed.role}' - now ${currentValue}`,
147
+ modelChoices(ctx, currentValue),
148
+ );
149
+ if (!pick) return;
150
+ const id = parseModelId(pick);
151
+ const map: ModelMap = { ...current };
152
+ if (parsed.role === "default") {
153
+ map.defaultModel = id;
154
+ } else {
155
+ map.agentOverrides = {
156
+ ...(current.agentOverrides ?? {}),
157
+ [parsed.role]: { ...current.agentOverrides?.[parsed.role], model: id },
158
+ };
159
+ }
160
+ saveSettings(path, map);
161
+ await announceAndReload(ctx, path, map, scope);
162
+ return;
163
+ }
164
+
165
+ let tier = parsed.tier;
166
+ if (!tier && ctx.hasUI) {
167
+ ctx.ui.notify(summaryText(current, "none", []), "info");
168
+ const pick = await ctx.ui.select("Pick a tier", [
169
+ "All inherit (cheapest) - every role uses the parent model (Recommended)",
170
+ "Light tier (scout low, oracle fallback, agents high thinking)",
171
+ "Custom per role",
172
+ ]);
173
+ if (!pick) return;
174
+ tier = pick.startsWith("Light") ? "light" : pick.startsWith("Custom") ? "custom" : "all-inherit";
175
+ }
176
+ tier ??= "all-inherit";
177
+
178
+ let map: ModelMap;
179
+ if (tier === "custom") {
180
+ const choices = availableModels(ctx.modelRegistry.getAvailable());
181
+ const overrides: Record<string, ModelSpec> = { ...(current.agentOverrides ?? {}) };
182
+ let defaultModel = current.defaultModel ?? "inherit";
183
+ for (const role of ROLE_KEYS) {
184
+ const selected = role === "default" ? defaultModel : overrides[role]?.model ?? defaultModel;
185
+ const pick = await ctx.ui.select(
186
+ `${role} - now ${selected}`,
187
+ choices.map((choice) => (choice.id === selected ? `${choice.label} [current]` : choice.label)),
188
+ );
189
+ if (!pick) continue;
190
+ const id = parseModelId(pick);
191
+ if (role === "default") {
192
+ defaultModel = id;
193
+ } else {
194
+ overrides[role] = { ...(overrides[role] ?? {}), model: id };
195
+ }
196
+ }
197
+ map = { defaultModel, agentOverrides: overrides };
198
+ if (!map) return;
199
+ } else {
200
+ map = buildTier(tier);
201
+ }
202
+ saveSettings(path, map);
203
+ await announceAndReload(ctx, path, map, scope);
204
+ }
205
+
206
+ async function pstackStatusHandler(
207
+ pi: ExtensionAPI,
208
+ _args: string,
209
+ ctx: ExtensionCommandContext,
210
+ ): Promise<void> {
211
+ const userPath = settingsPath(ctx.cwd, "user", CONFIG_DIR_NAME);
212
+ const projectPath = settingsPath(ctx.cwd, "project", CONFIG_DIR_NAME);
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";
218
+ const text =
219
+ summaryText(effective, scope, pstackSkillNames(pi.getCommands())) +
220
+ `\nsettings: ${projectWins ? projectPath : userPath}`;
221
+ const parent =
222
+ ctx.model && typeof ctx.model === "object"
223
+ ? `${(ctx.model as { provider?: string }).provider}/${(ctx.model as { id?: string }).id}`
224
+ : "unknown";
225
+ const full = `parent session model: ${parent}\n${text}`;
226
+ if (ctx.hasUI) {
227
+ ctx.ui.notify(full, "info");
228
+ } else {
229
+ console.log(full);
230
+ }
231
+ }
232
+
233
+ function quoteAssistantText(text: string): string {
234
+ return text.length > 400 ? `${text.slice(0, 400)}...` : text;
235
+ }
236
+
237
+ interface TextBlock {
238
+ type: "text";
239
+ text: string;
240
+ }
241
+
242
+ function isTextBlock(value: unknown): value is TextBlock {
243
+ if (typeof value !== "object" || value === null) return false;
244
+ const block = value as Record<string, unknown>;
245
+ return block.type === "text" && typeof block.text === "string";
246
+ }
247
+
248
+ function textContent(content: unknown): string | undefined {
249
+ if (typeof content === "string") return content.trim() || undefined;
250
+ if (!Array.isArray(content)) return undefined;
251
+ const parts = content.filter(isTextBlock).map((block) => block.text);
252
+ return parts.length > 0 ? parts.join("\n") : undefined;
253
+ }
254
+
255
+ async function broHandler(
256
+ pi: ExtensionAPI,
257
+ _args: string,
258
+ ctx: ExtensionCommandContext,
259
+ ): Promise<void> {
260
+ const entries = ctx.sessionManager.getBranch();
261
+ for (let i = entries.length - 1; i >= 0; i--) {
262
+ const entry = entries[i];
263
+ if (entry.type !== "message" || entry.message.role !== "assistant") continue;
264
+ const text = textContent(entry.message.content);
265
+ if (!text) continue;
266
+ pi.sendUserMessage(
267
+ `Restate your last reply in plain human language, no jargon:\n\n${quoteAssistantText(text)}`,
268
+ ctx.isIdle() ? undefined : { deliverAs: "followUp" },
269
+ );
270
+ return;
271
+ }
272
+ ctx.ui.notify("No assistant reply to restate.", "warning");
273
+ }
274
+
275
+ export default function (pi: ExtensionAPI): void {
276
+ let potetoMode = false;
277
+
278
+ function sessionEntries(ctx: {
279
+ sessionManager: { getBranch?: () => ModeEntry[]; getEntries: () => ModeEntry[] };
280
+ }): ModeEntry[] {
281
+ return typeof ctx.sessionManager.getBranch === "function"
282
+ ? ctx.sessionManager.getBranch()
283
+ : ctx.sessionManager.getEntries();
284
+ }
285
+
286
+ function setModeStatus(ctx: import("@earendil-works/pi-coding-agent").ExtensionContext): void {
287
+ if (ctx.mode !== "tui") return;
288
+ ctx.ui.setStatus("pstack-mode", potetoMode ? "pstack: poteto mode" : undefined);
289
+ }
290
+
291
+ function persistMode(enabled: boolean, ctx?: import("@earendil-works/pi-coding-agent").ExtensionContext): void {
292
+ potetoMode = enabled;
293
+ pi.appendEntry(MODE_ENTRY_TYPE, { enabled });
294
+ if (ctx) setModeStatus(ctx);
295
+ }
296
+
297
+ pi.on("session_start", async (_event, ctx) => {
298
+ potetoMode = lastModeEnabled(sessionEntries(ctx));
299
+ setModeStatus(ctx);
300
+ const toolNames = new Set(pi.getAllTools().map((tool) => tool.name));
301
+ const missing = COMPANIONS.filter((companion) => !toolNames.has(companion.tool));
302
+ if (missing.length === 0) return;
303
+ const lines = missing.map(
304
+ (companion) =>
305
+ `${companion.required ? "required" : "recommended"}: ${companion.pkg} (provides ${companion.tool}; ${companion.why})`,
306
+ );
307
+ ctx.ui.notify(`pi-pstack missing companions. ${lines.join(" | ")}`, "warning");
308
+ });
309
+
310
+ pi.on("input", (event, ctx) => {
311
+ if (/^\/skill:poteto-mode(?:\s|$)/.test(event.text)) {
312
+ persistMode(true, ctx);
313
+ }
314
+ return { action: "continue" as const };
315
+ });
316
+
317
+ pi.on("before_agent_start", (event) => {
318
+ if (!potetoMode) return;
319
+ return { systemPrompt: `${event.systemPrompt}\n\n${POTETO_DIRECTIVE}` };
320
+ });
321
+
322
+ const setupCommand = {
323
+ 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
+ getArgumentCompletions: completions,
325
+ handler: pstackSetupHandler,
326
+ };
327
+ pi.registerCommand("pstack-setup", setupCommand);
328
+ pi.registerCommand("poteto-mode", {
329
+ description: "Enable or disable sticky Poteto Mode. Usage: /poteto-mode [task] | /poteto-mode off",
330
+ getArgumentCompletions: (prefix: string) => {
331
+ const token = prefix.trim().toLowerCase();
332
+ if (!token || "off".startsWith(token)) return [{ value: "off", label: "off" }];
333
+ return null;
334
+ },
335
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
336
+ const raw = args.trim();
337
+ const token = raw.split(/\s+/)[0]?.toLowerCase() ?? "";
338
+ if (token === "off" || token === "disable" || token === "stop") {
339
+ persistMode(false, ctx);
340
+ ctx.ui.notify("Poteto Mode off.", "info");
341
+ return;
342
+ }
343
+ persistMode(true, ctx);
344
+ ctx.ui.notify("Poteto Mode on. Stays on until /poteto-mode off.", "info");
345
+ const payload = `/skill:poteto-mode${raw ? ` ${raw}` : ""}`;
346
+ pi.sendUserMessage(payload, ctx.isIdle() ? { expandPromptTemplates: true } : { expandPromptTemplates: true, deliverAs: "followUp" });
347
+ },
348
+ });
349
+
350
+ pi.registerCommand("pstack-status", {
351
+ description: "Show the effective pstack model map (resolved through defaults), the winning settings file, and loaded pstack skills.",
352
+ handler: (_args, ctx) => pstackStatusHandler(pi, _args, ctx),
353
+ });
354
+ pi.registerCommand("bro", {
355
+ description: "Restate the last assistant reply in plain human language, no jargon.",
356
+ handler: (_args, ctx) => broHandler(pi, _args, ctx),
357
+ });
358
+ }
@@ -0,0 +1,110 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { test } from "node:test";
6
+ import {
7
+ POTETO_DIRECTIVE,
8
+ buildTier,
9
+ lastModeEnabled,
10
+ loadSettings,
11
+ parseArgs,
12
+ parseModelId,
13
+ resolvedMapping,
14
+ saveSettings,
15
+ type ModeEntry,
16
+ } from "./settings.ts";
17
+
18
+ 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 });
25
+ });
26
+
27
+ test("parseModelId strips labels to ids", () => {
28
+ assert.equal(parseModelId("inherit (parent model - cheapest, recommended)"), "inherit");
29
+ assert.equal(parseModelId("opencode-go/hy3 (256k)"), "opencode-go/hy3");
30
+ assert.equal(parseModelId("openrouter/anthropic/claude-3.7-sonnet"), "openrouter/anthropic/claude-3.7-sonnet");
31
+ });
32
+
33
+ test("buildTier is pure and idempotent", () => {
34
+ const all = buildTier("all-inherit");
35
+ assert.equal(all.defaultModel, "inherit");
36
+ assert.deepEqual(all.agentOverrides?.oracle, { model: "inherit" });
37
+ assert.equal(JSON.stringify(buildTier("all-inherit")), JSON.stringify(all));
38
+
39
+ const light = buildTier("light");
40
+ assert.equal(light.defaultModel, "opencode-go/hy3");
41
+ assert.equal(light.defaultThinking, "high");
42
+ assert.equal(light.agentOverrides?.scout?.thinking, "low");
43
+ assert.deepEqual(light.agentOverrides?.oracle?.fallbackModels, ["opencode-go/deepseek-v4-flash"]);
44
+ assert.equal(light.agentOverrides?.["poteto-agent"]?.thinking, "high");
45
+ });
46
+
47
+ test("resolvedMapping falls back default -> inherit and includes extras", () => {
48
+ const light = buildTier("light");
49
+ const mapped = resolvedMapping(light, ["default", "scout", "oracle", "poteto-agent"]);
50
+ assert.deepEqual(mapped.map((row) => [row.role, row.model]), [
51
+ ["default", "opencode-go/hy3"],
52
+ ["scout", "inherit"],
53
+ ["oracle", "inherit"],
54
+ ["poteto-agent", "inherit"],
55
+ ]);
56
+ assert.ok(mapped.find((row) => row.role === "oracle")?.extra?.includes("fallback:"));
57
+ const empty = resolvedMapping({}, ["default", "worker"]);
58
+ assert.deepEqual(empty.map((row) => row.model), ["inherit", "inherit"]);
59
+ });
60
+
61
+ test("saveSettings merges into existing settings, never clobbers", () => {
62
+ const dir = mkdtempSync(join(tmpdir(), "pi-pstack-settings-"));
63
+ const path = join(dir, "settings.json");
64
+ writeFileSync(
65
+ path,
66
+ JSON.stringify({ theme: "dark", defaultProvider: "opencode-go", subagents: { defaultModel: "inherit" } }),
67
+ );
68
+ saveSettings(path, { defaultThinking: "high", agentOverrides: { scout: { model: "inherit" } } });
69
+ const file = JSON.parse(readFileSync(path, "utf8"));
70
+ assert.equal(file.theme, "dark", "unrelated top-level keys survive");
71
+ assert.equal(file.defaultProvider, "opencode-go");
72
+ assert.deepEqual(
73
+ file.subagents,
74
+ {
75
+ defaultModel: "inherit",
76
+ defaultThinking: "high",
77
+ agentOverrides: { scout: { model: "inherit" } },
78
+ },
79
+ "prior subagents keys survive and the new map merges in",
80
+ );
81
+ const loaded = loadSettings(path);
82
+ assert.equal(loaded.defaultModel, "inherit");
83
+ assert.equal(loaded.defaultThinking, "high");
84
+ assert.deepEqual(loaded.agentOverrides, { scout: { model: "inherit" } });
85
+ });
86
+
87
+ test("loadSettings returns {} when the file is missing or malformed", () => {
88
+ assert.deepEqual(loadSettings(join(tmpdir(), "does-not-exist.json")), {});
89
+ const dir = mkdtempSync(join(tmpdir(), "pi-pstack-bad-"));
90
+ const path = join(dir, "settings.json");
91
+ saveSettings(path, { defaultModel: "inherit" });
92
+ assert.equal(loadSettings(path).defaultModel, "inherit");
93
+ });
94
+
95
+ test("lastModeEnabled returns the last pstack-mode entry", () => {
96
+ const entries: ModeEntry[] = [
97
+ { type: "message", customType: undefined, data: undefined },
98
+ { type: "custom", customType: "pstack-mode", data: { enabled: true } },
99
+ { type: "custom", customType: "pstack-mode", data: { enabled: false } },
100
+ { type: "custom", customType: "other", data: { enabled: true } },
101
+ ];
102
+ assert.equal(lastModeEnabled(entries), false);
103
+ assert.equal(lastModeEnabled(entries.slice(0, 2)), true);
104
+ assert.equal(lastModeEnabled([]), false);
105
+ });
106
+
107
+ test("POTETO_DIRECTIVE names the skill as the source of truth", () => {
108
+ assert.ok(POTETO_DIRECTIVE.includes("poteto-mode skill"));
109
+ assert.ok(POTETO_DIRECTIVE.includes("/poteto-mode off"));
110
+ });