@tt-a1i/openpi 0.1.1 → 0.3.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 (49) hide show
  1. package/README.md +65 -28
  2. package/SETUP.md +8 -6
  3. package/extensions/ask-user/handoff.ts +5 -1
  4. package/extensions/ask-user/index.ts +44 -0
  5. package/extensions/background-terminals/index.ts +118 -29
  6. package/extensions/background-terminals/src/domain.ts +5 -1
  7. package/extensions/background-terminals/src/manager.ts +2 -1
  8. package/extensions/background-terminals/src/prompt.ts +35 -0
  9. package/extensions/background-terminals/src/result-delivery.ts +76 -3
  10. package/extensions/background-terminals/src/ui/tool-result.ts +52 -1
  11. package/extensions/capabilities/index.ts +198 -0
  12. package/extensions/context-pivot/index.ts +21 -0
  13. package/extensions/cron/index.ts +42 -15
  14. package/extensions/execution-convergence/active-evidence.ts +129 -0
  15. package/extensions/execution-convergence/index.ts +442 -0
  16. package/extensions/execution-convergence/workspace-provenance.ts +338 -0
  17. package/extensions/file-search/index.ts +8 -1
  18. package/extensions/file-search/src/binaries.ts +2 -1
  19. package/extensions/git-info/src/runtime.ts +1 -1
  20. package/extensions/goal/controller.ts +2 -1
  21. package/extensions/goal/index.ts +20 -1
  22. package/extensions/plan-mode/bash-policy.ts +219 -42
  23. package/extensions/plan-mode/index.ts +56 -19
  24. package/extensions/setup/index.ts +96 -10
  25. package/extensions/shared/child-session.ts +40 -4
  26. package/extensions/shared/editor-layers.ts +150 -0
  27. package/extensions/shared/setup-config.ts +26 -15
  28. package/extensions/shared/setup-episode-state.ts +7 -0
  29. package/extensions/shared/tool-surface.ts +435 -0
  30. package/extensions/subagents/index.ts +179 -96
  31. package/extensions/subagents/src/manager.ts +13 -11
  32. package/extensions/subagents/src/prompt.ts +7 -7
  33. package/extensions/subagents/src/ui/takeover.ts +231 -133
  34. package/extensions/subagents/src/ui/transcript.ts +252 -37
  35. package/extensions/subagents/src/ui/wait-result.ts +6 -19
  36. package/extensions/suggestions/index.ts +27 -18
  37. package/extensions/tasks/index.ts +63 -18
  38. package/extensions/ui-customization/footer.ts +65 -11
  39. package/extensions/workflows/graph-projection.ts +6 -4
  40. package/extensions/workflows/index.ts +44 -21
  41. package/extensions/workflows/invocation-ledger.ts +8 -2
  42. package/extensions/workflows/model.ts +5 -1
  43. package/extensions/workflows/prompt.ts +10 -40
  44. package/extensions/workflows/replay-safety.ts +9 -8
  45. package/package.json +10 -10
  46. package/skills/subagents/SKILL.md +7 -1
  47. package/skills/workflows/EXAMPLES.md +58 -0
  48. package/skills/workflows/REFERENCE.md +44 -0
  49. package/skills/workflows/SKILL.md +39 -0
@@ -25,24 +25,22 @@
25
25
  */
26
26
 
27
27
  /**
28
- * Any of these means the text is more than one plain command — a pipeline, a
29
- * sequence, a redirect, a substitution, a glob, or a background job. Rather
30
- * than parse shell (where every parser bug is a bypass), refuse outright.
31
- *
32
- * `\` is here because a line continuation splices in the next line; `$` covers
33
- * both `$(...)` and a `$VAR` that expands into arguments never inspected here.
28
+ * This module does not parse shell or admit shell composition. Its small
29
+ * tokenizer only recognizes words and quoted literal spans. `$`, backticks
30
+ * and `\` are refused everywhere; shell metacharacters are refused outside
31
+ * quotes. Globs are also refused outside quotes because the shell would expand
32
+ * them before the allowlisted program sees them, while a quoted glob is a
33
+ * literal pattern interpreted by that read-only program itself.
34
34
  */
35
- const SHELL_METACHARACTERS = /[;&|<>$`\\!*?{}()[\]\n\r#]/;
35
+ const UNQUOTED_SHELL_METACHARACTERS = /[;&|<>(){}\n\r#]/;
36
+ const EXPANSION_CHARACTERS = /[$`\\]/;
37
+ const UNQUOTED_GLOB_CHARACTERS = new Set(["*", "?", "[", "]"]);
36
38
 
37
39
  /**
38
- * Tilde expansion, but only where a shell would actually expand it: at the
39
- * start of a word. `HEAD~3` is ordinary revision syntax and must survive,
40
- * while `~/notes` and `~user/x` resolve to a path this module never sees.
40
+ * Tilde expansion is refused only at the start of an unquoted word. `HEAD~3`
41
+ * is ordinary revision syntax and must survive, while `~/notes` and
42
+ * `~user/x` resolve to a path this module never sees.
41
43
  */
