@ferris1225/pi-subagents 4.1.9 → 4.1.11

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/prompt.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  /**
2
- * Builds the authoritative delegation directive injected into the parent model's
3
- * system prompt via `before_agent_start`. Tool metadata stays intentionally
4
- * minimal so role/process guidance is not paid for twice.
2
+ * Builds the delegation directive injected into the parent model's
3
+ * system prompt via `before_agent_start`. It is paid on every turn, so it
4
+ * stays a lean routing contract when a child context pays for itself,
5
+ * how wide to fan out, and how results come back. Tool metadata stays
6
+ * intentionally minimal so role/process guidance is not paid for twice.
5
7
  */
6
8
 
7
9
  import type { AgentConfig } from "./agents.ts";
@@ -22,13 +24,10 @@ export function buildDelegationDirective(
22
24
  const hasCleaner = agents.some((agent) => agent.name === "cleaner");
23
25
  const hasDocumenter = agents.some((agent) => agent.name === "documenter");
24
26
  const hasReviewer = agents.some((agent) => agent.name === "reviewer");
25
- const hasMultiple = agents.length > 1;
26
- const autoFixEnabled = hasWorker;
27
27
  const codeWriterNames = [
28
28
  ...(hasWorker ? ["worker"] : []),
29
29
  ...(hasCleaner ? ["cleaner"] : []),
30
30
  ];
31
- const reviewedWriterNames = [...codeWriterNames];
32
31
  const namedWorktreeTargets = [
33
32
  ...(hasWorker ? ["worker"] : []),
34
33
  ...(hasCleaner ? ["cleaner"] : []),
@@ -39,71 +38,54 @@ export function buildDelegationDirective(
39
38
  : namedWorktreeTargets.length === 1
40
39
  ? `${namedWorktreeTargets[0]} or another`
41
40
  : `${namedWorktreeTargets.slice(0, -1).join(", ")}, ${namedWorktreeTargets.at(-1)}, or another`;
42
- const managedWriterWorkflowRule = reviewedWriterNames.length === 0
43
- ? undefined
44
- : hasReviewer && hasDocumenter
45
- ? `Successful top-level ${reviewedWriterNames.join("/")} runs continue through the enabled reviewer gate. Only REVIEW_PASS can authorize documenter, which runs for DOCUMENTATION: NEEDED or a missing marker; the workflow delivers once. Never duplicate stages.`
46
- : hasReviewer
47
- ? `Successful top-level ${reviewedWriterNames.join("/")} runs continue through the enabled reviewer gate and then deliver once; never duplicate the gate.`
48
- : hasDocumenter
49
- ? `With reviewer disabled, successful top-level ${reviewedWriterNames.join("/")} runs use documenter as the conservative final fallback and then deliver once; never duplicate the fallback.`
50
- : undefined;
51
41
 
52
42
  const dispatchRules = [
53
- "Keep small, known-target work in the main thread with direct tools: lookups and focused reads/edits do not justify a child context.",
43
+ "Route substantive work to sub-agents so your context stays lean for orchestration; inline only trivial work — a one-shot lookup, a single focused read/edit, or an answer already in context.",
54
44
  ...(hasExplorer
55
45
  ? [
56
- "Use `explorer` proactively only for broad or cross-file reconnaissance: mapping unfamiliar code, tracing symbols/dependencies, or finding multi-file references. It is a lightweight retrieval index, never an automatic gate. Re-read load-bearing files before edits or high-risk decisions. Use a stronger model/specialist for dynamic, concurrent, migration, or security analysis.",
46
+ "Use `explorer` for any broad or multi-file search; it is a retrieval index, never a gate — re-read load-bearing files before acting on its findings.",
57
47
  ]
58
48
  : []),
59
49
  ...(hasWorker
60
- ? ["Use `worker` for a self-contained implementation, fix, refactor, or test whose separate context pays for itself—not a small known-target edit."]
50
+ ? ["Use `worker` for a self-contained implementation, fix, refactor, or test whose separate context pays for itself."]
61
51
  : []),
62
52
  ...(hasCleaner
63
53
  ? [
64
- `Use \`cleaner\` only for user-authorized cleanup, removal, simplification, or duplicate-code consolidation; it applies every safe proven in-scope cut without item-by-item approval. Read-only audits and code-health reviews go to ${hasReviewer ? "`reviewer`" : "the main context because `reviewer` is disabled"}. Never dispatch cleaner by PR count or as the pre-commit gate.`,
54
+ `Use \`cleaner\` only for user-authorized cleanup or deduplication; it applies every safe proven cut without item-by-item approval, and never runs as a pre-commit gate or by PR count.`,
65
55
  ]
66
56
  : []),
67
57
  ...(hasDocumenter
68
58
  ? [
69
- `Use \`documenter\` directly only for explicit whole-codebase or standalone documentation/comment work; a top-level documenter delivers without an automatic reviewer.${codeWriterNames.length > 0 ? ` ${codeWriterNames.join("/")} must sync existing docs they directly affect; runtime runs documenter only after REVIEW_PASS with DOCUMENTATION: NEEDED or a missing marker, or as the reviewer-disabled fallback—never dispatch a duplicate.` : ""} It never changes runtime behavior, versions, or release state.`,
59
+ `Use \`documenter\` directly only for explicit standalone documentation work; a top-level documenter delivers without a gate.${codeWriterNames.length > 0 ? ` The runtime runs the final docs sync after REVIEW_PASS with DOCUMENTATION: NEEDED or a missing marker, or as the reviewer-disabled fallback; writers sync docs they directly affect never dispatch a duplicate.` : ""}`,
70
60
  ]
71
61
  : []),
72
62
  ...(hasReviewer
73
63
  ? [
74
- `Use \`reviewer\` for read-only assessments or a gate.${reviewedWriterNames.length > 0 ? ` Successful ${reviewedWriterNames.join("/")} runs already get one fresh read-only reviewer gate, independent of the writer.` : ""} Advisory output has no VERDICT and cannot authorize follow-up edits${hasDocumenter ? "; gates classify docs separately for the enabled documenter." : "."} Re-verifying your own fixes? Dispatch with \`advisory: true\`: the report returns to you and never starts the auto-fix chain.`,
64
+ `Use \`reviewer\` for read-only assessments or gates.${codeWriterNames.length > 0 ? ` Successful ${codeWriterNames.join("/")} runs already get one fresh gate and then deliver once.` : ""} Advisory output has no VERDICT and cannot authorize edits; dispatch your own re-verification with \`advisory: true\`.`,
75
65
  ]
76
66
  : []),
77
- "Brief each child with the complete goal, exact paths, constraints, and expected output; it has no conversation memory.",
78
- "Children are leaf processes without delegation tools; use `subagent_control resume` on a parked/settled thread to continue its retained context.",
79
- ...(hasMultiple
80
- ? [
81
- "Dispatch independent work in one `tasks` array and let the resumed main agent start dependent work only after prerequisites finish.",
82
- ]
83
- : []),
84
- `Filesystem isolation: single tasks default to shared${hasWorker ? "; parallel worker tasks default to detached Git worktrees" : ""}${hasCleaner ? "; cleaner defaults to shared" : ""}${hasDocumenter ? "; documenter defaults to shared" : ""}. Request \`isolation: "worktree"\` only for ${worktreeTargets} write-capable agent in a Git repository with committed HEAD. Read-only agents reject it, and setup/integration failure never falls back silently to shared.`,
67
+ "You own the fan-out breadth: every genuinely independent unit goes in one `tasks` array — there is no per-call cap, and extra tasks queue for the next free process slot. One child owns one coherent deliverable and its files; no two children share a file or re-answer the same question; dependent work starts only after its prerequisite delivers.",
68
+ "Brief each child with the complete goal, exact paths, constraints, and expected output; it has no conversation memory and cannot delegate. Continue a parked/settled thread with `subagent_control resume`.",
69
+ `Single tasks share the checkout${hasWorker ? "; parallel workers default to detached Git worktrees" : ""}. Request \`isolation: "worktree"\` only for ${worktreeTargets} write-capable agent in a Git repo with committed HEAD; setup failure never silently falls back to shared.`,
85
70
  "A configured child model/provider failure automatically continues the same retained session on the current main model; do not redispatch. Ordinary tool/task failures stay on the selected model.",
86
- "Trust but verify: inspect actual changes/results before reporting completion.",
87
71
  ];
88
72
 
89
73
  const handoffRules = [
90
- "Dispatch ends this turn; results resume the main agent, even mid-turn. Never sleep, poll, or call `subagent_wait` to hold the turn.",
91
- "Use `subagent_wait` with explicit `timeoutMs` only when the user asks to wait in-turn; its default lookup is non-blocking.",
92
- "Results are already shown. Do not restate, paraphrase, or re-summarize them; add only your conclusion or next action.",
93
- "A delivered result does not mean siblings are finished. Before declaring the overall task done, use `subagent_status` to confirm that no runs remain active.",
74
+ "Dispatch ends this turn; each completion resumes the main agent automatically never sleep, poll, or call `subagent_wait` to hold the turn.",
75
+ "Results are already shown; add only your conclusion or next action, never a restatement.",
76
+ "Before declaring the overall task done, use `subagent_status` to confirm that no runs remain active.",
94
77
  ];
95
78
 
96
79
  const verificationRules = [
97
- "Never report an unrun check as passed; identify unavailable checks and pre-existing failures honestly.",
98
- ...(managedWriterWorkflowRule ? [managedWriterWorkflowRule] : []),
80
+ "Never report an unrun check as passed; surface unavailable checks and pre-existing failures honestly, and inspect actual changes before reporting completion.",
99
81
  ...(hasReviewer
100
82
  ? [
101
83
  ...(hasDocumenter
102
84
  ? [
103
- `A direct REVIEW_PASS with DOCUMENTATION: CLEAN delivers immediately; NEEDED or a missing marker runs one documentation sync. A direct REVIEW_FAIL ${autoFixEnabled ? "keeps bounded worker/reviewer auto-fix, with docs considered only after its terminal REVIEW_PASS." : "cannot start fixes while worker is disabled."}`,
85
+ "A direct REVIEW_PASS with DOCUMENTATION: CLEAN delivers immediately; NEEDED or a missing marker runs one documentation sync.",
104
86
  ]
105
87
  : []),
106
- "Resolve every gate finding; do not bypass the auto-fix/re-review cap. A reviewer report without a standalone VERDICT is advisory and cannot trigger writes.",
88
+ "A REVIEW_FAIL — direct or from a managed gate returns the findings to you: resolve them yourself, inline or via a worker you brief, without waiting for the user; the runtime never auto-fixes. Ask only for genuinely destructive or scope-changing fixes. Advisory reports cannot trigger writes.",
107
89
  "Use multi-model cross-review only when explicitly requested or for genuinely high-risk security, unsafe/FFI, persistence-migration, or concurrency changes.",
108
90
  ]
109
91
  : []),
@@ -113,9 +95,9 @@ export function buildDelegationDirective(
113
95
  return `
114
96
  ## Sub-agent delegation (pi-subagents)
115
97
 
116
- The \`subagent\` tool starts isolated Pi child processes and context windows. Completions automatically resume the main agent.
98
+ The \`subagent\` tool runs isolated leaf Pi child processes and context windows; each completion automatically resumes the main agent.
117
99
 
118
- Available agents:
100
+ Agents:
119
101
  ${catalog}
120
102
 
121
103
  Dispatch:
package/src/runtime.ts CHANGED
@@ -50,7 +50,7 @@ export interface SubagentThread {
50
50
  executionCwd: string;
51
51
  thinkingLevel?: ThinkingLevel;
52
52
  isolation: IsolationMode;
53
- /** Report-only reviewer dispatch: verdicts never chain into auto-fix. */
53
+ /** Report-only reviewer dispatch: verdicts never chain into a managed workflow. */
54
54
  advisoryReview: boolean;
55
55
  worktree?: WorktreeIsolation;
56
56
  state: ThreadState;
@@ -137,8 +137,8 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
137
137
  // Computing this at delivery (emit) time — not when the item was
138
138
  // pushed — reflects the current monitor state, since finishing runs
139
139
  // are removed from the monitor before their completion is pushed.
140
- // Managed-workflow parents remain "running" through documenter,
141
- // reviewer, and any fix rounds, so they are included without a special case.
140
+ // Managed-workflow parents remain "running" through reviewer and
141
+ // documenter stages, so they are included without a special case.
142
142
  const active = monitor
143
143
  .getRuns()
144
144
  .filter((run) => isRunActiveStatus(run.status))
package/src/setup.ts CHANGED
@@ -1,25 +1,21 @@
1
1
  /**
2
2
  * Interactive configuration wizard for /subagents-setup.
3
3
  *
4
- * The UI intentionally has no backup pool or global thinking menu. Each agent
5
- * gets one optional model override; failures hand directly to the current main
6
- * model. Thinking defaults to Auto and manual choices are limited to levels Pi
7
- * reports as supported by the selected model.
4
+ * The wizard stays one level deep and exposes only what most users touch:
5
+ * which agents run, the model each runs on, and the delegation directive
6
+ * toggle. Everything else (per-agent thinking, agent scope, idle timeout,
7
+ * result lines, notifications) is config-file-only; model failures hand
8
+ * directly to the current main model, and thinking defaults to capability-
9
+ * aware Auto.
8
10
  */
9
11
 
10
12
  import { stat } from "node:fs/promises";
11
- import type { Api, Model } from "@earendil-works/pi-ai";
12
13
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
13
14
  import {
14
- AGENT_SCOPE_VALUES,
15
15
  BUILTIN_AGENT_NAMES,
16
16
  DEFAULT_CONFIG,
17
17
  DEFAULT_ENABLED_AGENTS,
18
- DEFAULT_IDLE_TIMEOUT_SEC,
19
- DEFAULT_THINKING_LEVEL,
20
- type AgentScope,
21
18
  type SubagentsConfig,
22
- type ThinkingLevel,
23
19
  errorMessage,
24
20
  getConfigPath,
25
21
  loadConfig,
@@ -31,28 +27,9 @@ import {
31
27
  availableModelsInScope,
32
28
  buildModelPickerItems,
33
29
  currentModelRef,
34
- findModelByRef,
35
30
  modelRef,
36
- resolveThinkingLevel,
37
- supportedThinkingLevels,
38
31
  } from "./models.ts";
39
32
  import { promptSelectMany, promptSelectOne } from "./ui.ts";
40
- import { discoverAgents } from "./agents.ts";
41
-
42
- const AUTO_THINKING = "__auto_thinking__";
43
-
44
- function actualAgentThinkingDefault(
45
- ctx: ExtensionCommandContext,
46
- config: SubagentsConfig,
47
- agentName: string,
48
- ): ThinkingLevel {
49
- const { agents } = discoverAgents(ctx.cwd, {
50
- scope: config.agentScope,
51
- enabledNames: config.enabledAgents,
52
- projectTrusted: ctx.isProjectTrusted(),
53
- });
54
- return agents.find((agent) => agent.name === agentName)?.thinking ?? DEFAULT_THINKING_LEVEL;
55
- }
56
33
 
57
34
  /** Short, selection-friendly descriptions for the built-in agents. */
58
35
  const MODULE_HINTS: Record<string, string> = {
@@ -121,61 +98,6 @@ async function pickAgentModel(
121
98
  return pickConfiguredModel(ctx, `Model for "${agentName}"?`, currentRef, escNote);
122
99
  }
123
100
 
124
- const THINKING_LEVEL_HINTS: Record<ThinkingLevel, string> = {
125
- off: "no reasoning tokens",
126
- minimal: "minimal reasoning",
127
- low: "light reasoning",
128
- medium: "balanced reasoning",
129
- high: "deep reasoning",
130
- xhigh: "extra-deep reasoning",
131
- max: "strongest reasoning",
132
- };
133
-
134
- function effectiveModelForChoice(
135
- ctx: ExtensionCommandContext,
136
- choice: string,
137
- ): Model<Api> | undefined {
138
- if (choice === CURRENT_MAIN_MODEL) return ctx.model;
139
- return findModelByRef(availableModelsInScope(ctx), choice);
140
- }
141
-
142
- /** Auto is the default. Manual rows are exactly the levels Pi exposes for the
143
- * selected model; unsupported xhigh/max entries never appear. */
144
- async function pickAgentStrength(
145
- ctx: ExtensionCommandContext,
146
- agentName: string,
147
- model: Model<Api> | undefined,
148
- current: ThinkingLevel | undefined,
149
- agentDefault: ThinkingLevel,
150
- escNote = "cancels this setup pass",
151
- ): Promise<ThinkingLevel | typeof AUTO_THINKING | undefined> {
152
- const supported = supportedThinkingLevels(model);
153
- const automatic = resolveThinkingLevel(model, agentDefault);
154
- // No model metadata, or a non-reasoning model whose only valid value is off:
155
- // Auto is already the complete and least surprising choice.
156
- if (supported.length <= 1) return AUTO_THINKING;
157
-
158
- const currentEffective = current ? resolveThinkingLevel(model, current) : undefined;
159
- const modelName = model ? modelRef(model) : "current main model";
160
- const options = [
161
- {
162
- value: AUTO_THINKING,
163
- label: `auto — ${automatic} for ${modelName}${current === undefined ? " (current, recommended)" : " (recommended)"}`,
164
- },
165
- ...supported.map((level) => ({
166
- value: level,
167
- label: `${level} — ${THINKING_LEVEL_HINTS[level]}${current !== undefined && currentEffective === level ? " (current)" : ""}`,
168
- })),
169
- ];
170
- return promptSelectOne(
171
- ctx,
172
- `Thinking for "${agentName}"?`,
173
- `Only levels supported by ${modelName} are shown • Enter selects • Esc ${escNote}`,
174
- options,
175
- current === undefined ? AUTO_THINKING : currentEffective,
176
- ) as Promise<ThinkingLevel | typeof AUTO_THINKING | undefined>;
177
- }
178
-
179
101
  async function pickAgentToConfigure(
180
102
  ctx: ExtensionCommandContext,
181
103
  enabledAgents: readonly string[],
@@ -195,11 +117,10 @@ async function pickAgentToConfigure(
195
117
  interface ConfiguredAgentChoice {
196
118
  name: string;
197
119
  model: string;
198
- strength: ThinkingLevel | typeof AUTO_THINKING;
199
120
  }
200
121
 
201
- /** Configure one agent while preserving the UI back stack: thinking model →
202
- * agent selection. Esc from agent selection ends this configuration pass. */
122
+ /** Configure agents while preserving the UI back stack: model selection returns
123
+ * to the agent picker on Esc; agent-picker Esc ends this configuration pass. */
203
124
  async function configureOneAgent(
204
125
  ctx: ExtensionCommandContext,
205
126
  config: SubagentsConfig,
@@ -207,27 +128,14 @@ async function configureOneAgent(
207
128
  while (true) {
208
129
  const name = await pickAgentToConfigure(ctx, config.enabledAgents);
209
130
  if (name === undefined) return undefined;
210
-
211
- while (true) {
212
- const modelChoice = await pickAgentModel(
213
- ctx,
214
- name,
215
- config.agentModels[name],
216
- "returns to agent selection",
217
- );
218
- if (modelChoice === undefined) break;
219
- const model = effectiveModelForChoice(ctx, modelChoice);
220
- const strength = await pickAgentStrength(
221
- ctx,
222
- name,
223
- model,
224
- config.agentThinkingLevels[name],
225
- actualAgentThinkingDefault(ctx, config, name),
226
- "returns to model selection",
227
- );
228
- if (strength === undefined) continue;
229
- return { name, model: modelChoice, strength };
230
- }
131
+ const model = await pickAgentModel(
132
+ ctx,
133
+ name,
134
+ config.agentModels[name],
135
+ "returns to agent selection",
136
+ );
137
+ if (model === undefined) continue;
138
+ return { name, model };
231
139
  }
232
140
  }
233
141
 
@@ -239,40 +147,6 @@ async function pickInjection(ctx: ExtensionCommandContext, current: boolean): Pr
239
147
  return choice.startsWith("On");
240
148
  }
241
149
 
242
- const IDLE_TIMEOUT_STEPS = [0, 30, 60, 90, 120, 180, 300, 600];
243
-
244
- async function pickCount(
245
- ctx: ExtensionCommandContext,
246
- title: string,
247
- steps: readonly number[],
248
- current: number,
249
- defaultValue: number,
250
- ): Promise<number | undefined> {
251
- const values = [...new Set([...steps, current])].sort((a, b) => a - b);
252
- const options = values.map((value) => {
253
- const tags = [value === current ? "current" : "", value === defaultValue ? "default" : ""]
254
- .filter(Boolean)
255
- .join(", ");
256
- return tags ? `${value} (${tags})` : String(value);
257
- });
258
- const choice = await ctx.ui.select(title, options);
259
- return choice === undefined ? undefined : Number.parseInt(choice, 10);
260
- }
261
-
262
- async function pickScope(ctx: ExtensionCommandContext, current: AgentScope): Promise<AgentScope | undefined> {
263
- const labels: Record<AgentScope, string> = {
264
- user: "user — built-in + ~/.pi/agent/agents (default)",
265
- project: "project — built-in + nearest .pi/agents only",
266
- both: "both — user agents, overridden by project agents",
267
- };
268
- const options = AGENT_SCOPE_VALUES.map((scope) =>
269
- scope === current ? `${labels[scope]} (current)` : labels[scope],
270
- );
271
- const choice = await ctx.ui.select("Which agent directories to discover from?", options);
272
- if (choice === undefined) return undefined;
273
- return AGENT_SCOPE_VALUES.find((scope) => choice.startsWith(scope));
274
- }
275
-
276
150
  function keepAgentEntries<T>(record: Record<string, T>, enabled: readonly string[]): Record<string, T> {
277
151
  const keep = new Set(enabled);
278
152
  return Object.fromEntries(Object.entries(record).filter(([name]) => keep.has(name)));
@@ -291,16 +165,6 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
291
165
 
292
166
  const injection = await pickInjection(ctx, base.proactiveInjection);
293
167
  if (injection === undefined) return false;
294
- const scope = await pickScope(ctx, base.agentScope);
295
- if (scope === undefined) return false;
296
- const idleTimeoutSec = await pickCount(
297
- ctx,
298
- "Idle timeout in seconds? (0 = disabled)",
299
- IDLE_TIMEOUT_STEPS,
300
- base.idleTimeoutSec,
301
- DEFAULT_IDLE_TIMEOUT_SEC,
302
- );
303
- if (idleTimeoutSec === undefined) return false;
304
168
 
305
169
  const next: SubagentsConfig = {
306
170
  enabledAgents: enabled,
@@ -310,49 +174,20 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
310
174
  notifyOnReviewPass: base.notifyOnReviewPass,
311
175
  maxResultLines: base.maxResultLines,
312
176
  proactiveInjection: injection,
313
- agentScope: scope,
314
- idleTimeoutSec,
177
+ agentScope: base.agentScope,
178
+ idleTimeoutSec: base.idleTimeoutSec,
315
179
  };
316
180
  await saveConfig(next, configPath);
317
181
  ctx.ui.notify(`pi-subagents configured with Auto thinking. Saved to ${configPath}`, "info");
318
182
  return true;
319
183
  }
320
184
 
321
- async function updateRuntimeSetting(
322
- ctx: ExtensionCommandContext,
323
- config: SubagentsConfig,
324
- ): Promise<SubagentsConfig | undefined> {
325
- while (true) {
326
- const choice = await ctx.ui.select("Runtime setting", [
327
- "Proactive injection",
328
- "Agent scope",
329
- "Idle timeout",
330
- ]);
331
- if (choice === undefined) return undefined;
332
- const next = { ...config };
333
- if (choice.startsWith("Proactive")) {
334
- const value = await pickInjection(ctx, config.proactiveInjection);
335
- if (value === undefined) continue;
336
- next.proactiveInjection = value;
337
- } else if (choice.startsWith("Agent scope")) {
338
- const value = await pickScope(ctx, config.agentScope);
339
- if (value === undefined) continue;
340
- next.agentScope = value;
341
- } else {
342
- const value = await pickCount(ctx, "Idle timeout in seconds?", IDLE_TIMEOUT_STEPS, config.idleTimeoutSec, DEFAULT_IDLE_TIMEOUT_SEC);
343
- if (value === undefined) continue;
344
- next.idleTimeoutSec = value;
345
- }
346
- return next;
347
- }
348
- }
349
-
350
185
  async function runMenu(ctx: ExtensionCommandContext, configPath: string, config: SubagentsConfig): Promise<void> {
351
186
  while (true) {
352
187
  const choice = await ctx.ui.select("pi-subagents settings", [
353
188
  "Enable/disable agents",
354
- "Configure an agent (model + thinking)",
355
- "Runtime settings",
189
+ "Configure agent models",
190
+ "Proactive injection",
356
191
  "Full re-setup",
357
192
  ]);
358
193
  if (choice === undefined) return;
@@ -395,17 +230,14 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
395
230
  next.agentModels = keepAgentEntries(next.agentModels, enabled);
396
231
  next.agentThinkingLevels = keepAgentEntries(next.agentThinkingLevels, enabled);
397
232
  } else if (choice.startsWith("Configure")) {
398
- // Per-agent loop: thinking Esc returns to that agent's model picker;
399
- // model Esc returns to the agent picker; agent-picker Esc saves completed
400
- // choices and returns to this settings menu.
233
+ // Per-agent loop: model Esc returns to the agent picker; agent-picker
234
+ // Esc saves completed choices and returns to this settings menu.
401
235
  let configuredAny = false;
402
236
  while (true) {
403
237
  const picked = await configureOneAgent(ctx, next);
404
238
  if (!picked) break;
405
239
  configuredAny = true;
406
240
  next.agentModels = applyAgentModelChoice(next.agentModels, picked.name, picked.model);
407
- if (picked.strength === AUTO_THINKING) delete next.agentThinkingLevels[picked.name];
408
- else next.agentThinkingLevels[picked.name] = picked.strength;
409
241
  }
410
242
  if (!configuredAny) continue;
411
243
  await saveConfig(next, configPath);
@@ -413,9 +245,9 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
413
245
  config = next;
414
246
  continue;
415
247
  } else {
416
- const updated = await updateRuntimeSetting(ctx, next);
417
- if (updated === undefined) continue;
418
- next = updated;
248
+ const injection = await pickInjection(ctx, next.proactiveInjection);
249
+ if (injection === undefined) continue;
250
+ next.proactiveInjection = injection;
419
251
  }
420
252
 
421
253
  await saveConfig(next, configPath);
@@ -46,13 +46,12 @@ import {
46
46
  } from "./format.ts";
47
47
  import {
48
48
  canStartManagedWorkflow,
49
- formatChainSummary,
50
49
  formatManagedWorkflowSummary,
51
50
  getManagedWorkflowPlan,
52
51
  workflowAgentAvailability,
53
52
  type ManagedWorkflowOutcome,
54
53
  type ManagedWorkflowPlan,
55
- } from "./fixloop.ts";
54
+ } from "./workflow.ts";
56
55
  import {
57
56
  availableModelsInScope,
58
57
  currentModelRef,
@@ -289,18 +288,23 @@ export interface ResumeReservation {
289
288
  }
290
289
 
291
290
  /** The dispatcher's full internal entry point; the public tool surface only
292
- * uses the first five parameters. */
291
+ * uses the first four parameters. */
292
+ export interface StartBackgroundOptions {
293
+ /** Resume path only: the thread whose retained context continues. */
294
+ existingThread?: SubagentThread;
295
+ appendedObjectiveOnResume?: boolean;
296
+ environment?: DispatchEnvironment;
297
+ seed?: SessionSeed;
298
+ resumeReservation?: ResumeReservation;
299
+ advisoryReview?: boolean;
300
+ }
301
+
293
302
  export type StartBackgroundInternal = (
294
303
  agentName: string,
295
304
  task: string,
296
305
  cwd: string | undefined,
297
306
  isolation?: IsolationMode,
298
- existingThread?: SubagentThread,
299
- appendedObjectiveOnResume?: boolean,
300
- environment?: DispatchEnvironment,
301
- seed?: SessionSeed,
302
- resumeReservation?: ResumeReservation,
303
- options?: { advisoryReview?: boolean },
307
+ options?: StartBackgroundOptions,
304
308
  ) => Promise<SingleResult>;
305
309
 
306
310
  export interface ThreadLifecycleDeps {
@@ -398,13 +402,15 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
398
402
  task: string,
399
403
  cwd: string | undefined,
400
404
  isolation: IsolationMode = "shared",
401
- existingThread?: SubagentThread,
402
- appendedObjectiveOnResume = false,
403
- environment?: DispatchEnvironment,
404
- seed?: SessionSeed,
405
- resumeReservation?: ResumeReservation,
406
- options?: { advisoryReview?: boolean },
405
+ startOptions: StartBackgroundOptions = {},
407
406
  ): Promise<SingleResult> => {
407
+ const {
408
+ existingThread,
409
+ appendedObjectiveOnResume = false,
410
+ environment,
411
+ seed,
412
+ resumeReservation,
413
+ } = startOptions;
408
414
  if (!runtime.sessionActive) {
409
415
  return failedStartResult(agentName, task, "Parent session shut down before this subagent generation could start.");
410
416
  }
@@ -421,7 +427,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
421
427
  const resolveLiveAgentTools = (candidate: AgentConfig): AgentConfig =>
422
428
  resolveAgentTools({ ...candidate, tools: discoveredAgent.tools }, runtime.getActiveTools());
423
429
  const resolvedAgent = resolveLiveAgentTools(discoveredAgent);
424
- const advisoryReview = options?.advisoryReview ?? existingThread?.advisoryReview ?? false;
430
+ const advisoryReview = startOptions.advisoryReview ?? existingThread?.advisoryReview ?? false;
425
431
  const agent = agentName === "reviewer"
426
432
  ? advisoryReview
427
433
  ? withAdvisoryReviewContract(resolvedAgent)
@@ -471,9 +477,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
471
477
  const priorTask = existingThread?.task;
472
478
  const priorSessionId = seed?.sessionId ?? existingThread?.sessionId;
473
479
  const priorSessionDir = seed?.sessionDir ?? existingThread?.sessionDir;
474
- if (existingThread && resumeReservation && !ownsResumeReservation(runtime, existingThread, resumeReservation)) {
475
- return failedStartResult(agentName, task, `Run #${existingThread.id} changed while resume was preparing; no new generation was started.`);
476
- }
477
480
  const runId = existingThread?.id ?? monitor.addRun(agent.name, task, route.agent.model, thinkingLevel, {
478
481
  isolation,
479
482
  ...(worktreeGroup ? { worktreeId: worktreeGroup } : {}),
@@ -532,13 +535,13 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
532
535
  // A newly admitted generation owns no output yet. Keeping the prior
533
536
  // generation here would make a queued stop publish stale task,
534
537
  // session metadata as this generation's partial.
535
- thread.lastResult = undefined;
536
- if (seed?.sessionId && seed.sessionDir) {
537
- thread.sessionId = seed.sessionId;
538
- thread.sessionDir = seed.sessionDir;
539
- }
540
- thread.retireOnSettle = false;
541
- thread.isolationFailureNotified = false;
538
+ thread.lastResult = undefined;
539
+ if (seed?.sessionId && seed.sessionDir) {
540
+ thread.sessionId = seed.sessionId;
541
+ thread.sessionDir = seed.sessionDir;
542
+ }
543
+ thread.retireOnSettle = false;
544
+ thread.isolationFailureNotified = false;
542
545
  } else {
543
546
  thread = {
544
547
  id: runId,
@@ -674,23 +677,20 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
674
677
  if (thread.retireOnSettle) runtime.retireThreadSession(thread);
675
678
  let workflowOutcome: ManagedWorkflowOutcome | undefined;
676
679
  const workflowPlan = getManagedWorkflowPlan(result, workflowAvailability, thread.advisoryReview);
677
- if (workflowPlan && runtime.sessionActive) {
678
- // The continuation is runtime-initiated (gate review, auto-fix
679
- // rounds, documentation sync): release this generation's
680
- // concurrency slot so managed chains never starve manual
681
- // dispatches. Cancellation and quiescence guarantees are
682
- // unchanged — the task stays abortable and awaited.
683
- runtime.backgroundQueue.suspend(generationController);
684
- thread.state = "running";
685
- // The stable parent row now represents workflow ownership, not whichever
686
- // model stage ran most recently. Internal rows own their exact role/model/
687
- // thinking/timing telemetry and remain independently queryable.
688
- monitor.setManagedWorkflow(runId, true);
689
- monitor.setStatus(runId, "running");
690
- monitor.setActivity(
691
- runId,
692
- workflowPlan.kind === "auto-fix" ? "auto-fix chain running" : "managed workflow running",
693
- );
680
+ if (workflowPlan && runtime.sessionActive) {
681
+ // The continuation is runtime-initiated (gate review,
682
+ // documentation sync): release this generation's
683
+ // concurrency slot so managed chains never starve manual
684
+ // dispatches. Cancellation and quiescence guarantees are
685
+ // unchanged — the task stays abortable and awaited.
686
+ runtime.backgroundQueue.suspend(generationController);
687
+ thread.state = "running";
688
+ // The stable parent row now represents workflow ownership, not whichever
689
+ // model stage ran most recently. Internal rows own their exact role/model/
690
+ // thinking/timing telemetry and remain independently queryable.
691
+ monitor.setManagedWorkflow(runId, true);
692
+ monitor.setStatus(runId, "running");
693
+ monitor.setActivity(runId, "managed workflow running");
694
694
  workflowOutcome = await runManagedWorkflow({
695
695
  plan: workflowPlan,
696
696
  initialResult: result,
@@ -793,9 +793,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
793
793
 
794
794
  if (workflowOutcome) {
795
795
  const lastStep = workflowOutcome.steps[workflowOutcome.steps.length - 1]!;
796
- let block = workflowOutcome.kind === "auto-fix"
797
- ? formatChainSummary(workflowOutcome.steps, result)
798
- : formatManagedWorkflowSummary(workflowOutcome.steps, result);
796
+ let block = formatManagedWorkflowSummary(workflowOutcome.steps, result);
799
797
  const finalVerdict = lastStep.result.agent === "reviewer"
800
798
  ? reviewVerdict(getResultOutput(lastStep.result))
801
799
  : undefined;
@@ -805,7 +803,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
805
803
  }
806
804
  if (modelLevel) block += `\n\n${modelLevelTakeoverNote(result, { runId })}`;
807
805
  runtime.sendCompletionGroup([{
808
- agent: `${workflowOutcome.kind === "auto-fix" ? "auto-fix chain" : "managed workflow"} (${result.agent})`,
806
+ agent: `managed workflow (${result.agent})`,
809
807
  block,
810
808
  triggerTurn: true,
811
809
  usage: sumUsage(workflowOutcome.steps.map((step) => step.result.usage)),
@@ -1197,15 +1195,17 @@ export function installThreadLifecycle(thread: SubagentThread, deps: ThreadLifec
1197
1195
  nextTask,
1198
1196
  thread.cwd,
1199
1197
  thread.isolation,
1200
- thread,
1201
- objective !== undefined,
1202
1198
  {
1203
- ctx: currentCtx,
1204
- config: currentConfig,
1205
- agents: currentAgents,
1199
+ existingThread: thread,
1200
+ appendedObjectiveOnResume: objective !== undefined,
1201
+ environment: {
1202
+ ctx: currentCtx,
1203
+ config: currentConfig,
1204
+ agents: currentAgents,
1205
+ },
1206
+ seed,
1207
+ resumeReservation: reservation,
1206
1208
  },
1207
- seed,
1208
- reservation,
1209
1209
  );
1210
1210
  if (pending.exitCode !== -1) {
1211
1211
  if (clonedSession) {