@ferris1225/pi-subagents 4.2.13 → 4.3.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/src/setup.ts CHANGED
@@ -1,28 +1,28 @@
1
1
  /**
2
2
  * Interactive configuration wizard for /subagents-setup.
3
3
  *
4
- * The wizard stays one level deep and exposes only what most users touch:
5
- * which agents run and the model and thinking strength each runs on.
6
- * Everything else (agent scope, idle timeout, result lines, notifications) is
7
- * config-file-only; model failures hand directly to the current main model,
8
- * and thinking defaults to capability-aware Auto.
4
+ * The wizard stays one level deep: which agents run, then a model and an
5
+ * optional thinking override per agent. Role thinking defaults apply until
6
+ * the user picks a level. Everything else (agent scope, idle timeout, result
7
+ * lines) is config-file-only.
9
8
  */
10
9
 
11
10
  import { stat } from "node:fs/promises";
12
11
  import type { Api, Model } from "@earendil-works/pi-ai";
13
12
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
14
- import { discoverAgents } from "./agents.ts";
15
13
  import {
14
+ AGENT_PROFILES,
16
15
  BUILTIN_AGENT_NAMES,
17
16
  DEFAULT_CONFIG,
18
17
  DEFAULT_ENABLED_AGENTS,
19
- DEFAULT_THINKING_LEVEL,
20
- type SubagentsConfig,
21
- type ThinkingLevel,
18
+ agentProfile,
22
19
  errorMessage,
23
20
  getConfigPath,
24
21
  loadConfig,
22
+ roleThinkingLevel,
25
23
  saveConfig,
24
+ type SubagentsConfig,
25
+ type ThinkingLevel,
26
26
  } from "./config.ts";
27
27
  import {
28
28
  CURRENT_MAIN_MODEL,
@@ -37,15 +37,30 @@ import {
37
37
  } from "./models.ts";
38
38
  import { promptSelectMany, promptSelectOne } from "./ui.ts";
39
39
 
40
- /** Short, selection-friendly descriptions for the built-in agents. */
41
- const MODULE_HINTS: Record<string, string> = {
42
- explorer: "read-only codebase recon (fast, read-only tools)",
43
- executor: "implement / fix / clean up / docs sync / merge results (full tools)",
40
+ const THINKING_LEVEL_HINTS: Record<ThinkingLevel, string> = {
41
+ off: "no reasoning tokens",
42
+ minimal: "minimal reasoning",
43
+ low: "light reasoning",
44
+ medium: "balanced reasoning",
45
+ high: "deep reasoning",
46
+ xhigh: "extra-deep reasoning",
47
+ max: "strongest reasoning",
44
48
  };
45
49
 
50
+ function agentPickerItems(): Array<{ value: string; label: string; description: string }> {
51
+ return BUILTIN_AGENT_NAMES.map((name) => {
52
+ const profile = AGENT_PROFILES[name];
53
+ return {
54
+ value: name,
55
+ label: name,
56
+ description: `${profile.summary} — ${profile.remark}`,
57
+ };
58
+ });
59
+ }
60
+
46
61
  function moduleLabel(name: string): string {
47
- const hint = MODULE_HINTS[name];
48
- return hint ? `${name} — ${hint}` : name;
62
+ const profile = agentProfile(name);
63
+ return profile ? `${name} — ${profile.summary}` : name;
49
64
  }
50
65
 
51
66
  async function configExists(configPath: string): Promise<boolean> {
@@ -61,14 +76,14 @@ async function pickEnabledAgents(
61
76
  ctx: ExtensionCommandContext,
62
77
  current: readonly string[],
63
78
  ): Promise<string[] | undefined> {
64
- const items = BUILTIN_AGENT_NAMES.map((name) => ({ value: name, label: moduleLabel(name) }));
65
- return promptSelectMany(
79
+ const picked = await promptSelectMany(
66
80
  ctx,
67
- "Enable which sub-agents?",
68
- "Space toggles • Enter confirms • Esc returns to settings",
69
- items,
81
+ "Which agents should run?",
82
+ "Each line is a role and its job. Space toggles • Enter confirms • Esc back",
83
+ agentPickerItems(),
70
84
  current,
71
85
  );
86
+ return picked;
72
87
  }
73
88
 
74
89
  async function pickConfiguredModel(
@@ -86,7 +101,7 @@ async function pickConfiguredModel(
86
101
  return promptSelectOne(
87
102
  ctx,
88
103
  title,
89
- `Type to filter by provider, model, capability, or thinking level • ↑/↓ • Enter selects • Esc ${escNote}`,
104
+ `Type to filter by provider, model, or capability • ↑/↓ • Enter selects • Esc ${escNote}`,
90
105
  items,
91
106
  configuredRef ?? CURRENT_MAIN_MODEL,
92
107
  );
@@ -98,34 +113,11 @@ async function pickAgentModel(
98
113
  currentRef: string | undefined,
99
114
  escNote = "cancels this setup pass",
100
115
  ): Promise<string | undefined> {
101
- return pickConfiguredModel(ctx, `Model for "${agentName}"?`, currentRef, escNote);
102
- }
103
-
104
- const AUTO_THINKING = "__auto_thinking__";
105
-
106
- function actualAgentThinkingDefault(
107
- ctx: ExtensionCommandContext,
108
- config: SubagentsConfig,
109
- agentName: string,
110
- ): ThinkingLevel {
111
- const { agents } = discoverAgents(ctx.cwd, {
112
- scope: config.agentScope,
113
- enabledNames: config.enabledAgents,
114
- projectTrusted: ctx.isProjectTrusted(),
115
- });
116
- return agents.find((agent) => agent.name === agentName)?.thinking ?? DEFAULT_THINKING_LEVEL;
116
+ const profile = agentProfile(agentName);
117
+ const duty = profile ? ` — ${profile.summary}` : "";
118
+ return pickConfiguredModel(ctx, `Model for ${agentName}${duty}?`, currentRef, escNote);
117
119
  }
118
120
 
119
- const THINKING_LEVEL_HINTS: Record<ThinkingLevel, string> = {
120
- off: "no reasoning tokens",
121
- minimal: "minimal reasoning",
122
- low: "light reasoning",
123
- medium: "balanced reasoning",
124
- high: "deep reasoning",
125
- xhigh: "extra-deep reasoning",
126
- max: "strongest reasoning",
127
- };
128
-
129
121
  function effectiveModelForChoice(
130
122
  ctx: ExtensionCommandContext,
131
123
  choice: string,
@@ -134,41 +126,37 @@ function effectiveModelForChoice(
134
126
  return findModelByRef(availableModelsInScope(ctx), choice);
135
127
  }
136
128
 
137
- /** Auto is the default. Manual rows are exactly the levels Pi exposes for the
138
- * selected model; unsupported xhigh/max entries never appear. */
129
+ /** Role default is the first row. Picking it clears a stored override. */
139
130
  async function pickAgentStrength(
140
131
  ctx: ExtensionCommandContext,
141
132
  agentName: string,
142
133
  model: Model<Api> | undefined,
143
134
  current: ThinkingLevel | undefined,
144
- agentDefault: ThinkingLevel,
145
135
  escNote = "cancels this setup pass",
146
- ): Promise<ThinkingLevel | typeof AUTO_THINKING | undefined> {
136
+ ): Promise<ThinkingLevel | undefined> {
147
137
  const supported = supportedThinkingLevels(model);
148
- const automatic = resolveThinkingLevel(model, agentDefault);
149
- // No model metadata, or a non-reasoning model whose only valid value is off:
150
- // Auto is already the complete and least surprising choice.
151
- if (supported.length <= 1) return AUTO_THINKING;
138
+ const roleDefault = resolveThinkingLevel(model, roleThinkingLevel(agentName));
139
+ if (supported.length <= 1) return roleDefault;
152
140
 
153
- const currentEffective = current ? resolveThinkingLevel(model, current) : undefined;
141
+ const currentEffective = current ? resolveThinkingLevel(model, current) : roleDefault;
154
142
  const modelName = model ? modelRef(model) : "current main model";
155
- const options = [
156
- {
157
- value: AUTO_THINKING,
158
- label: `auto ${automatic} for ${modelName}${current === undefined ? " (current, recommended)" : " (recommended)"}`,
159
- },
160
- ...supported.map((level) => ({
143
+ const options = supported.map((level) => {
144
+ const tags = [
145
+ level === roleDefault ? "role default" : "",
146
+ current !== undefined && currentEffective === level ? "current" : "",
147
+ ].filter(Boolean);
148
+ return {
161
149
  value: level,
162
- label: `${level} — ${THINKING_LEVEL_HINTS[level]}${current !== undefined && currentEffective === level ? " (current)" : ""}`,
163
- })),
164
- ];
150
+ label: `${level} — ${THINKING_LEVEL_HINTS[level]}${tags.length ? ` (${tags.join(", ")})` : ""}`,
151
+ };
152
+ });
165
153
  return promptSelectOne(
166
154
  ctx,
167
- `Thinking for "${agentName}"?`,
168
- `Only levels supported by ${modelName} are shown • Enter selects • Esc ${escNote}`,
155
+ `Thinking for ${agentName}?`,
156
+ `${agentName} defaults to ${roleDefault} on ${modelName} • Enter selects • Esc ${escNote}`,
169
157
  options,
170
- current === undefined ? AUTO_THINKING : currentEffective,
171
- ) as Promise<ThinkingLevel | typeof AUTO_THINKING | undefined>;
158
+ currentEffective,
159
+ ) as Promise<ThinkingLevel | undefined>;
172
160
  }
173
161
 
174
162
  async function pickAgentToConfigure(
@@ -182,19 +170,24 @@ async function pickAgentToConfigure(
182
170
  return promptSelectOne(
183
171
  ctx,
184
172
  "Configure which agent?",
185
- "Type to filter • ↑/↓ • Enter selects • Esc returns to settings",
186
- enabledAgents.map((name) => ({ value: name, label: moduleLabel(name) })),
173
+ "Name, then what it owns • ↑/↓ • Enter selects • Esc returns to settings",
174
+ enabledAgents.map((name) => {
175
+ const profile = agentProfile(name);
176
+ return {
177
+ value: name,
178
+ label: moduleLabel(name),
179
+ description: profile?.remark,
180
+ };
181
+ }),
187
182
  );
188
183
  }
189
184
 
190
185
  interface ConfiguredAgentChoice {
191
186
  name: string;
192
187
  model: string;
193
- strength: ThinkingLevel | typeof AUTO_THINKING;
188
+ strength: ThinkingLevel;
194
189
  }
195
190
 
196
- /** Configure one agent while preserving the UI back stack: thinking → model →
197
- * agent selection. Esc from agent selection ends this configuration pass. */
198
191
  async function configureOneAgent(
199
192
  ctx: ExtensionCommandContext,
200
193
  config: SubagentsConfig,
@@ -202,6 +195,8 @@ async function configureOneAgent(
202
195
  while (true) {
203
196
  const name = await pickAgentToConfigure(ctx, config.enabledAgents);
204
197
  if (name === undefined) return undefined;
198
+ const profile = agentProfile(name);
199
+ if (profile) ctx.ui.notify(`${name}: ${profile.remark}`, "info");
205
200
 
206
201
  while (true) {
207
202
  const modelChoice = await pickAgentModel(
@@ -217,7 +212,6 @@ async function configureOneAgent(
217
212
  name,
218
213
  model,
219
214
  config.agentThinkingLevels[name],
220
- actualAgentThinkingDefault(ctx, config, name),
221
215
  "returns to model selection",
222
216
  );
223
217
  if (strength === undefined) continue;
@@ -231,12 +225,42 @@ function keepAgentEntries<T>(record: Record<string, T>, enabled: readonly string
231
225
  return Object.fromEntries(Object.entries(record).filter(([name]) => keep.has(name)));
232
226
  }
233
227
 
228
+ function applyThinkingChoice(
229
+ levels: Record<string, ThinkingLevel>,
230
+ agentName: string,
231
+ strength: ThinkingLevel,
232
+ ): Record<string, ThinkingLevel> {
233
+ const next = { ...levels };
234
+ if (strength === roleThinkingLevel(agentName)) delete next[agentName];
235
+ else next[agentName] = strength;
236
+ return next;
237
+ }
238
+
239
+ async function introduceSetup(ctx: ExtensionCommandContext): Promise<boolean> {
240
+ const lines = BUILTIN_AGENT_NAMES.map((name) => {
241
+ const profile = AGENT_PROFILES[name];
242
+ return `${name} — ${profile.summary}. ${profile.remark}`;
243
+ });
244
+ ctx.ui.notify(
245
+ `pi-subagents: ${lines.join(" ")} Pick a model for each role next. Thinking defaults per role (scout low, artisan high, steward medium); change it on a role when you want.`,
246
+ "info",
247
+ );
248
+ const choice = await ctx.ui.select("How to configure pi-subagents", [
249
+ "Continue — pick a model for each role (thinking has a role default you can change later)",
250
+ ]);
251
+ return choice !== undefined;
252
+ }
253
+
234
254
  async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, base: SubagentsConfig): Promise<boolean> {
255
+ if (!(await introduceSetup(ctx))) return false;
256
+
235
257
  const enabled = await pickEnabledAgents(ctx, base.enabledAgents);
236
258
  if (enabled === undefined) return false;
237
259
 
238
260
  let agentModels = keepAgentEntries(base.agentModels, enabled);
239
261
  for (const agentName of enabled) {
262
+ const profile = agentProfile(agentName);
263
+ if (profile) ctx.ui.notify(`${agentName}: ${profile.remark}`, "info");
240
264
  const choice = await pickAgentModel(ctx, agentName, agentModels[agentName]);
241
265
  if (choice === undefined) return false;
242
266
  agentModels = applyAgentModelChoice(agentModels, agentName, choice);
@@ -244,27 +268,26 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
244
268
 
245
269
  const next: SubagentsConfig = {
246
270
  enabledAgents: enabled,
247
- // The wizard surfaces every built-in, so an untoggled one was seen and
248
- // deliberately left off — record them all as known.
249
- knownAgents: [...BUILTIN_AGENT_NAMES],
250
271
  agentModels,
251
- // Full setup returns every agent to capability-aware Auto thinking.
252
- agentThinkingLevels: {},
272
+ agentThinkingLevels: keepAgentEntries(base.agentThinkingLevels, enabled),
253
273
  maxResultLines: base.maxResultLines,
254
274
  agentScope: base.agentScope,
255
275
  idleTimeoutSec: base.idleTimeoutSec,
256
276
  };
257
277
  await saveConfig(next, configPath);
258
- ctx.ui.notify(`pi-subagents configured with Auto thinking. Saved to ${configPath}`, "info");
278
+ ctx.ui.notify(
279
+ `pi-subagents saved to ${configPath}. Role thinking defaults apply; open Configure an agent to change one.`,
280
+ "info",
281
+ );
259
282
  return true;
260
283
  }
261
284
 
262
285
  async function runMenu(ctx: ExtensionCommandContext, configPath: string, config: SubagentsConfig): Promise<void> {
263
286
  while (true) {
264
287
  const choice = await ctx.ui.select("pi-subagents settings", [
265
- "Enable/disable agents",
266
- "Configure an agent (model + thinking)",
267
- "Full re-setup",
288
+ "Enable/disable agents — choose which roles are available",
289
+ "Configure an agent model and thinking, with its job on the row",
290
+ "Full re-setup — walk through the team and pick models again",
268
291
  ]);
269
292
  if (choice === undefined) return;
270
293
  if (choice.startsWith("Full")) {
@@ -281,33 +304,16 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
281
304
  const enabled = await pickEnabledAgents(ctx, config.enabledAgents);
282
305
  if (enabled === undefined) continue;
283
306
  next.enabledAgents = enabled;
284
- // A newly enabled role inherits explorer's configured model and
285
- // thinking level, so the file reflects what it will actually run
286
- // instead of silently falling back to the current main model: these
287
- // roles do light migration-grade work on the fast explorer lane.
288
- for (const agent of enabled) {
289
- if (agent === "explorer" || config.enabledAgents.includes(agent)) continue;
290
- if (!next.agentModels[agent] && config.agentModels.explorer) {
291
- next.agentModels[agent] = config.agentModels.explorer;
292
- }
293
- if (!next.agentThinkingLevels[agent] && config.agentThinkingLevels.explorer) {
294
- next.agentThinkingLevels[agent] = config.agentThinkingLevels.explorer;
295
- }
296
- }
297
307
  next.agentModels = keepAgentEntries(next.agentModels, enabled);
298
308
  next.agentThinkingLevels = keepAgentEntries(next.agentThinkingLevels, enabled);
299
309
  } else {
300
- // Per-agent loop: thinking Esc returns to that agent's model picker;
301
- // model Esc returns to the agent picker; agent-picker Esc saves completed
302
- // choices and returns to this settings menu.
303
310
  let configuredAny = false;
304
311
  while (true) {
305
312
  const picked = await configureOneAgent(ctx, next);
306
313
  if (!picked) break;
307
314
  configuredAny = true;
308
315
  next.agentModels = applyAgentModelChoice(next.agentModels, picked.name, picked.model);
309
- if (picked.strength === AUTO_THINKING) delete next.agentThinkingLevels[picked.name];
310
- else next.agentThinkingLevels[picked.name] = picked.strength;
316
+ next.agentThinkingLevels = applyThinkingChoice(next.agentThinkingLevels, picked.name, picked.strength);
311
317
  }
312
318
  if (!configuredAny) continue;
313
319
  await saveConfig(next, configPath);
package/src/spawn.ts CHANGED
@@ -115,16 +115,18 @@ export const RESULT_ARTIFACT_MAX_FILES_PER_PROJECT = 50;
115
115
  // Explicit current prefix plus the strict timestamp/token convention used by 1.1.0.
116
116
  const RESULT_ARTIFACT_NAME = /^(?:pi-subagent-\d{13,}-[0-9a-f]{12}|\d{13,}-[a-z0-9]{6})-[\w.-]+\.md$/;
117
117
 
118
- /** Name of the single per-extension root under the pi home directory that
119
- * holds every project-scoped artifact (sessions, worktrees, result excerpts).
120
- * Nothing long-lived is written to the OS temp directory. */
118
+ /** Name of the single internal-state root under the pi agent directory. */
121
119
  export const PROJECT_ROOTS_DIR_NAME = "ferris-pi-subagents";
122
120
 
121
+ export function getSubagentsRoot(configPath: string): string {
122
+ return join(dirname(configPath), PROJECT_ROOTS_DIR_NAME);
123
+ }
124
+
123
125
  /** Per-project directory that groups every durable artifact of one checkout:
124
126
  * `<pi home>/ferris-pi-subagents/<project-slug-hash>/{sessions,worktrees,results}`.
125
127
  * Callers join the kind-specific subdirectory themselves. */
126
128
  export function getProjectRoot(configPath: string, cwd?: string): string {
127
- return join(dirname(configPath), PROJECT_ROOTS_DIR_NAME, resultArtifactProjectKey(cwd));
129
+ return join(getSubagentsRoot(configPath), resultArtifactProjectKey(cwd));
128
130
  }
129
131
 
130
132
  interface ResultArtifactRetentionOptions {