42
- const TILDE_EXPANSION = /(^|\s)~/;
43
-
44
- /** Quotes hide word boundaries from the tokenizer below, so they are refused too. */
45
- const QUOTES = /["']/;
46
44
 
47
45
  /**
48
46
  * Read-only git subcommands. Absent on purpose: `config`, `stash`, `tag`,
@@ -194,6 +192,94 @@ const GH_FLAGS = new Set([
194
192
  "--comments",
195
193
  ]);
196
194
 
195
+ const RG_FLAGS = new Set([
196
+ "-n",
197
+ "--line-number",
198
+ "-i",
199
+ "--ignore-case",
200
+ "-l",
201
+ "--files-with-matches",
202
+ "-c",
203
+ "--count",
204
+ "-w",
205
+ "-F",
206
+ "--fixed-strings",
207
+ "-e",
208
+ "--regexp",
209
+ "-g",
210
+ "--glob",
211
+ "-t",
212
+ "--type",
213
+ "--files",
214
+ "--hidden",
215
+ "--no-ignore",
216
+ "-A",
217
+ "-B",
218
+ "-C",
219
+ "--after-context",
220
+ "--before-context",
221
+ "--context",
222
+ "-m",
223
+ "--max-count",
224
+ "-o",
225
+ "--only-matching",
226
+ "--sort",
227
+ "--json",
228
+ "--color",
229
+ "-H",
230
+ "-N",
231
+ "--no-filename",
232
+ "-v",
233
+ "--invert-match",
234
+ "-u",
235
+ "-uu",
236
+ ]);
237
+
238
+ const FD_FLAGS = new Set([
239
+ "-e",
240
+ "--extension",
241
+ "-t",
242
+ "--type",
243
+ "-d",
244
+ "--max-depth",
245
+ "--min-depth",
246
+ "-H",
247
+ "--hidden",
248
+ "-I",
249
+ "--no-ignore",
250
+ "-g",
251
+ "--glob",
252
+ "-F",
253
+ "--fixed-strings",
254
+ "-p",
255
+ "--full-path",
256
+ "-a",
257
+ "--absolute-path",
258
+ "-l",
259
+ "--list-details",
260
+ "--color",
261
+ "-0",
262
+ "-S",
263
+ "--size",
264
+ ]);
265
+
266
+ const LS_FLAGS = new Set([
267
+ "-l",
268
+ "-a",
269
+ "-A",
270
+ "-h",
271
+ "-t",
272
+ "-r",
273
+ "-R",
274
+ "-d",
275
+ "-1",
276
+ "-S",
277
+ "-F",
278
+ "--color",
279
+ ]);
280
+ const WC_FLAGS = new Set(["-l", "-w", "-c", "-m"]);
281
+ const HEAD_TAIL_FLAGS = new Set(["-n", "-c", "--lines", "--bytes"]);
282
+
197
283
  /** `-5`, `-20`: git's count shorthand, which is a number rather than a flag. */
198
284
  const NUMERIC_SHORTHAND = /^-\d+$/;
199
285
 
@@ -208,6 +294,83 @@ const refuse = (reason: string): BashPlanDecision => ({
208
294
  reason,
209
295
  });
210
296
 
297
+ /**
298
+ * Tokenize words without pretending to be a shell parser. Quoted spans are
299
+ * removed and become literal text; no expansion or command composition is
300
+ * supported. The validation happens while tokenizing so an unquoted glob can
301
+ * never be mistaken for a literal program argument.
302
+ */
303
+ function tokenize(command: string) {
304
+ const words: string[] = [];
305
+ let word = "";
306
+ let inWord = false;
307
+ let quote: "'" | '"' | undefined;
308
+ let wordStartsUnquoted = false;
309
+
310
+ for (const character of command) {
311
+ if (quote) {
312
+ if (character === quote) {
313
+ quote = undefined;
314
+ } else if (EXPANSION_CHARACTERS.test(character)) {
315
+ return refuse(
316
+ `plan mode rejected expansion character ${JSON.stringify(character)} — remove expansion syntax and pass literal arguments instead`,
317
+ );
318
+ } else {
319
+ word += character;
320
+ }
321
+ inWord = true;
322
+ continue;
323
+ }
324
+
325
+ if (EXPANSION_CHARACTERS.test(character)) {
326
+ return refuse(
327
+ `plan mode rejected expansion character ${JSON.stringify(character)} — remove expansion syntax and pass literal arguments instead`,
328
+ );
329
+ }
330
+ if (UNQUOTED_SHELL_METACHARACTERS.test(character)) {
331
+ return refuse(
332
+ `plan mode rejected unquoted shell metacharacter ${JSON.stringify(character)} — run a single plain command without shell composition`,
333
+ );
334
+ }
335
+ if (UNQUOTED_GLOB_CHARACTERS.has(character)) {
336
+ return refuse(
337
+ "plan mode will not run an unquoted glob because the shell would expand it — quote it instead, e.g. --glob '*.ts'",
338
+ );
339
+ }
340
+ if (character === "'" || character === '"') {
341
+ quote = character;
342
+ inWord = true;
343
+ if (!word) wordStartsUnquoted = false;
344
+ continue;
345
+ }
346
+ if (/\s/.test(character)) {
347
+ if (inWord) {
348
+ words.push(word);
349
+ word = "";
350
+ inWord = false;
351
+ wordStartsUnquoted = false;
352
+ }
353
+ continue;
354
+ }
355
+ if (!inWord) wordStartsUnquoted = true;
356
+ if (wordStartsUnquoted && word.length === 0 && character === "~") {
357
+ return refuse(
358
+ "plan mode does not run tilde-expanded paths — use a path relative to the project instead",
359
+ );
360
+ }
361
+ word += character;
362
+ inWord = true;
363
+ }
364
+
365
+ if (quote) {
366
+ return refuse(
367
+ `plan mode rejected an unterminated ${quote === "'" ? "single" : "double"} quote — close the quote or pass a literal argument instead`,
368
+ );
369
+ }
370
+ if (inWord) words.push(word);
371
+ return { allowed: true as const, words };
372
+ }
373
+
211
374
  /** Split `--flag=value` into the flag part the allowlists are keyed on. */
212
375
  function flagName(word: string) {
213
376
  const eq = word.indexOf("=");
@@ -223,16 +386,27 @@ function scanArguments(
223
386
  words: readonly string[],
224
387
  allowed: ReadonlySet<string>,
225
388
  program: string,
389
+ /**
390
+ * Whether `-la` may stand for `-l -a`. Only the file-inspection programs opt
391
+ * in: git and gh keep their historical one-flag-per-word rule, so widening
392
+ * the tokenizer cannot quietly widen their surface too.
393
+ */
394
+ allowShortClusters = false,
226
395
  ): BashPlanDecision {
227
396
  for (const word of words) {
228
397
  if (word === "--") break;
229
398
  if (!word.startsWith("-")) continue;
230
399
  if (NUMERIC_SHORTHAND.test(word)) continue;
231
- if (!allowed.has(flagName(word))) {
232
- return refuse(
233
- `plan mode does not recognize "${word}" as a read-only ${program} option, so it will not run this command`,
234
- );
235
- }
400
+ if (allowed.has(flagName(word))) continue;
401
+ const isAllowedCluster =
402
+ allowShortClusters &&
403
+ !word.startsWith("--") &&
404
+ word.length > 2 &&
405
+ [...word.slice(1)].every((character) => allowed.has(`-${character}`));
406
+ if (isAllowedCluster) continue;
407
+ return refuse(
408
+ `plan mode does not recognize "${word}" as a read-only ${program} option — use only the allowlisted ${program} flags`,
409
+ );
236
410
  }
237
411
  return { allowed: true };
238
412
  }
@@ -249,21 +423,10 @@ export function planBashDecision(command: unknown): BashPlanDecision {
249
423
  const text = command.trim();
250
424
  if (!text) return refuse("plan mode received an empty command");
251
425
 
252
- if (SHELL_METACHARACTERS.test(text)) {
253
- return refuse(
254
- "plan mode only runs a single plain command — no pipes, redirects, substitutions, globs, or chained commands",
255
- );
256
- }
257
- if (QUOTES.test(text)) {
258
- return refuse("plan mode only runs unquoted commands while planning");
259
- }
260
- if (TILDE_EXPANSION.test(text)) {
261
- return refuse(
262
- "plan mode does not run commands with `~` paths — give a path relative to the project instead",
263
- );
264
- }
265
-
266
- const [program, ...rest] = text.split(/\s+/);
426
+ const tokenized = tokenize(text);
427
+ if (!("words" in tokenized)) return tokenized;
428
+ const [program, ...rest] = tokenized.words;
429
+ if (!program) return refuse("plan mode received an empty command");
267
430
 
268
431
  /*
269
432
  * The subcommand must be the FIRST word, never "the first word that is not a
@@ -300,14 +463,28 @@ export function planBashDecision(command: unknown): BashPlanDecision {
300
463
  return scanArguments(args, GH_FLAGS, "gh");
301
464
  }
302
465
 
303
- /*
304
- * Nothing else is admitted. `ls`, `cat`, `head`, `tail` and `wc` were on an
305
- * earlier version of this list and are gone: plan mode already grants the
306
- * `ls`, `read`, `grep` and `fd`/`rg` TOOLS, so those shell forms added no
307
- * capability while each contributed its own flag grammar to get wrong
308
- * (`file --compile` and `tree -ao` both write files).
309
- */
466
+ const readOnlyPrograms = new Map([
467
+ ["rg", RG_FLAGS],
468
+ ["fd", FD_FLAGS],
469
+ ["ls", LS_FLAGS],
470
+ ["wc", WC_FLAGS],
471
+ ["head", HEAD_TAIL_FLAGS],
472
+ ["tail", HEAD_TAIL_FLAGS],
473
+ ]);
474
+ const flags = readOnlyPrograms.get(program);
475
+ if (flags) {
476
+ if (
477
+ program === "tail" &&
478
+ rest.some((word) => word === "-f" || word === "--follow")
479
+ ) {
480
+ return refuse(
481
+ 'plan mode refuses "tail -f/--follow" because it can block forever — use a finite tail command instead',
482
+ );
483
+ }
484
+ return scanArguments(rest, flags, program, true);
485
+ }
486
+
310
487
  return refuse(
311
- `plan mode runs only read-only git and gh investigation commands while planning, not "${program}" use the read, ls, grep or fd tools for files`,
488
+ `plan mode does not allow "${program}" — use read-only git and gh investigation commands; available commands are git, gh, rg, fd, ls, wc, head or tail, plus the read/grep/fd tools`,
312
489
  );
313
490
  }
@@ -36,6 +36,10 @@ import {
36
36
  PLAN_MODE_CHANNEL,
37
37
  type PlanModeState,
38
38
  } from "../shared/plan-mode-state.ts";
39
+ import {
40
+ OPENPI_TOOL_SURFACE,
41
+ patchOwnedTools,
42
+ } from "../shared/tool-surface.ts";
39
43
  import { sanitizeTerminalText } from "../shared/terminal-text.ts";
40
44
  import { planBashDecision } from "./bash-policy.ts";
41
45
 
@@ -138,8 +142,12 @@ export const PLAN_READY_ACTIONS = {
138
142
  continue: "Continue planning",
139
143
  current: "Implement in this session",
140
144
  fresh: "Start a fresh session",
145
+ off: "Turn plan mode off",
141
146
  } as const;
142
147
 
148
+ /** Menu label for the same effect as `/plan done`. */
149
+ const FINALIZE_NOW = "Finalize now";
150
+
143
151
  export function buildPlanImplementationPrompt(plan: string) {
144
152
  return [
145
153
  "Implement the approved plan below. Re-check the repository state before editing, follow the project instructions, and verify the finished change.",
@@ -231,6 +239,12 @@ export function planToolCallDecision(
231
239
  export default function planMode(pi: ExtensionAPI) {
232
240
  let planning = false;
233
241
  let readyPlan: string | undefined;
242
+ const syncPlanTool = () =>
243
+ patchOwnedTools(pi, "plan", {
244
+ ...(planning && !readyPlan
245
+ ? { enable: OPENPI_TOOL_SURFACE.plan.deferred }
246
+ : { disable: OPENPI_TOOL_SURFACE.plan.deferred }),
247
+ });
234
248
 
235
249
  /**
236
250
  * Publish the stance and reflect it in the footer. Every place `planning`
@@ -242,15 +256,12 @@ export default function planMode(pi: ExtensionAPI) {
242
256
  hasUI: boolean;
243
257
  ui: { setStatus: (key: string, value?: string) => void };
244
258
  }) => {
259
+ syncPlanTool();
245
260
  pi.events.emit(PLAN_MODE_CHANNEL, { planning } satisfies PlanModeState);
246
261
  if (!ctx.hasUI) return;
247
262
  ctx.ui.setStatus(
248
263
  "plan-mode",
249
- readyPlan
250
- ? "plan mode · ready"
251
- : planning
252
- ? "plan mode · read-only"
253
- : undefined,
264
+ readyPlan ? "plan ready" : planning ? "plan mode" : undefined,
254
265
  );
255
266
  };
256
267
 
@@ -360,9 +371,25 @@ export default function planMode(pi: ExtensionAPI) {
360
371
  implementHere(ctx);
361
372
  } else if (choice === PLAN_READY_ACTIONS.fresh) {
362
373
  await implementFresh(ctx);
374
+ } else if (choice === PLAN_READY_ACTIONS.off) {
375
+ clearPlan(ctx);
376
+ ctx.ui.notify("Plan mode is off.", "info");
363
377
  }
364
378
  };
365
379
 
380
+ const requestPlanFinalization = () => {
381
+ pi.sendMessage(
382
+ {
383
+ customType: "plan-finalize-requested",
384
+ content:
385
+ "Finalize the plan now. Resolve any remaining material ambiguity with ask_user; otherwise call plan_ready alone with the complete implementation-ready Markdown plan. Do not implement it.",
386
+ display: true,
387
+ details: {},
388
+ },
389
+ { deliverAs: "followUp", triggerTurn: true },
390
+ );
391
+ };
392
+
366
393
  pi.registerTool({
367
394
  name: "plan_ready",
368
395
  label: "Plan Ready",
@@ -451,16 +478,7 @@ export default function planMode(pi: ExtensionAPI) {
451
478
  ctx.ui.notify("Plan mode is not active.", "warning");
452
479
  return;
453
480
  }
454
- pi.sendMessage(
455
- {
456
- customType: "plan-finalize-requested",
457
- content:
458
- "Finalize the plan now. Resolve any remaining material ambiguity with ask_user; otherwise call plan_ready alone with the complete implementation-ready Markdown plan. Do not implement it.",
459
- display: true,
460
- details: {},
461
- },
462
- { deliverAs: "followUp", triggerTurn: true },
463
- );
481
+ requestPlanFinalization();
464
482
  return;
465
483
  }
466
484
 
@@ -470,10 +488,28 @@ export default function planMode(pi: ExtensionAPI) {
470
488
  }
471
489
 
472
490
  if (planning) {
473
- ctx.ui.notify(
474
- "Plan mode is already active. `/plan done` requests completion; `/plan off` cancels.",
475
- "info",
491
+ if (!ctx.hasUI) {
492
+ ctx.ui.notify(
493
+ "Plan mode is already active. `/plan done` requests completion; `/plan off` cancels.",
494
+ "info",
495
+ );
496
+ return;
497
+ }
498
+ const choice = await ctx.ui.select(
499
+ "Plan Mode — choose what happens next",
500
+ [PLAN_READY_ACTIONS.continue, FINALIZE_NOW, PLAN_READY_ACTIONS.off],
476
501
  );
502
+ if (choice === PLAN_READY_ACTIONS.continue) {
503
+ ctx.ui.notify(
504
+ "Plan mode is already active. `/plan done` requests completion; `/plan off` cancels.",
505
+ "info",
506
+ );
507
+ } else if (choice === FINALIZE_NOW) {
508
+ requestPlanFinalization();
509
+ } else if (choice === PLAN_READY_ACTIONS.off) {
510
+ clearPlan(ctx);
511
+ ctx.ui.notify("Plan mode is off.", "info");
512
+ }
477
513
  return;
478
514
  }
479
515
 
@@ -499,7 +535,7 @@ export default function planMode(pi: ExtensionAPI) {
499
535
  return {
500
536
  block: true as const,
501
537
  reason:
502
- "The plan is ready and the write gate remains closed. Wait for the user to choose the next action with `/plan`; do not call more tools.",
538
+ "The plan is ready and the write gate remains closed. Wait for the user to choose the next action with `/plan` or turn it off with `/plan off`; do not call more tools.",
503
539
  };
504
540
  }
505
541
  const batchDecision =
@@ -532,6 +568,7 @@ export default function planMode(pi: ExtensionAPI) {
532
568
  pi.on("session_shutdown", () => {
533
569
  planning = false;
534
570
  readyPlan = undefined;
571
+ syncPlanTool();
535
572
  // Broadcast without a ctx: subagents keeps its own copy of the stance, and
536
573
  // leaving it armed would restrict children in whatever session comes next.
537
574
  pi.events.emit(PLAN_MODE_CHANNEL, { planning } satisfies PlanModeState);
@@ -10,6 +10,11 @@ import {
10
10
  type SubagentRoleModels,
11
11
  } from "../shared/subagent-roles.ts";
12
12
  import { sanitizeTerminalText } from "../shared/terminal-text.ts";
13
+ import {
14
+ OPENPI_SETUP_EPISODE_CHANNEL,
15
+ type OpenPiSetupEpisodeState,
16
+ } from "../shared/setup-episode-state.ts";
17
+ import { patchOwnedTools } from "../shared/tool-surface.ts";
13
18
  import {
14
19
  formatPiIntercomStatus,
15
20
  inspectPiIntercom,
@@ -18,6 +23,7 @@ import {
18
23
  } from "./intercom.ts";
19
24
  import {
20
25
  applyFooterConfig,
26
+ CAPABILITY_DISCOVERY_MODES,
21
27
  DETAIL_DISPLAYS,
22
28
  FOOTER_ITEMS,
23
29
  FOOTER_LAYOUT_ITEMS,
@@ -33,6 +39,7 @@ import {
33
39
  REASONING_LEVELS,
34
40
  SETUP_CONFIG_CHANGED_CHANNEL,
35
41
  type FooterLayoutItem,
42
+ type CapabilityDiscoveryMode,
36
43
  type FooterPreset,
37
44
  type FooterStyle,
38
45
  type MyPiSetupConfig,
@@ -102,12 +109,12 @@ export function buildInteractiveSetupPrompt(options: {
102
109
  }) {
103
110
  const configurationState = options.savedConfigExists
104
111
  ? [
105
- "This package has already been configured. Explain the current settings in the user's language, then ask whether they want to keep them or change Next-action suggestions, Workflow limits, UI/Footer, result detail display, Post-edit, Agent role models, or review everything.",
112
+ "This package has already been configured. Explain the current settings in the user's language, then ask whether they want to keep them or change Capability discovery, Next-action suggestions, Workflow limits, UI/Footer, result detail display, Post-edit, Agent role models, or review everything.",
106
113
  "If the user keeps the current settings, do not call configure_my_pi_setup. If they choose a category, ask only the follow-up needed for that category.",
107
114
  ]
108
115
  : [
109
116
  "This is the first setup. Explain the available choices and their impact in the user's language, then collect the initial preferences.",
110
- "Prefer one ask_user call with up to three independent questions covering Next-action suggestions, Workflow limits, and UI/Footer/result display. Explain that Post-edit defaults off; keep it off unless the user opts in, then ask only for the command. Explain that built-in Agent roles used by subagent_spawn and workflow agent_type inherit the parent model unless the user assigns an available model to a role.",
117
+ "Prefer one ask_user call with up to three independent questions covering Capability discovery plus Workflow limits, Next-action suggestions, and UI/Footer/result display. Explain that Post-edit defaults off; keep it off unless the user opts in, then ask only for the command. Explain that built-in Agent roles used by subagent_spawn and workflow agent_type inherit the parent model unless the user assigns an available model to a role.",
111
118
  ];
112
119
 
113
120
  return [
@@ -122,16 +129,19 @@ export function buildInteractiveSetupPrompt(options: {
122
129
  ...configurationState,
123
130
  "",
124
131
  "Before asking, briefly explain what can be configured and the practical impact:",
132
+ "- Capability discovery: explicit is the safe default and keeps OpenPI model tools absent until the user asks for a capability. adaptive is opt-in and keeps only the small openpi_load_tools gateway visible, allowing the model to load Subagents, Workflows, background terminals, structured search, or Session tracking when it judges them useful. Loaded groups remain session-stable, and normal permission, concurrency, and workflow limits still apply.",
125
133
  "- Next-action suggestions: disabled, or model-generated after a fully settled main-agent run. A suggestion appears as dim inline text on the first row of an empty editor; reserved cells at the row end keep CJK IME preedit from overwriting it. Right accepts it without submitting, and any other editor input dismisses it. Enabling requires an available provider/model and reasoning level and adds one small model call per settled run.",
126
134
  "- Workflow fan-out: concurrency controls simultaneous agents and resource pressure; max agent calls controls the total capacity of one workflow. Valid ranges are 1-64 and 1-1024.",
127
- "- UI: the large header costs vertical space; the custom footer is a declarative dashboard. Presets: powerline (default one-line ANSI256 blocks), powerline-mono (one-line high-contrast gray powerline), and compact (one-line plain text). Style can also be set independently: plain, powerline, powerline-mono. Custom lines are a 2D layout of cwd/model/thinking/context/cache/cost/throughput/git/pr plus at most one flex per line for left/right alignment. Nerd Font only affects powerline separator glyphs; text stays readable without it. Changes apply immediately in the active TUI session.",
135
+ "- UI: the large header costs vertical space; the custom footer is a declarative dashboard. Presets: powerline (one-line ANSI256 blocks), powerline-mono (one-line high-contrast gray powerline), and compact (one-line plain text); the default is plain with cwd/git/pr on the left and model/context/cost on the right. Style can also be set independently: plain, powerline, powerline-mono. Custom lines are a 2D layout of cwd/model/thinking/context/cache/cost/throughput/git/pr plus at most one flex per line for left/right alignment. Nerd Font only affects powerline separator glyphs; text stays readable without it. Changes apply immediately in the active TUI session.",
128
136
  "- Operational activity for Subagents, Workflows, and background terminals is core status and always remains visible whenever the custom footer is enabled.",
129
137
  "- Post-edit command: one optional shell command (maximum 500 characters) run in the background after a turn with successful Write/Edit operations (e.g. `npm run format`). Off by default, interactive TUI sessions only, failures surface as a notification. This is a single command, not an event-hook system.",
130
- "- Result detail display: Subagent results, Bash operations, and Write/Edit operations can each default to full (always expanded) or compact (Claude Code-style folded preview with a hidden-line count). Compact output can still be temporarily expanded with the configured app.tools.expand key (Ctrl+O by default). Bash and Write/Edit default to compact. Recommend compact for users who do not usually inspect implementation details.",
138
+ "- Result detail display: Subagent results, Bash operations, and Write/Edit operations can each default to full or compact. Compact Subagent results show only bounded status rows and keep raw child reports behind app.tools.expand; compact Bash and Write/Edit operations use folded previews. Ctrl+O expands compact output by default. Bash and Write/Edit default to compact. Recommend compact for users who do not usually inspect implementation details.",
131
139
  "- Agent role models: built-in explorer, implementer, reviewer, and advisor roles are shared by subagent_spawn and workflow agent_type, and inherit the parent model by default. Assign only an available registry model to an individual role when needed; clearing that role returns it to inheritance. Custom agent-type files still override a built-in role's complete definition.",
132
140
  "- Intercom: optional cross-session messaging is installed only after a native setup confirmation. It stays parent-only; Direct/Workflow children and Replay cannot use it. The status above is informational for this model-guided step—do not install packages or edit its config yourself.",
133
141
  "",
134
142
  "Natural-language configuration examples the user might ask for:",
143
+ '- "let the model discover OpenPI capabilities when useful" → capability_discovery=adaptive',
144
+ '- "only use OpenPI capabilities when I ask" → capability_discovery=explicit',
135
145
  '- "switch footer to powerline" → ui_footer_preset=powerline',
136
146
  '- "use mono powerline" → ui_footer_preset=powerline-mono',
137
147
  '- "compact footer" → ui_footer_preset=compact',
@@ -151,6 +161,16 @@ const safeSetupNotice = (value: unknown, maximum = 500) =>
151
161
  .trim()
152
162
  .slice(0, maximum);
153
163
 
164
+ export function buildSetupSuccessText(
165
+ currentConfiguration: string,
166
+ normalizationNote = "",
167
+ ) {
168
+ return [
169
+ `Updated OpenPI setup. ${currentConfiguration}${normalizationNote}`,
170
+ "This setup episode is complete; configure_my_pi_setup is now hidden. Do not call it again. Do not edit configuration files directly. If the user requests another configuration change, tell them to run /openpi-setup <request> to start a new setup episode.",
171
+ ].join(" ");
172
+ }
173
+
154
174
  export function shouldOfferPiIntercom(options: {
155
175
  readonly request: string;
156
176
  readonly status: PiIntercomStatus;
@@ -231,13 +251,72 @@ async function maybeOfferPiIntercom(
231
251
  }
232
252
  }
233
253
 
254
+ export const CONFIGURE_MY_PI_SETUP_TOOL_NAME = "configure_my_pi_setup";
255
+
256
+ type SetupEpisode = "idle" | "armed" | "active";
257
+
258
+ function showConfigureTool(pi: ExtensionAPI) {
259
+ patchOwnedTools(pi, "setup", {
260
+ enable: [CONFIGURE_MY_PI_SETUP_TOOL_NAME],
261
+ });
262
+ }
263
+
264
+ function hideConfigureTool(pi: ExtensionAPI) {
265
+ patchOwnedTools(pi, "setup", {
266
+ disable: [CONFIGURE_MY_PI_SETUP_TOOL_NAME],
267
+ });
268
+ }
269
+
234
270
  export default function openPiSetup(pi: ExtensionAPI) {
271
+ let episode: SetupEpisode = "idle";
272
+ const publishEpisode = () =>
273
+ pi.events.emit(OPENPI_SETUP_EPISODE_CHANNEL, {
274
+ active: episode !== "idle",
275
+ } satisfies OpenPiSetupEpisodeState);
276
+
277
+ const endEpisode = () => {
278
+ episode = "idle";
279
+ hideConfigureTool(pi);
280
+ publishEpisode();
281
+ };
282
+
283
+ pi.on("session_start", () => {
284
+ endEpisode();
285
+ });
286
+
287
+ pi.on("agent_start", () => {
288
+ if (episode === "armed") {
289
+ episode = "active";
290
+ publishEpisode();
291
+ }
292
+ });
293
+
294
+ pi.on("tool_execution_end", (event) => {
295
+ if (
296
+ episode === "active" &&
297
+ event.toolName === CONFIGURE_MY_PI_SETUP_TOOL_NAME &&
298
+ !event.isError
299
+ ) {
300
+ endEpisode();
301
+ }
302
+ });
303
+
304
+ pi.on("agent_settled", () => {
305
+ if (episode === "active") endEpisode();
306
+ });
307
+
235
308
  pi.registerTool({
236
309
  name: "configure_my_pi_setup",
237
310
  label: "Configure OpenPI",
238
311
  description:
239
- "Apply a user-requested configuration change for this Pi setup. Configures next-action suggestions, workflow fan-out, UI/Footer (presets, style, multi-line layout), result detail display, optional Post-edit, and built-in Agent-role model assignments shared by subagent_spawn and workflow agent_type. Role models must be available in the Pi registry; null clears a role back to parent-model inheritance. Footer examples: powerline preset, powerline-mono, compact, or custom ui_footer_lines with flex. Preserve current values for settings the user did not ask to change. Changes apply immediately to an active TUI footer.",
312
+ "Apply a user-requested configuration change for this Pi setup. Configures capability discovery (explicit or opt-in adaptive), next-action suggestions, workflow fan-out, UI/Footer (presets, style, multi-line layout), result detail display, optional Post-edit, and built-in Agent-role model assignments shared by subagent_spawn and workflow agent_type. Role models must be available in the Pi registry; null clears a role back to parent-model inheritance. Footer examples: powerline preset, powerline-mono, compact, or custom ui_footer_lines with flex. Preserve current values for settings the user did not ask to change. Changes apply immediately to the capability gateway and active TUI footer.",
240
313
  parameters: Type.Object({
314
+ capability_discovery: Type.Optional(
315
+ StringEnum(CAPABILITY_DISCOVERY_MODES, {
316
+ description:
317
+ "Capability adoption policy. explicit keeps OpenPI tools absent until the user asks for a capability; adaptive keeps only the small openpi_load_tools gateway visible so the model may load a useful group on its own. Adaptive can start expensive work such as Subagents or Workflows, so it is opt-in. Omit to preserve the current value.",
318
+ }),
319
+ ),
241
320
  suggestions_enabled: Type.Optional(
242
321
  Type.Boolean({
243
322
  description:
@@ -316,7 +395,7 @@ export default function openPiSetup(pi: ExtensionAPI) {
316
395
  subagent_result_display: Type.Optional(
317
396
  StringEnum(DETAIL_DISPLAYS, {
318
397
  description:
319
- "How completed Subagent results render by default: full preserves complete output; compact shows a bounded preview that can be expanded with app.tools.expand. Omit to preserve the current value.",
398
+ "How completed Subagent results render by default: full shows complete output; compact shows only bounded status rows while app.tools.expand reveals the full child report. Omit to preserve the current value.",
320
399
  }),
321
400
  ),
322
401
  bash_tool_display: Type.Optional(
@@ -406,6 +485,12 @@ export default function openPiSetup(pi: ExtensionAPI) {
406
485
  );
407
486
 
408
487
  const config: MyPiSetupConfig = {
488
+ capabilities: {
489
+ discovery:
490
+ (params.capability_discovery as
491
+ | CapabilityDiscoveryMode
492
+ | undefined) ?? current.capabilities.discovery,
493
+ },
409
494
  suggestions: {
410
495
  enabled: suggestionsEnabled,
411
496
  ...(model ? { model } : {}),
@@ -457,9 +542,7 @@ export default function openPiSetup(pi: ExtensionAPI) {
457
542
  : "";
458
543
  if (ctx.hasUI) ctx.ui.notify(`${text}${note}`, "info");
459
544
  return {
460
- content: [
461
- { type: "text", text: `Updated OpenPI setup. ${text}${note}` },
462
- ],
545
+ content: [{ type: "text", text: buildSetupSuccessText(text, note) }],
463
546
  details: config,
464
547
  };
465
548
  },
@@ -490,7 +573,7 @@ export default function openPiSetup(pi: ExtensionAPI) {
490
573
  "Current configuration:",
491
574
  currentConfiguration,
492
575
  "",
493
- "Footer tips: presets are powerline, powerline-mono, compact; style is plain/powerline/powerline-mono; custom layouts use ui_footer_lines (2D enum arrays with optional flex). Do not use ui_footer_items together with ui_footer_lines. Built-in Agent role models (explorer, implementer, reviewer, advisor) are shared by subagent_spawn and workflow agent_type; they inherit the parent unless assigned an available registry model, and clearing an assignment restores inheritance. Custom agent-type files still override built-in role definitions. Nerd Font only affects powerline separator glyphs. Changes apply immediately in the active TUI session. Intercom installation is handled only by the native setup confirmation; do not install packages or edit its config yourself.",
576
+ "Capability discovery is explicit by default; adaptive is an opt-in that keeps only openpi_load_tools visible so the model may load useful groups. Footer tips: presets are powerline, powerline-mono, compact; style is plain/powerline/powerline-mono; custom layouts use ui_footer_lines (2D enum arrays with optional flex). Do not use ui_footer_items together with ui_footer_lines. Built-in Agent role models (explorer, implementer, reviewer, advisor) are shared by subagent_spawn and workflow agent_type; they inherit the parent unless assigned an available registry model, and clearing an assignment restores inheritance. Custom agent-type files still override built-in role definitions. Nerd Font only affects powerline separator glyphs. Changes apply immediately in the active TUI session. Intercom installation is handled only by the native setup confirmation; do not install packages or edit its config yourself.",
494
577
  "",
495
578
  "Use configure_my_pi_setup to apply only the requested OpenPI-owned changes and preserve everything else. Interpret model names from the available Pi registry. Do not edit configuration files directly.",
496
579
  ]
@@ -501,6 +584,9 @@ export default function openPiSetup(pi: ExtensionAPI) {
501
584
  savedConfigExists,
502
585
  });
503
586
 
587
+ episode = "armed";
588
+ showConfigureTool(pi);
589
+ publishEpisode();
504
590
  pi.sendUserMessage(
505
591
  prompt.join("\n"),
506
592
  ctx.isIdle() ? undefined : { deliverAs: "followUp" },