@tt-a1i/openpi 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -389
  3. package/SETUP.md +24 -22
  4. package/THIRD_PARTY_NOTICES.md +3 -4
  5. package/assets/readme-hero-mobile.svg +2 -2
  6. package/assets/readme-hero.svg +10 -10
  7. package/extensions/ask-user/handoff.ts +5 -1
  8. package/extensions/ask-user/index.ts +44 -0
  9. package/extensions/background-terminals/index.ts +118 -29
  10. package/extensions/background-terminals/src/domain.ts +5 -1
  11. package/extensions/background-terminals/src/manager.ts +2 -1
  12. package/extensions/background-terminals/src/prompt.ts +35 -0
  13. package/extensions/background-terminals/src/result-delivery.ts +76 -3
  14. package/extensions/background-terminals/src/ui/tool-result.ts +52 -1
  15. package/extensions/capabilities/index.ts +198 -0
  16. package/extensions/context-pivot/index.ts +21 -0
  17. package/extensions/cron/index.ts +42 -15
  18. package/extensions/execution-convergence/active-evidence.ts +129 -0
  19. package/extensions/execution-convergence/index.ts +442 -0
  20. package/extensions/execution-convergence/workspace-provenance.ts +338 -0
  21. package/extensions/file-search/index.ts +8 -1
  22. package/extensions/file-search/src/binaries.ts +2 -1
  23. package/extensions/git-info/src/runtime.ts +1 -1
  24. package/extensions/goal/controller.ts +2 -1
  25. package/extensions/goal/index.ts +20 -1
  26. package/extensions/plan-mode/index.ts +12 -0
  27. package/extensions/setup/index.ts +241 -45
  28. package/extensions/setup/intercom-fs-helper.cjs +130 -0
  29. package/extensions/setup/intercom.ts +603 -0
  30. package/extensions/shared/child-session.ts +42 -5
  31. package/extensions/shared/setup-config.ts +27 -1
  32. package/extensions/shared/setup-episode-state.ts +7 -0
  33. package/extensions/shared/tool-surface.ts +435 -0
  34. package/extensions/subagents/index.ts +16 -1
  35. package/extensions/subagents/src/manager.ts +13 -11
  36. package/extensions/subagents/src/prompt.ts +1 -1
  37. package/extensions/tasks/index.ts +39 -12
  38. package/extensions/ui-customization/footer.ts +6 -1
  39. package/extensions/workflows/artifacts.ts +6 -1
  40. package/extensions/workflows/dashboard.ts +138 -27
  41. package/extensions/workflows/graph-projection.ts +240 -0
  42. package/extensions/workflows/handoff.ts +194 -0
  43. package/extensions/workflows/index.ts +258 -56
  44. package/extensions/workflows/invocation-ledger.ts +368 -0
  45. package/extensions/workflows/model.ts +57 -1
  46. package/extensions/workflows/operator.ts +131 -0
  47. package/extensions/workflows/prompt.ts +10 -38
  48. package/extensions/workflows/replay-safety.ts +9 -8
  49. package/extensions/workflows/runner.ts +10 -2
  50. package/extensions/workflows/sandbox.ts +5 -0
  51. package/package.json +15 -15
  52. package/skills/subagents/SKILL.md +6 -0
  53. package/skills/workflows/EXAMPLES.md +58 -0
  54. package/skills/workflows/REFERENCE.md +44 -0
  55. package/skills/workflows/SKILL.md +39 -0
@@ -63,6 +63,10 @@ export type FooterLines = readonly (readonly FooterLayoutItem[])[];
63
63
  export const DETAIL_DISPLAYS = ["full", "compact"] as const;
64
64
  export type DetailDisplay = (typeof DETAIL_DISPLAYS)[number];
65
65
 
66
+ export const CAPABILITY_DISCOVERY_MODES = ["explicit", "adaptive"] as const;
67
+ export type CapabilityDiscoveryMode =
68
+ (typeof CAPABILITY_DISCOVERY_MODES)[number];
69
+
66
70
  /** Canonical default layout: one-line Powerline dashboard with flex alignment. */
67
71
  export const DEFAULT_FOOTER_LINES: FooterLines = [
68
72
  [
@@ -123,6 +127,9 @@ export const POST_EDIT_COMMAND_MAX_CHARS = 500;
123
127
  export const SETUP_CONFIG_CHANGED_CHANNEL = "my-pi-setup:config-changed";
124
128
 
125
129
  export interface MyPiSetupConfig {
130
+ readonly capabilities: {
131
+ readonly discovery: CapabilityDiscoveryMode;
132
+ };
126
133
  readonly suggestions: {
127
134
  readonly enabled: boolean;
128
135
  readonly model?: SuggestionModelConfig;
@@ -157,6 +164,7 @@ export interface MyPiSetupConfig {
157
164
  }
158
165
 
159
166
  export const DEFAULT_SETUP_CONFIG: MyPiSetupConfig = {
167
+ capabilities: { discovery: "explicit" },
160
168
  suggestions: { enabled: false },
161
169
  workflows: {
162
170
  concurrency: DEFAULT_WORKFLOW_CONCURRENCY,
@@ -205,6 +213,12 @@ const isFooterStyle = (value: unknown): value is FooterStyle =>
205
213
  const isFooterPreset = (value: unknown): value is FooterPreset =>
206
214
  typeof value === "string" && FOOTER_PRESETS.includes(value as FooterPreset);
207
215
 
216
+ const isCapabilityDiscoveryMode = (
217
+ value: unknown,
218
+ ): value is CapabilityDiscoveryMode =>
219
+ typeof value === "string" &&
220
+ CAPABILITY_DISCOVERY_MODES.includes(value as CapabilityDiscoveryMode);
221
+
208
222
  export function flattenFooterItems(lines: FooterLines): readonly FooterItem[] {
209
223
  const items: FooterItem[] = [];
210
224
  const seen = new Set<FooterItem>();
@@ -401,6 +415,8 @@ function boundedInteger(value: unknown, fallback: number, maximum: number) {
401
415
  export function parseSetupConfig(value: unknown): MyPiSetupConfig {
402
416
  if (!isRecord(value)) return DEFAULT_SETUP_CONFIG;
403
417
 
418
+ const capabilities = isRecord(value.capabilities) ? value.capabilities : {};
419
+
404
420
  // `summaries` is the pre-suggestion config key. Read it once as a migration
405
421
  // source; every subsequent save writes only the canonical `suggestions` key.
406
422
  const suggestions = isRecord(value.suggestions)
@@ -430,6 +446,11 @@ export function parseSetupConfig(value: unknown): MyPiSetupConfig {
430
446
  const subagents = isRecord(value.subagents) ? value.subagents : {};
431
447
  const footer = parseUiFooter(ui);
432
448
  return {
449
+ capabilities: {
450
+ discovery: isCapabilityDiscoveryMode(capabilities.discovery)
451
+ ? capabilities.discovery
452
+ : "explicit",
453
+ },
433
454
  suggestions: {
434
455
  enabled: requestedEnabled && Boolean(model),
435
456
  ...(model ? { model } : {}),
@@ -929,7 +950,10 @@ export async function saveSetupConfig(config: MyPiSetupConfig) {
929
950
  });
930
951
  }
931
952
 
932
- export function formatSetupConfig(config = loadSetupConfig()) {
953
+ export function formatSetupConfig(
954
+ config = loadSetupConfig(),
955
+ integrationLines: readonly string[] = [],
956
+ ) {
933
957
  const suggestionModel = config.suggestions.model;
934
958
  const suggestions =
935
959
  !config.suggestions.enabled || !suggestionModel
@@ -939,6 +963,7 @@ export function formatSetupConfig(config = loadSetupConfig()) {
939
963
  ? `on · ${config.ui.footerStyle} · ${formatFooterLines(config.ui.footerLines)}`
940
964
  : "off";
941
965
  return [
966
+ `Capability discovery: ${config.capabilities.discovery}`,
942
967
  suggestions,
943
968
  `Workflows: ${config.workflows.concurrency} concurrent agents · ${config.workflows.maxAgentCalls} total calls`,
944
969
  `UI: large header ${config.ui.showHeader ? "on" : "off"} · custom footer ${footer}`,
@@ -947,6 +972,7 @@ export function formatSetupConfig(config = loadSetupConfig()) {
947
972
  `Write/Edit operations: ${config.ui.fileMutationDisplay === "full" ? "expanded by default" : "folded preview (Ctrl+O expands all)"}`,
948
973
  `Post-edit command: ${config.postEdit.command ? config.postEdit.command : "off"}`,
949
974
  `Agent role models (Subagents + Workflows): ${SUBAGENT_ROLE_NAMES.map((role) => `${role} ${config.subagents.roleModels[role] ? `${config.subagents.roleModels[role].provider}/${config.subagents.roleModels[role].model}` : "inherit"}`).join(" · ")}`,
975
+ ...integrationLines,
950
976
  ].join("\n");
951
977
  }
952
978
 
@@ -0,0 +1,7 @@
1
+ /** Broadcast whenever the package-owned setup episode becomes usable or ends. */
2
+ export const OPENPI_SETUP_EPISODE_CHANNEL = "openpi:setup-episode";
3
+
4
+ export interface OpenPiSetupEpisodeState {
5
+ /** True for both armed and actively running setup episodes. */
6
+ readonly active: boolean;
7
+ }
@@ -0,0 +1,435 @@
1
+ import { fileURLToPath } from "node:url";
2
+
3
+ /**
4
+ * OpenPI-owned model tools, grouped by the extension that owns their runtime
5
+ * state. Ordinary parent sessions add no resident OpenPI tool. Explicit user
6
+ * intent can reveal the capability gateway or load one capability group;
7
+ * capability-owned tools remain registered but are projected only after their
8
+ * group is loaded. Lifecycle tools add a second resource/mode-state gate inside
9
+ * that loaded group.
10
+ */
11
+ export const OPENPI_TOOL_SURFACE = {
12
+ capabilities: {
13
+ entry: ["openpi_load_tools"],
14
+ deferred: [],
15
+ },
16
+ fileSearch: {
17
+ entry: ["fd", "rg"],
18
+ deferred: [],
19
+ },
20
+ subagents: {
21
+ entry: ["subagent_spawn"],
22
+ deferred: [
23
+ "subagent_wait",
24
+ "subagent_cancel",
25
+ "subagent_send",
26
+ "subagent_check",
27
+ "subagent_list",
28
+ ],
29
+ },
30
+ workflows: {
31
+ entry: ["workflow"],
32
+ deferred: ["workflow_stop", "workflow_status"],
33
+ },
34
+ background: {
35
+ entry: ["bg_start"],
36
+ deferred: ["bg_status", "bg_list", "bg_kill", "bg_watch"],
37
+ },
38
+ tasks: {
39
+ entry: ["tasks_add"],
40
+ deferred: ["tasks_update", "tasks_list"],
41
+ },
42
+ goal: {
43
+ entry: ["create_goal"],
44
+ deferred: ["get_goal", "update_goal"],
45
+ },
46
+ interaction: {
47
+ entry: [],
48
+ deferred: ["ask_user", "human_handoff"],
49
+ },
50
+ plan: {
51
+ entry: [],
52
+ deferred: ["plan_ready"],
53
+ },
54
+ setup: {
55
+ entry: [],
56
+ deferred: ["configure_my_pi_setup"],
57
+ },
58
+ context: {
59
+ entry: [],
60
+ deferred: ["context_pivot"],
61
+ },
62
+ } as const;
63
+
64
+ export type OpenPiToolOwner = keyof typeof OPENPI_TOOL_SURFACE;
65
+
66
+ export const OPENPI_CAPABILITY_GROUPS = {
67
+ search: {
68
+ owners: ["fileSearch"],
69
+ summary: "Fast structured file and content search with fd and rg.",
70
+ },
71
+ delegate: {
72
+ owners: ["subagents"],
73
+ summary: "Spawn and manage isolated in-process Pi subagents.",
74
+ },
75
+ workflow: {
76
+ owners: ["workflows"],
77
+ summary: "Run replay-safe multi-stage workflows.",
78
+ },
79
+ background: {
80
+ owners: ["background"],
81
+ summary: "Start and manage long-running background terminals.",
82
+ },
83
+ session: {
84
+ owners: ["tasks", "goal"],
85
+ summary:
86
+ "Track explicit session tasks and persistent user-requested goals.",
87
+ },
88
+ } as const satisfies Record<
89
+ string,
90
+ { owners: readonly OpenPiToolOwner[]; summary: string }
91
+ >;
92
+
93
+ export type OpenPiCapability = keyof typeof OPENPI_CAPABILITY_GROUPS;
94
+
95
+ export const OPENPI_CAPABILITY_NAMES = Object.keys(
96
+ OPENPI_CAPABILITY_GROUPS,
97
+ ) as OpenPiCapability[];
98
+
99
+ export const DEFAULT_OPENPI_ACTIVE_TOOL_NAMES: readonly string[] = [];
100
+
101
+ export const OPENPI_TOOL_SURFACE_NAMES = Object.values(
102
+ OPENPI_TOOL_SURFACE,
103
+ ).flatMap(({ entry, deferred }) => [...entry, ...deferred]);
104
+
105
+ interface ActiveToolSurface {
106
+ events?: {
107
+ emit(channel: string, data: unknown): void;
108
+ on(channel: string, handler: (data: unknown) => void): () => void;
109
+ };
110
+ getActiveTools(): string[];
111
+ getAllTools?(): {
112
+ name: string;
113
+ sourceInfo?: { path: string; source?: string };
114
+ }[];
115
+ setActiveTools(names: string[]): void;
116
+ }
117
+
118
+ interface OwnedToolPatch {
119
+ enable?: readonly string[];
120
+ disable?: readonly string[];
121
+ }
122
+
123
+ interface ToolSurfaceState {
124
+ loaded: Set<OpenPiCapability>;
125
+ desiredByOwner: Map<OpenPiToolOwner, Set<string>>;
126
+ sourceByOwner: Map<OpenPiToolOwner, string>;
127
+ managedOwners: Set<OpenPiToolOwner>;
128
+ knownAvailable: Set<string>;
129
+ subscribed: boolean;
130
+ }
131
+
132
+ const OWNER_SOURCE_PATHS = {
133
+ capabilities: fileURLToPath(
134
+ new URL("../capabilities/index.ts", import.meta.url),
135
+ ),
136
+ fileSearch: fileURLToPath(
137
+ new URL("../file-search/index.ts", import.meta.url),
138
+ ),
139
+ subagents: fileURLToPath(new URL("../subagents/index.ts", import.meta.url)),
140
+ workflows: fileURLToPath(new URL("../workflows/index.ts", import.meta.url)),
141
+ background: fileURLToPath(
142
+ new URL("../background-terminals/index.ts", import.meta.url),
143
+ ),
144
+ tasks: fileURLToPath(new URL("../tasks/index.ts", import.meta.url)),
145
+ goal: fileURLToPath(new URL("../goal/index.ts", import.meta.url)),
146
+ interaction: fileURLToPath(new URL("../ask-user/index.ts", import.meta.url)),
147
+ plan: fileURLToPath(new URL("../plan-mode/index.ts", import.meta.url)),
148
+ setup: fileURLToPath(new URL("../setup/index.ts", import.meta.url)),
149
+ context: fileURLToPath(new URL("../context-pivot/index.ts", import.meta.url)),
150
+ } as const satisfies Record<OpenPiToolOwner, string>;
151
+
152
+ const states = new WeakMap<object, ToolSurfaceState>();
153
+ export const OPENPI_CAPABILITY_STATE_CHANNEL = "openpi:capability-state";
154
+
155
+ function initialDesiredByOwner() {
156
+ return new Map<OpenPiToolOwner, Set<string>>(
157
+ (Object.keys(OPENPI_TOOL_SURFACE) as OpenPiToolOwner[]).map((owner) => [
158
+ owner,
159
+ new Set<string>(
160
+ owner === "capabilities" ? [] : OPENPI_TOOL_SURFACE[owner].entry,
161
+ ),
162
+ ]),
163
+ );
164
+ }
165
+
166
+ function newState(): ToolSurfaceState {
167
+ return {
168
+ loaded: new Set<OpenPiCapability>(),
169
+ desiredByOwner: initialDesiredByOwner(),
170
+ sourceByOwner: new Map<OpenPiToolOwner, string>(),
171
+ managedOwners: new Set<OpenPiToolOwner>(),
172
+ knownAvailable: new Set<string>(),
173
+ subscribed: false,
174
+ };
175
+ }
176
+
177
+ function capabilityStateFromEvent(data: unknown) {
178
+ if (typeof data !== "object" || data === null) return undefined;
179
+ const loaded = (data as { loaded?: unknown }).loaded;
180
+ if (!Array.isArray(loaded)) return undefined;
181
+ if (
182
+ loaded.some(
183
+ (name) =>
184
+ typeof name !== "string" ||
185
+ !OPENPI_CAPABILITY_NAMES.includes(name as OpenPiCapability),
186
+ )
187
+ ) {
188
+ return undefined;
189
+ }
190
+ return loaded as OpenPiCapability[];
191
+ }
192
+
193
+ function subscribeToCapabilityState(
194
+ pi: ActiveToolSurface,
195
+ state: ToolSurfaceState,
196
+ ) {
197
+ if (
198
+ state.subscribed ||
199
+ typeof pi.events?.on !== "function" ||
200
+ typeof pi.events?.emit !== "function"
201
+ ) {
202
+ return;
203
+ }
204
+ state.subscribed = true;
205
+ pi.events.on(OPENPI_CAPABILITY_STATE_CHANNEL, (data) => {
206
+ const loaded = capabilityStateFromEvent(data);
207
+ if (!loaded) return;
208
+ state.loaded = new Set(loaded);
209
+ reconcileManagedOwners(pi, state);
210
+ });
211
+ }
212
+
213
+ function stateFor(pi: ActiveToolSurface) {
214
+ let state = states.get(pi);
215
+ if (!state) {
216
+ state = newState();
217
+ if (
218
+ typeof pi.events?.on !== "function" ||
219
+ typeof pi.events?.emit !== "function"
220
+ ) {
221
+ state.loaded = new Set(OPENPI_CAPABILITY_NAMES);
222
+ }
223
+ states.set(pi, state);
224
+ }
225
+ return state;
226
+ }
227
+
228
+ function capabilityForOwner(owner: OpenPiToolOwner) {
229
+ return OPENPI_CAPABILITY_NAMES.find((capability) =>
230
+ OPENPI_CAPABILITY_GROUPS[capability].owners.includes(owner as never),
231
+ );
232
+ }
233
+
234
+ function ownerIsVisible(state: ToolSurfaceState, owner: OpenPiToolOwner) {
235
+ const capability = capabilityForOwner(owner);
236
+ return capability === undefined || state.loaded.has(capability);
237
+ }
238
+
239
+ function ownedToolNames(owner: OpenPiToolOwner) {
240
+ return [
241
+ ...OPENPI_TOOL_SURFACE[owner].entry,
242
+ ...OPENPI_TOOL_SURFACE[owner].deferred,
243
+ ] as readonly string[];
244
+ }
245
+
246
+ function availableOwnedToolNames(
247
+ pi: ActiveToolSurface,
248
+ state: ToolSurfaceState,
249
+ owner: OpenPiToolOwner,
250
+ ) {
251
+ for (const name of pi.getActiveTools()) state.knownAvailable.add(name);
252
+ const reportedTools = pi.getAllTools?.();
253
+ const owned = new Set<string>(ownedToolNames(owner));
254
+ if (!reportedTools || reportedTools.length === 0) {
255
+ return new Set([...state.knownAvailable].filter((name) => owned.has(name)));
256
+ }
257
+ if (reportedTools.every((tool) => tool.sourceInfo === undefined)) {
258
+ return new Set(
259
+ reportedTools.map(({ name }) => name).filter((name) => owned.has(name)),
260
+ );
261
+ }
262
+
263
+ const expectedSource =
264
+ state.sourceByOwner.get(owner) ?? OWNER_SOURCE_PATHS[owner];
265
+ return new Set(
266
+ reportedTools
267
+ .filter(
268
+ (tool) =>
269
+ owned.has(tool.name) && tool.sourceInfo?.path === expectedSource,
270
+ )
271
+ .map(({ name }) => name),
272
+ );
273
+ }
274
+
275
+ function projectedOwnerTools(
276
+ pi: ActiveToolSurface,
277
+ state: ToolSurfaceState,
278
+ owner: OpenPiToolOwner,
279
+ ) {
280
+ const available = availableOwnedToolNames(pi, state, owner);
281
+ const owned = ownedToolNames(owner);
282
+ const ownedAvailable = new Set(owned.filter((name) => available.has(name)));
283
+ const desired = state.desiredByOwner.get(owner)!;
284
+ const visible = ownerIsVisible(state, owner);
285
+ const next = pi
286
+ .getActiveTools()
287
+ .filter(
288
+ (name) => !ownedAvailable.has(name) || (visible && desired.has(name)),
289
+ );
290
+ const nextSet = new Set(next);
291
+
292
+ if (visible) {
293
+ for (const name of owned) {
294
+ if (desired.has(name) && !nextSet.has(name) && available.has(name)) {
295
+ next.push(name);
296
+ nextSet.add(name);
297
+ }
298
+ }
299
+ }
300
+
301
+ return next;
302
+ }
303
+
304
+ function applyActiveTools(pi: ActiveToolSurface, next: string[]) {
305
+ const active = pi.getActiveTools();
306
+ if (
307
+ active.length === next.length &&
308
+ active.every((name, index) => name === next[index])
309
+ ) {
310
+ return false;
311
+ }
312
+ pi.setActiveTools(next);
313
+ return true;
314
+ }
315
+
316
+ function reconcileOwner(
317
+ pi: ActiveToolSurface,
318
+ state: ToolSurfaceState,
319
+ owner: OpenPiToolOwner,
320
+ ) {
321
+ return applyActiveTools(pi, projectedOwnerTools(pi, state, owner));
322
+ }
323
+
324
+ function reconcileManagedOwners(
325
+ pi: ActiveToolSurface,
326
+ state: ToolSurfaceState,
327
+ ) {
328
+ let changed = false;
329
+ for (const owner of state.managedOwners) {
330
+ changed = reconcileOwner(pi, state, owner) || changed;
331
+ }
332
+ return changed;
333
+ }
334
+
335
+ /** Reset one bound Pi Session to the minimal parent surface. */
336
+ export function resetOpenPiToolSurface(
337
+ pi: ActiveToolSurface,
338
+ sourceByOwner: Readonly<Partial<Record<OpenPiToolOwner, string>>> = {},
339
+ ) {
340
+ const state = newState();
341
+ for (const [owner, source] of Object.entries(sourceByOwner)) {
342
+ if (source) state.sourceByOwner.set(owner as OpenPiToolOwner, source);
343
+ }
344
+ states.set(pi, state);
345
+ state.managedOwners.add("capabilities");
346
+ const changed = reconcileOwner(pi, state, "capabilities");
347
+ pi.events?.emit(OPENPI_CAPABILITY_STATE_CHANNEL, { loaded: [] });
348
+ return changed;
349
+ }
350
+
351
+ export function getLoadedOpenPiCapabilities(pi: ActiveToolSurface) {
352
+ return OPENPI_CAPABILITY_NAMES.filter((capability) =>
353
+ stateFor(pi).loaded.has(capability),
354
+ );
355
+ }
356
+
357
+ /**
358
+ * Load capability groups monotonically for this Session. There is deliberately
359
+ * no unload operation: a stable surface is more cache-friendly and easier for
360
+ * the model to reason about than tools that repeatedly disappear and return.
361
+ */
362
+ export function loadOpenPiCapabilities(
363
+ pi: ActiveToolSurface,
364
+ capabilities: readonly OpenPiCapability[],
365
+ ) {
366
+ const invalid = capabilities.filter(
367
+ (capability) => !OPENPI_CAPABILITY_NAMES.includes(capability),
368
+ );
369
+ if (invalid.length > 0) {
370
+ throw new Error(
371
+ `Unknown OpenPI ${invalid.length === 1 ? "capability" : "capabilities"}: ${invalid.map((name) => JSON.stringify(name)).join(", ")}.`,
372
+ );
373
+ }
374
+
375
+ const state = stateFor(pi);
376
+ const before = pi.getActiveTools();
377
+ const newlyLoaded: OpenPiCapability[] = [];
378
+ for (const capability of capabilities) {
379
+ if (!state.loaded.has(capability)) {
380
+ state.loaded.add(capability);
381
+ newlyLoaded.push(capability);
382
+ }
383
+ }
384
+ reconcileManagedOwners(pi, state);
385
+ pi.events?.emit(OPENPI_CAPABILITY_STATE_CHANNEL, {
386
+ loaded: getLoadedOpenPiCapabilities(pi),
387
+ });
388
+ const beforeSet = new Set(before);
389
+ return {
390
+ newlyLoaded,
391
+ loaded: getLoadedOpenPiCapabilities(pi),
392
+ activatedTools: pi.getActiveTools().filter((name) => !beforeSet.has(name)),
393
+ };
394
+ }
395
+
396
+ /**
397
+ * Record one owner's desired tools and reconcile them through both gates:
398
+ * capability loaded first, then the owner's authoritative resource/mode state.
399
+ * Foreign and Pi-native tools are always preserved from the latest active list.
400
+ */
401
+ export function patchOwnedTools(
402
+ pi: ActiveToolSurface,
403
+ owner: OpenPiToolOwner,
404
+ patch: OwnedToolPatch,
405
+ ) {
406
+ const owned = [
407
+ ...OPENPI_TOOL_SURFACE[owner].entry,
408
+ ...OPENPI_TOOL_SURFACE[owner].deferred,
409
+ ] as readonly string[];
410
+ const ownedSet = new Set(owned);
411
+ const enable = new Set(patch.enable ?? []);
412
+ const disable = new Set(patch.disable ?? []);
413
+
414
+ for (const name of [...enable, ...disable]) {
415
+ if (!ownedSet.has(name)) {
416
+ throw new Error(`${owner} does not own tool ${JSON.stringify(name)}.`);
417
+ }
418
+ }
419
+ for (const name of enable) {
420
+ if (disable.has(name)) {
421
+ throw new Error(
422
+ `${owner} cannot enable and disable tool ${JSON.stringify(name)} in one patch.`,
423
+ );
424
+ }
425
+ }
426
+
427
+ const state = stateFor(pi);
428
+ state.managedOwners.add(owner);
429
+ if (capabilityForOwner(owner)) subscribeToCapabilityState(pi, state);
430
+ const desired = state.desiredByOwner.get(owner)!;
431
+ for (const name of enable) state.knownAvailable.add(name);
432
+ for (const name of disable) desired.delete(name);
433
+ for (const name of enable) desired.add(name);
434
+ return reconcileOwner(pi, state, owner);
435
+ }
@@ -65,6 +65,10 @@ import {
65
65
  hasActivity,
66
66
  unreadActivityCounts,
67
67
  } from "../shared/activity-status.ts";
68
+ import {
69
+ OPENPI_TOOL_SURFACE,
70
+ patchOwnedTools,
71
+ } from "../shared/tool-surface.ts";
68
72
  import { formatContextUtilization } from "./src/format.ts";
69
73
  import { SubagentManager, type SubagentManagerShape } from "./src/manager.ts";
70
74
  import {
@@ -198,6 +202,14 @@ export default function (pi: ExtensionAPI) {
198
202
  let requestWidgetRender: (() => void) | undefined;
199
203
  let dashboardOpen = false;
200
204
  const resultDelivery = createDeferredResultDelivery<SubagentSnapshot>();
205
+ const hideLifecycleTools = () =>
206
+ patchOwnedTools(pi, "subagents", {
207
+ disable: OPENPI_TOOL_SURFACE.subagents.deferred,
208
+ });
209
+ const showLifecycleTools = () =>
210
+ patchOwnedTools(pi, "subagents", {
211
+ enable: OPENPI_TOOL_SURFACE.subagents.deferred,
212
+ });
201
213
 
202
214
  const getRuntime = () => (runtime ??= createSubagentRuntime());
203
215
 
@@ -408,6 +420,7 @@ export default function (pi: ExtensionAPI) {
408
420
 
409
421
  pi.on("session_start", (_event, ctx) => {
410
422
  refreshAgentTypes(ctx.cwd, ctx.isProjectTrusted());
423
+ hideLifecycleTools();
411
424
  sessionContext = ctx;
412
425
  settledAcknowledgedAt = 0;
413
426
  if (ctx.hasUI) ui = ctx.ui;
@@ -633,7 +646,7 @@ export default function (pi: ExtensionAPI) {
633
646
  ? planModeChildTools(declaredChildTools)
634
647
  : declaredChildTools;
635
648
  const childTools = effectiveChildToolAllowlist(requestedChildTools);
636
- // Read at spawn time so `/my-pi-setup` changes affect the next child
649
+ // Read at spawn time so `/openpi-setup` changes affect the next child
637
650
  // without reloading this extension. Undefined preserves parent-model
638
651
  // inheritance in the backend.
639
652
  const model = selectSubagentModel(
@@ -681,6 +694,8 @@ export default function (pi: ExtensionAPI) {
681
694
  throw error;
682
695
  }
683
696
 
697
+ showLifecycleTools();
698
+
684
699
  return {
685
700
  content: [
686
701
  {
@@ -205,7 +205,8 @@ const makeManager = Effect.gen(function* () {
205
205
  let reservedBtw = 0;
206
206
  let disposed = false;
207
207
  let onSettled:
208
- ((snap: SubagentSnapshot, consumed: boolean) => void) | undefined;
208
+ | ((snap: SubagentSnapshot, consumed: boolean) => void)
209
+ | undefined;
209
210
 
210
211
  const notify = (id?: string) => {
211
212
  const waiters = changeWaiters;
@@ -636,16 +637,17 @@ const makeManager = Effect.gen(function* () {
636
637
  pruneSettled();
637
638
  }),
638
639
  ),
639
- Effect.map((): ReadonlyArray<CancelResult> =>
640
- unique.map((id) => {
641
- const snapshot = entries.get(id)?.snapshot;
642
- return {
643
- id,
644
- title: snapshot?.title ?? "?",
645
- status: snapshot?.status ?? "error",
646
- cancelled: runningIds.includes(id),
647
- };
648
- }),
640
+ Effect.map(
641
+ (): ReadonlyArray<CancelResult> =>
642
+ unique.map((id) => {
643
+ const snapshot = entries.get(id)?.snapshot;
644
+ return {
645
+ id,
646
+ title: snapshot?.title ?? "?",
647
+ status: snapshot?.status ?? "error",
648
+ cancelled: runningIds.includes(id),
649
+ };
650
+ }),
649
651
  ),
650
652
  );
651
653
  });
@@ -75,7 +75,7 @@ export const SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS = {
75
75
  workingDir:
76
76
  "Trusted working directory for the autonomous child (default: current working directory)",
77
77
  isolation:
78
- 'Set to "worktree" to run this child in its own git worktree on its own branch, branched from HEAD. Use it whenever children may edit the same files or stage changes concurrently — without it, parallel children share one checkout and one git index, so their edits and `git add`s overwrite each other. The child should COMMIT its work. A direct child can receive later subagent_send turns, so its checkout lives with that child Session. On retirement it is reclaimed only when a bounded inspection proves it empty; commits, dirty/untracked/ignored files, detached HEAD, timeout, or Git failure preserve it. Requires a git repository, and the checkout starts clean, so anything gitignored (build output, .env) will not be there.',
78
+ 'Set to "worktree" for concurrent writers and tell the child to commit. Requires Git and a clean checkout. Read the subagents Skill for lifecycle, merge location, and costs.',
79
79
  model:
80
80
  'Optional model override, as "provider/model-id" or a bare id resolved against the current provider. Precedence: explicit spawn model > selected type file model > configured built-in role model > parent model. Never guess a model name.',
81
81
  reasoningEffort: