@oh-my-pi/pi-coding-agent 15.12.4 → 15.13.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 (291) hide show
  1. package/CHANGELOG.md +304 -6
  2. package/dist/cli.js +1015 -881
  3. package/dist/types/async/job-manager.d.ts +15 -0
  4. package/dist/types/autolearn/controller.d.ts +25 -0
  5. package/dist/types/autolearn/managed-skills.d.ts +45 -0
  6. package/dist/types/autoresearch/state.d.ts +1 -1
  7. package/dist/types/autoresearch/types.d.ts +1 -1
  8. package/dist/types/cli/args.d.ts +19 -1
  9. package/dist/types/cli/session-picker.d.ts +1 -1
  10. package/dist/types/cli/setup-cli.d.ts +1 -1
  11. package/dist/types/cli/setup-model-picker.d.ts +14 -0
  12. package/dist/types/collab/protocol.d.ts +1 -1
  13. package/dist/types/commands/say.d.ts +24 -0
  14. package/dist/types/config/keybindings.d.ts +3 -3
  15. package/dist/types/config/model-registry.d.ts +10 -0
  16. package/dist/types/config/models-config-schema.d.ts +12 -0
  17. package/dist/types/config/models-config.d.ts +8 -2
  18. package/dist/types/config/settings-schema.d.ts +261 -58
  19. package/dist/types/export/html/index.d.ts +2 -1
  20. package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  22. package/dist/types/extensibility/extensions/types.d.ts +47 -1
  23. package/dist/types/extensibility/hooks/index.d.ts +2 -1
  24. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
  25. package/dist/types/extensibility/plugins/loader.d.ts +11 -0
  26. package/dist/types/extensibility/shared-events.d.ts +1 -1
  27. package/dist/types/extensibility/skills.d.ts +10 -0
  28. package/dist/types/goals/guided-setup.d.ts +18 -0
  29. package/dist/types/goals/state.d.ts +1 -1
  30. package/dist/types/hindsight/transcript.d.ts +1 -1
  31. package/dist/types/index.d.ts +5 -0
  32. package/dist/types/internal-urls/local-protocol.d.ts +4 -2
  33. package/dist/types/main.d.ts +4 -3
  34. package/dist/types/mcp/startup-events.d.ts +11 -0
  35. package/dist/types/memories/index.d.ts +7 -0
  36. package/dist/types/memory-backend/local-backend.d.ts +4 -3
  37. package/dist/types/mnemopi/config.d.ts +4 -4
  38. package/dist/types/modes/components/agent-hub.d.ts +6 -0
  39. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  40. package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
  41. package/dist/types/modes/components/custom-editor.d.ts +39 -1
  42. package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
  43. package/dist/types/modes/components/session-selector.d.ts +1 -1
  44. package/dist/types/modes/components/tool-execution.d.ts +26 -16
  45. package/dist/types/modes/components/transcript-container.d.ts +23 -2
  46. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  47. package/dist/types/modes/components/usage-row.d.ts +3 -0
  48. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  49. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  50. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  51. package/dist/types/modes/gradient-highlight.d.ts +9 -4
  52. package/dist/types/modes/image-references.d.ts +6 -0
  53. package/dist/types/modes/interactive-mode.d.ts +27 -3
  54. package/dist/types/modes/magic-keywords.d.ts +13 -1
  55. package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
  56. package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
  57. package/dist/types/modes/runtime-init.d.ts +4 -0
  58. package/dist/types/modes/theme/theme.d.ts +13 -2
  59. package/dist/types/modes/types.d.ts +8 -2
  60. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  61. package/dist/types/registry/agent-registry.d.ts +17 -0
  62. package/dist/types/secrets/obfuscator.d.ts +1 -1
  63. package/dist/types/session/agent-session.d.ts +14 -2
  64. package/dist/types/session/indexed-session-storage.d.ts +3 -4
  65. package/dist/types/session/session-context.d.ts +39 -0
  66. package/dist/types/session/session-entries.d.ts +159 -0
  67. package/dist/types/session/session-listing.d.ts +69 -0
  68. package/dist/types/session/session-loader.d.ts +16 -0
  69. package/dist/types/session/session-manager.d.ts +82 -474
  70. package/dist/types/session/session-migrations.d.ts +12 -0
  71. package/dist/types/session/session-paths.d.ts +25 -0
  72. package/dist/types/session/session-persistence.d.ts +8 -0
  73. package/dist/types/session/session-storage.d.ts +11 -12
  74. package/dist/types/session/snapcompact-inline.d.ts +12 -1
  75. package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
  76. package/dist/types/session/tool-choice-queue.d.ts +6 -6
  77. package/dist/types/stt/asr-client.d.ts +90 -0
  78. package/dist/types/stt/asr-protocol.d.ts +97 -0
  79. package/dist/types/stt/asr-worker.d.ts +2 -0
  80. package/dist/types/stt/downloader.d.ts +38 -0
  81. package/dist/types/stt/endpointer.d.ts +59 -0
  82. package/dist/types/stt/index.d.ts +5 -1
  83. package/dist/types/stt/models.d.ts +120 -0
  84. package/dist/types/stt/recorder.d.ts +17 -0
  85. package/dist/types/stt/stt-controller.d.ts +6 -0
  86. package/dist/types/stt/transcriber.d.ts +5 -7
  87. package/dist/types/stt/wav.d.ts +29 -0
  88. package/dist/types/system-prompt.d.ts +4 -0
  89. package/dist/types/task/executor.d.ts +2 -0
  90. package/dist/types/task/index.d.ts +9 -1
  91. package/dist/types/task/types.d.ts +36 -0
  92. package/dist/types/tools/bash.d.ts +2 -2
  93. package/dist/types/tools/eval-render.d.ts +1 -1
  94. package/dist/types/tools/index.d.ts +11 -1
  95. package/dist/types/tools/irc.d.ts +1 -0
  96. package/dist/types/tools/learn.d.ts +51 -0
  97. package/dist/types/tools/manage-skill.d.ts +40 -0
  98. package/dist/types/tools/plan-mode-guard.d.ts +10 -0
  99. package/dist/types/tools/renderers.d.ts +7 -11
  100. package/dist/types/tools/ssh.d.ts +1 -1
  101. package/dist/types/tools/todo.d.ts +1 -1
  102. package/dist/types/tools/tts.d.ts +25 -0
  103. package/dist/types/tools/write.d.ts +1 -1
  104. package/dist/types/tts/downloader.d.ts +20 -0
  105. package/dist/types/tts/index.d.ts +8 -0
  106. package/dist/types/tts/models.d.ts +82 -0
  107. package/dist/types/tts/player.d.ts +32 -0
  108. package/dist/types/tts/runtime.d.ts +6 -0
  109. package/dist/types/tts/streaming-player.d.ts +41 -0
  110. package/dist/types/tts/tts-client.d.ts +93 -0
  111. package/dist/types/tts/tts-protocol.d.ts +95 -0
  112. package/dist/types/tts/tts-worker.d.ts +2 -0
  113. package/dist/types/tts/vocalizer.d.ts +41 -0
  114. package/dist/types/tts/wav.d.ts +8 -0
  115. package/dist/types/utils/tool-choice.d.ts +8 -0
  116. package/dist/types/utils/tools-manager.d.ts +2 -1
  117. package/dist/types/utils/tools-manager.test.d.ts +1 -0
  118. package/dist/types/web/scrapers/github.d.ts +1 -1
  119. package/package.json +15 -14
  120. package/src/async/job-manager.ts +49 -0
  121. package/src/autolearn/controller.ts +139 -0
  122. package/src/autolearn/managed-skills.ts +257 -0
  123. package/src/autoresearch/state.ts +1 -1
  124. package/src/autoresearch/types.ts +1 -1
  125. package/src/cli/args.ts +56 -2
  126. package/src/cli/session-picker.ts +2 -1
  127. package/src/cli/setup-cli.ts +148 -47
  128. package/src/cli/setup-model-picker.ts +43 -0
  129. package/src/cli-commands.ts +1 -0
  130. package/src/cli.ts +45 -13
  131. package/src/collab/host.ts +1 -1
  132. package/src/collab/protocol.ts +1 -1
  133. package/src/commands/say.ts +102 -0
  134. package/src/commands/setup.ts +1 -1
  135. package/src/commit/agentic/tools/analyze-file.ts +3 -0
  136. package/src/config/keybindings.ts +2 -2
  137. package/src/config/model-discovery.ts +11 -5
  138. package/src/config/model-registry.ts +64 -9
  139. package/src/config/models-config-schema.ts +4 -1
  140. package/src/config/models-config.ts +2 -1
  141. package/src/config/settings-schema.ts +248 -32
  142. package/src/config/settings.ts +10 -0
  143. package/src/discovery/builtin.ts +23 -1
  144. package/src/discovery/claude-plugins.ts +44 -5
  145. package/src/discovery/helpers.ts +41 -1
  146. package/src/eval/__tests__/budget-bridge.test.ts +1 -1
  147. package/src/eval/js/shared/prelude.txt +69 -17
  148. package/src/export/html/index.ts +3 -6
  149. package/src/extensibility/extensions/model-api.ts +41 -0
  150. package/src/extensibility/extensions/runner.ts +4 -0
  151. package/src/extensibility/extensions/types.ts +52 -1
  152. package/src/extensibility/extensions/wrapper.ts +41 -5
  153. package/src/extensibility/hooks/index.ts +2 -1
  154. package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
  155. package/src/extensibility/plugins/loader.ts +30 -19
  156. package/src/extensibility/plugins/manager.ts +221 -90
  157. package/src/extensibility/shared-events.ts +1 -1
  158. package/src/extensibility/skills.ts +96 -15
  159. package/src/goals/guided-setup.ts +133 -0
  160. package/src/goals/state.ts +1 -1
  161. package/src/hindsight/transcript.ts +1 -1
  162. package/src/index.ts +5 -0
  163. package/src/internal-urls/docs-index.generated.ts +10 -10
  164. package/src/internal-urls/history-protocol.ts +1 -1
  165. package/src/internal-urls/local-protocol.ts +29 -7
  166. package/src/main.ts +27 -7
  167. package/src/mcp/startup-events.ts +21 -0
  168. package/src/mcp/transports/stdio.ts +2 -1
  169. package/src/memories/index.ts +146 -11
  170. package/src/memory-backend/local-backend.ts +11 -5
  171. package/src/mnemopi/backend.ts +1 -0
  172. package/src/mnemopi/config.ts +26 -10
  173. package/src/modes/acp/acp-agent.ts +3 -5
  174. package/src/modes/components/agent-hub.ts +49 -4
  175. package/src/modes/components/assistant-message.ts +4 -37
  176. package/src/modes/components/compaction-summary-message.ts +125 -26
  177. package/src/modes/components/custom-editor.test.ts +96 -0
  178. package/src/modes/components/custom-editor.ts +164 -8
  179. package/src/modes/components/session-selector.ts +1 -1
  180. package/src/modes/components/settings-defs.ts +7 -0
  181. package/src/modes/components/tool-execution.ts +82 -43
  182. package/src/modes/components/transcript-container.ts +70 -1
  183. package/src/modes/components/tree-selector.ts +1 -1
  184. package/src/modes/components/usage-row.ts +18 -0
  185. package/src/modes/components/user-message.ts +4 -2
  186. package/src/modes/controllers/command-controller.ts +14 -4
  187. package/src/modes/controllers/event-controller.ts +78 -11
  188. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  189. package/src/modes/controllers/input-controller.ts +258 -27
  190. package/src/modes/controllers/selector-controller.ts +12 -2
  191. package/src/modes/gradient-highlight.ts +21 -9
  192. package/src/modes/image-references.ts +20 -0
  193. package/src/modes/interactive-mode.ts +286 -40
  194. package/src/modes/magic-keywords.ts +27 -5
  195. package/src/modes/rpc/rpc-mode.ts +146 -14
  196. package/src/modes/rpc/rpc-subagents.ts +2 -2
  197. package/src/modes/rpc/rpc-types.ts +8 -2
  198. package/src/modes/runtime-init.ts +28 -3
  199. package/src/modes/theme/theme.ts +98 -50
  200. package/src/modes/types.ts +6 -2
  201. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  202. package/src/modes/utils/ui-helpers.ts +34 -6
  203. package/src/priority.json +5 -1
  204. package/src/prompts/agents/task.md +1 -0
  205. package/src/prompts/goals/guided-goal-interview.md +8 -0
  206. package/src/prompts/goals/guided-goal-system.md +12 -0
  207. package/src/prompts/memories/read-path.md +6 -0
  208. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  209. package/src/prompts/system/autolearn-guidance.md +7 -0
  210. package/src/prompts/system/autolearn-nudge.md +3 -0
  211. package/src/prompts/system/eager-task.md +7 -0
  212. package/src/prompts/system/eager-todo.md +11 -6
  213. package/src/prompts/system/subagent-system-prompt.md +4 -0
  214. package/src/prompts/system/system-prompt.md +10 -5
  215. package/src/prompts/system/title-marker-instruction.md +1 -0
  216. package/src/prompts/system/title-system-marker.md +16 -0
  217. package/src/prompts/tools/job.md +1 -0
  218. package/src/prompts/tools/learn.md +7 -0
  219. package/src/prompts/tools/manage-skill.md +9 -0
  220. package/src/prompts/tools/task.md +3 -0
  221. package/src/registry/agent-registry.ts +30 -0
  222. package/src/sdk.ts +88 -24
  223. package/src/secrets/obfuscator.ts +1 -1
  224. package/src/session/agent-session.ts +209 -87
  225. package/src/session/history-storage.ts +2 -2
  226. package/src/session/indexed-session-storage.ts +7 -17
  227. package/src/session/session-context.ts +352 -0
  228. package/src/session/session-entries.ts +194 -0
  229. package/src/session/session-listing.ts +588 -0
  230. package/src/session/session-loader.ts +106 -0
  231. package/src/session/session-manager.ts +933 -3145
  232. package/src/session/session-migrations.ts +78 -0
  233. package/src/session/session-paths.ts +193 -0
  234. package/src/session/session-persistence.ts +131 -0
  235. package/src/session/session-storage.ts +91 -50
  236. package/src/session/snapcompact-inline.ts +21 -1
  237. package/src/session/snapcompact-savings-journal.ts +113 -0
  238. package/src/session/tool-choice-queue.ts +23 -11
  239. package/src/slash-commands/builtin-registry.ts +25 -3
  240. package/src/stt/asr-client.ts +520 -0
  241. package/src/stt/asr-protocol.ts +65 -0
  242. package/src/stt/asr-worker.ts +790 -0
  243. package/src/stt/downloader.ts +107 -47
  244. package/src/stt/endpointer.ts +259 -0
  245. package/src/stt/index.ts +5 -1
  246. package/src/stt/models.ts +150 -0
  247. package/src/stt/recorder.ts +247 -60
  248. package/src/stt/stt-controller.ts +201 -22
  249. package/src/stt/transcriber.ts +37 -68
  250. package/src/stt/wav.ts +173 -0
  251. package/src/system-prompt.ts +8 -0
  252. package/src/task/agents.ts +1 -2
  253. package/src/task/executor.ts +49 -15
  254. package/src/task/index.ts +60 -6
  255. package/src/task/render.ts +83 -8
  256. package/src/task/types.ts +53 -0
  257. package/src/tools/ask.ts +8 -0
  258. package/src/tools/bash.ts +4 -3
  259. package/src/tools/eval-render.ts +4 -3
  260. package/src/tools/index.ts +40 -4
  261. package/src/tools/irc.ts +10 -2
  262. package/src/tools/job.ts +14 -2
  263. package/src/tools/learn.ts +144 -0
  264. package/src/tools/manage-skill.ts +104 -0
  265. package/src/tools/plan-mode-guard.ts +53 -19
  266. package/src/tools/renderers.ts +7 -11
  267. package/src/tools/ssh.ts +4 -3
  268. package/src/tools/todo.ts +1 -1
  269. package/src/tools/tts.ts +203 -92
  270. package/src/tools/write.ts +18 -2
  271. package/src/tts/downloader.ts +64 -0
  272. package/src/tts/index.ts +8 -0
  273. package/src/tts/models.ts +137 -0
  274. package/src/tts/player.ts +137 -0
  275. package/src/tts/runtime.ts +21 -0
  276. package/src/tts/streaming-player.ts +266 -0
  277. package/src/tts/tts-client.ts +647 -0
  278. package/src/tts/tts-protocol.ts +60 -0
  279. package/src/tts/tts-worker.ts +497 -0
  280. package/src/tts/vocalizer.ts +162 -0
  281. package/src/tts/wav.ts +58 -0
  282. package/src/utils/title-generator.ts +48 -5
  283. package/src/utils/tool-choice.ts +16 -0
  284. package/src/utils/tools-manager.test.ts +25 -0
  285. package/src/utils/tools-manager.ts +19 -1
  286. package/src/web/scrapers/github.ts +96 -0
  287. package/src/web/search/index.ts +13 -0
  288. package/src/web/search/providers/searxng.ts +13 -1
  289. package/dist/types/stt/setup.d.ts +0 -18
  290. package/src/stt/setup.ts +0 -52
  291. package/src/stt/transcribe.py +0 -70
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import * as os from "node:os";
2
3
  import * as path from "node:path";
3
4
  import {
4
5
  getPluginsDir,
@@ -11,6 +12,8 @@ import {
11
12
  logger,
12
13
  } from "@oh-my-pi/pi-utils";
13
14
  import { type GitSource, parseGitUrl } from "./git-url";
15
+ import { installLegacyPiSpecifierShim, loadLegacyPiModule } from "./legacy-pi-compat";
16
+ import { resolvePluginManifestEntries } from "./loader";
14
17
  import { extractPackageName, parsePluginSpec } from "./parser";
15
18
  import type {
16
19
  DoctorCheck,
@@ -74,6 +77,34 @@ function gitInstallSpec(original: string, source: GitSource): string {
74
77
  return `${source.repo}#${source.ref}`;
75
78
  }
76
79
 
80
+ function findGitPackageName(source: GitSource, deps: Record<string, string>): string | undefined {
81
+ for (const [key, value] of Object.entries(deps)) {
82
+ if (typeof value !== "string") {
83
+ continue;
84
+ }
85
+ const installedSource = parseGitUrl(value);
86
+ if (installedSource && installedSource.host === source.host && installedSource.path === source.path) {
87
+ return key;
88
+ }
89
+ }
90
+ return undefined;
91
+ }
92
+
93
+ function hasDefaultExport(value: unknown): value is { default?: unknown } {
94
+ return typeof value === "object" && value !== null && "default" in value;
95
+ }
96
+
97
+ function hasExtensionFactoryExport(module: unknown): boolean {
98
+ return typeof module === "function" || (hasDefaultExport(module) && typeof module.default === "function");
99
+ }
100
+
101
+ interface PluginPackageSnapshot {
102
+ readonly actualName: string;
103
+ readonly packagePath: string;
104
+ readonly backupRoot: string;
105
+ readonly backupPath: string;
106
+ }
107
+
77
108
  // =============================================================================
78
109
  // Plugin Manager
79
110
  // =============================================================================
@@ -173,6 +204,88 @@ export class PluginManager {
173
204
  }
174
205
  }
175
206
 
207
+ async #snapshotInstalledPackage(actualName: string | undefined): Promise<PluginPackageSnapshot | null> {
208
+ if (!actualName) {
209
+ return null;
210
+ }
211
+ const packagePath = path.join(getPluginsNodeModules(), actualName);
212
+ try {
213
+ await fs.promises.lstat(packagePath);
214
+ } catch (err) {
215
+ if (isEnoent(err)) {
216
+ return null;
217
+ }
218
+ throw err;
219
+ }
220
+
221
+ const backupRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), "omp-plugin-backup-"));
222
+ const backupPath = path.join(backupRoot, "package");
223
+ await fs.promises.cp(packagePath, backupPath, { recursive: true, verbatimSymlinks: true });
224
+ return { actualName, packagePath, backupRoot, backupPath };
225
+ }
226
+
227
+ async #cleanupSnapshot(snapshot: PluginPackageSnapshot | null): Promise<void> {
228
+ if (!snapshot) {
229
+ return;
230
+ }
231
+ try {
232
+ await fs.promises.rm(snapshot.backupRoot, { recursive: true, force: true });
233
+ } catch (err) {
234
+ logger.warn("Failed to remove plugin install backup", { plugin: snapshot.actualName, error: String(err) });
235
+ }
236
+ }
237
+
238
+ async #rollbackFailedInstall(
239
+ actualName: string,
240
+ packageJsonBefore: string,
241
+ snapshot: PluginPackageSnapshot | null,
242
+ ): Promise<void> {
243
+ await Bun.write(getPluginsPackageJson(), packageJsonBefore);
244
+ const packagePath = path.join(getPluginsNodeModules(), actualName);
245
+ await fs.promises.rm(packagePath, { recursive: true, force: true });
246
+ if (!snapshot) {
247
+ return;
248
+ }
249
+ await fs.promises.mkdir(path.dirname(snapshot.packagePath), { recursive: true });
250
+ await fs.promises.cp(snapshot.backupPath, snapshot.packagePath, { recursive: true, verbatimSymlinks: true });
251
+ }
252
+
253
+ async #validateInstalledExtensions(plugin: InstalledPlugin): Promise<void> {
254
+ const declaredEntries = resolvePluginManifestEntries(plugin, "extensions");
255
+ if (declaredEntries.length === 0) {
256
+ return;
257
+ }
258
+
259
+ const errors: string[] = [];
260
+ const loadable: string[] = [];
261
+ for (const { entry, resolvedPath } of declaredEntries) {
262
+ if (resolvedPath === null) {
263
+ errors.push(`${entry}: declared extension entry not found on disk`);
264
+ } else {
265
+ loadable.push(resolvedPath);
266
+ }
267
+ }
268
+
269
+ if (loadable.length > 0) {
270
+ installLegacyPiSpecifierShim();
271
+ for (const extensionPath of loadable) {
272
+ try {
273
+ const module = await loadLegacyPiModule(extensionPath);
274
+ if (!hasExtensionFactoryExport(module)) {
275
+ errors.push(`${extensionPath}: extension does not export a valid factory function`);
276
+ }
277
+ } catch (err) {
278
+ const message = err instanceof Error ? err.message : String(err);
279
+ errors.push(`${extensionPath}: ${message}`);
280
+ }
281
+ }
282
+ }
283
+
284
+ if (errors.length > 0) {
285
+ throw new Error(`Plugin ${plugin.name} extension validation failed:\n${errors.join("\n")}`);
286
+ }
287
+ }
288
+
176
289
  // ==========================================================================
177
290
  // Install / Uninstall
178
291
  // ==========================================================================
@@ -217,113 +330,131 @@ export class PluginManager {
217
330
  };
218
331
  }
219
332
  const pkgJsonPath = getPluginsPackageJson();
220
- const depsBefore = gitSource ? await this.#readDeps(pkgJsonPath) : {};
333
+ const packageJsonBefore = await Bun.file(pkgJsonPath).text();
334
+ const depsBefore = await this.#readDeps(pkgJsonPath);
221
335
  const packageInstallSpec = gitSource ? gitInstallSpec(spec.packageName, gitSource) : spec.packageName;
336
+ const existingActualName = gitSource
337
+ ? findGitPackageName(gitSource, depsBefore)
338
+ : extractPackageName(spec.packageName);
339
+ const packageSnapshot = await this.#snapshotInstalledPackage(existingActualName);
222
340
 
223
- // Run npm install
224
- const proc = Bun.spawn(["bun", "install", packageInstallSpec], {
225
- cwd: getPluginsDir(),
226
- stdin: "ignore",
227
- stdout: "pipe",
228
- stderr: "pipe",
229
- windowsHide: true,
230
- });
341
+ try {
342
+ // Run npm install
343
+ const proc = Bun.spawn(["bun", "install", packageInstallSpec], {
344
+ cwd: getPluginsDir(),
345
+ stdin: "ignore",
346
+ stdout: "pipe",
347
+ stderr: "pipe",
348
+ windowsHide: true,
349
+ });
231
350
 
232
- const exitCode = await proc.exited;
233
- if (exitCode !== 0) {
234
- const stderr = await new Response(proc.stderr).text();
235
- throw new Error(`npm install failed: ${stderr}`);
236
- }
237
- // Resolve actual package name. npm specs encode the name (strip version);
238
- // git specs do not, so diff plugins/package.json deps to find the new entry.
239
- let actualName: string;
240
- if (gitSource) {
241
- const depsAfter = await this.#readDeps(pkgJsonPath);
242
- let resolved: string | undefined;
243
- for (const key of Object.keys(depsAfter)) {
244
- if (!(key in depsBefore)) {
245
- resolved = key;
246
- break;
247
- }
351
+ const exitCode = await proc.exited;
352
+ if (exitCode !== 0) {
353
+ const stderr = await new Response(proc.stderr).text();
354
+ throw new Error(`npm install failed: ${stderr}`);
248
355
  }
249
- // Fallback: a force-reinstall of an already-present git plugin will not
250
- // add a new key, just rewrite the existing one to the new spec value.
251
- // Match by the install value for force-reinstalls where no new key is
252
- // added (non-GitHub shorthands are normalized before bun sees them).
253
- if (!resolved) {
254
- const needle = packageInstallSpec.replace(/^git\+/i, "");
255
- for (const [key, value] of Object.entries(depsAfter)) {
256
- if (typeof value === "string" && value.includes(needle)) {
356
+ // Resolve actual package name. npm specs encode the name (strip version);
357
+ // git specs do not, so diff plugins/package.json deps to find the new entry.
358
+ let actualName: string;
359
+ if (gitSource) {
360
+ const depsAfter = await this.#readDeps(pkgJsonPath);
361
+ let resolved: string | undefined;
362
+ for (const key of Object.keys(depsAfter)) {
363
+ if (!(key in depsBefore)) {
257
364
  resolved = key;
258
365
  break;
259
366
  }
260
367
  }
368
+ // Fallback: a force-reinstall of an already-present git plugin will not
369
+ // add a new key, just rewrite the existing one to the new spec value.
370
+ // Match by repository identity, not by ref, so failed upgrades from
371
+ // one ref to another still resolve to the original package name.
372
+ if (!resolved) {
373
+ resolved = findGitPackageName(gitSource, depsAfter);
374
+ }
375
+ if (!resolved) {
376
+ throw new Error(
377
+ `Installed ${spec.packageName} but could not determine package name from plugins/package.json`,
378
+ );
379
+ }
380
+ actualName = resolved;
381
+ } else {
382
+ actualName = extractPackageName(spec.packageName);
261
383
  }
262
- if (!resolved) {
263
- throw new Error(
264
- `Installed ${spec.packageName} but could not determine package name from plugins/package.json`,
265
- );
266
- }
267
- actualName = resolved;
268
- } else {
269
- actualName = extractPackageName(spec.packageName);
270
- }
271
- const pkgPath = path.join(getPluginsNodeModules(), actualName, "package.json");
384
+ const pkgPath = path.join(getPluginsNodeModules(), actualName, "package.json");
272
385
 
273
- let pkg: { name: string; version: string; omp?: PluginManifest; pi?: PluginManifest };
274
- try {
275
- pkg = await Bun.file(pkgPath).json();
276
- } catch (err) {
277
- if (isEnoent(err)) {
278
- throw new Error(`Package installed but package.json not found at ${pkgPath}`);
386
+ let pkg: { name: string; version: string; omp?: PluginManifest; pi?: PluginManifest };
387
+ try {
388
+ pkg = await Bun.file(pkgPath).json();
389
+ } catch (err) {
390
+ if (isEnoent(err)) {
391
+ throw new Error(`Package installed but package.json not found at ${pkgPath}`);
392
+ }
393
+ throw err;
279
394
  }
280
- throw err;
281
- }
282
- const manifest: PluginManifest = pkg.omp || pkg.pi || { version: pkg.version };
283
- manifest.version = pkg.version;
284
-
285
- // Resolve enabled features
286
- let enabledFeatures: string[] | null = null;
287
- if (spec.features === "*") {
288
- // All features
289
- enabledFeatures = manifest.features ? Object.keys(manifest.features) : null;
290
- } else if (Array.isArray(spec.features)) {
291
- if (spec.features.length > 0) {
292
- // Validate requested features exist
293
- if (manifest.features) {
294
- for (const feat of spec.features) {
295
- if (!(feat in manifest.features)) {
296
- throw new Error(
297
- `Unknown feature "${feat}" in ${actualName}. Available: ${Object.keys(manifest.features).join(", ")}`,
298
- );
395
+ const manifest: PluginManifest = pkg.omp || pkg.pi || { version: pkg.version };
396
+ manifest.version = pkg.version;
397
+
398
+ // Resolve enabled features
399
+ let enabledFeatures: string[] | null = null;
400
+ if (spec.features === "*") {
401
+ // All features
402
+ enabledFeatures = manifest.features ? Object.keys(manifest.features) : null;
403
+ } else if (Array.isArray(spec.features)) {
404
+ if (spec.features.length > 0) {
405
+ // Validate requested features exist
406
+ if (manifest.features) {
407
+ for (const feat of spec.features) {
408
+ if (!(feat in manifest.features)) {
409
+ throw new Error(
410
+ `Unknown feature "${feat}" in ${actualName}. Available: ${Object.keys(manifest.features).join(", ")}`,
411
+ );
412
+ }
299
413
  }
300
414
  }
415
+ enabledFeatures = spec.features;
416
+ } else {
417
+ // Empty array = no optional features
418
+ enabledFeatures = [];
301
419
  }
302
- enabledFeatures = spec.features;
303
- } else {
304
- // Empty array = no optional features
305
- enabledFeatures = [];
306
420
  }
307
- }
308
- // null = use defaults
421
+ // null = use defaults
309
422
 
310
- // Update runtime config
311
- const config = await this.#ensureConfigLoaded();
312
- config.plugins[pkg.name] = {
313
- version: pkg.version,
314
- enabledFeatures,
315
- enabled: true,
316
- };
317
- await this.#saveRuntimeConfig();
423
+ const installedPlugin: InstalledPlugin = {
424
+ name: pkg.name,
425
+ version: pkg.version,
426
+ path: path.join(getPluginsNodeModules(), actualName),
427
+ manifest,
428
+ enabledFeatures,
429
+ enabled: true,
430
+ };
318
431
 
319
- return {
320
- name: pkg.name,
321
- version: pkg.version,
322
- path: path.join(getPluginsNodeModules(), actualName),
323
- manifest,
324
- enabledFeatures,
325
- enabled: true,
326
- };
432
+ try {
433
+ await this.#validateInstalledExtensions(installedPlugin);
434
+ } catch (err) {
435
+ try {
436
+ await this.#rollbackFailedInstall(actualName, packageJsonBefore, packageSnapshot);
437
+ } catch (rollbackErr) {
438
+ const message = err instanceof Error ? err.message : String(err);
439
+ const rollbackMessage = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
440
+ throw new Error(`${message}\nRollback failed: ${rollbackMessage}`);
441
+ }
442
+ throw err;
443
+ }
444
+
445
+ // Update runtime config
446
+ const config = await this.#ensureConfigLoaded();
447
+ config.plugins[pkg.name] = {
448
+ version: pkg.version,
449
+ enabledFeatures,
450
+ enabled: true,
451
+ };
452
+ await this.#saveRuntimeConfig();
453
+
454
+ return installedPlugin;
455
+ } finally {
456
+ await this.#cleanupSnapshot(packageSnapshot);
457
+ }
327
458
  }
328
459
 
329
460
  /**
@@ -17,7 +17,7 @@ import type { CompactionPreparation, CompactionResult } from "@oh-my-pi/pi-agent
17
17
  import type { ImageContent, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
18
18
  import type { Rule } from "../capability/rule";
19
19
  import type { Goal, GoalModeState } from "../goals/state";
20
- import type { BranchSummaryEntry, CompactionEntry, SessionEntry } from "../session/session-manager";
20
+ import type { BranchSummaryEntry, CompactionEntry, SessionEntry } from "../session/session-entries";
21
21
  import type { TodoItem } from "../tools/todo";
22
22
 
23
23
  // ============================================================================
@@ -1,6 +1,11 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as os from "node:os";
3
3
  import { getProjectDir } from "@oh-my-pi/pi-utils";
4
+ import {
5
+ isValidManagedSkillName,
6
+ MANAGED_SKILLS_PROVIDER_ID,
7
+ sanitizeManagedDescription,
8
+ } from "../autolearn/managed-skills";
4
9
  import { skillCapability } from "../capability/skill";
5
10
  import type { SourceMeta } from "../capability/types";
6
11
  import type { SkillsSettings } from "../config/settings";
@@ -54,6 +59,21 @@ export function resetActiveSkillsForTests(): void {
54
59
  activeSkills = [];
55
60
  }
56
61
 
62
+ /**
63
+ * Whether `name` is already claimed by an active authored (non-managed) skill.
64
+ *
65
+ * Managed (auto-learn) skills resolve dead-last in discovery, so an authored
66
+ * skill of the same name always wins (see `loadSkills`) and a managed skill
67
+ * written under an authored name is silently dropped — it never surfaces.
68
+ * `manage_skill` create consults this to refuse the write up front instead of
69
+ * reporting a false "Created" for a skill that can never appear.
70
+ */
71
+ export function isNameClaimedByAuthoredSkill(name: string): boolean {
72
+ return getActiveSkills().some(
73
+ skill => skill.name === name && skill._source?.provider !== MANAGED_SKILLS_PROVIDER_ID,
74
+ );
75
+ }
76
+
57
77
  export interface LoadSkillsFromDirOptions {
58
78
  /** Directory to scan for skills */
59
79
  dir: string;
@@ -119,24 +139,23 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
119
139
  if (!enabled) {
120
140
  return { skills: [], warnings: [] };
121
141
  }
122
-
123
142
  // Fall-through gate for third-party CLI providers (claude-plugins, opencode,
124
- // gemini, github, ...) that share user intent with the named source toggles
125
- // but don't have a dedicated control of their own. The OMP-native providers
126
- // (`agents`, `native`) get explicit toggles above and never fall through:
127
- // disabling Claude/Codex must not silently break `.agent[s]/skills`
128
- // discovery (issue #2401).
129
- const anyBuiltInSkillSourceEnabled =
130
- enableCodexUser ||
131
- enableClaudeUser ||
132
- enableClaudeProject ||
133
- enablePiUser ||
134
- enablePiProject ||
135
- enableAgentsUser ||
136
- enableAgentsProject;
143
+ // gemini, github, ...) that share user intent with the named third-party
144
+ // source toggles but don't have a dedicated control of their own. Only the
145
+ // third-party toggles count here: the OMP-native providers (`agents`,
146
+ // `native`) get explicit branches in `isSourceEnabled` below, so folding
147
+ // them into the fallback would re-enable unrelated third-party CLIs whenever
148
+ // the user kept the default `.agent[s]/skills` toggles on while turning off
149
+ // Codex/Claude/Pi (issue #2401 / PR #2405 review).
150
+ const anyThirdPartySkillToggleEnabled =
151
+ enableCodexUser || enableClaudeUser || enableClaudeProject || enablePiUser || enablePiProject;
137
152
 
138
153
  function isSourceEnabled(source: SourceMeta): boolean {
139
154
  const { provider, level } = source;
155
+ // Managed skills (auto-learn) are OMP-native and discovered unconditionally
156
+ // — third-party CLI toggles must never silently hide them (cf. #2401). The
157
+ // master `enabled` flag above still gates them.
158
+ if (provider === MANAGED_SKILLS_PROVIDER_ID) return true;
140
159
  if (provider === "codex" && level === "user") return enableCodexUser;
141
160
  if (provider === "claude" && level === "user") return enableClaudeUser;
142
161
  if (provider === "claude" && level === "project") return enableClaudeProject;
@@ -144,7 +163,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
144
163
  if (provider === "native" && level === "project") return enablePiProject;
145
164
  if (provider === "agents" && level === "user") return enableAgentsUser;
146
165
  if (provider === "agents" && level === "project") return enableAgentsProject;
147
- return anyBuiltInSkillSourceEnabled;
166
+ return anyThirdPartySkillToggleEnabled;
148
167
  }
149
168
 
150
169
  // Use capability API to load all skills
@@ -192,6 +211,9 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
192
211
  // Process skills with resolved paths
193
212
  for (let i = 0; i < filteredSkills.length; i++) {
194
213
  const capSkill = filteredSkills[i];
214
+ // Managed (auto-learn) skills are resolved dead-last (below) so any
215
+ // authored skill of the same name — from ANY provider or custom dir — wins.
216
+ if (capSkill._source.provider === MANAGED_SKILLS_PROVIDER_ID) continue;
195
217
  const resolvedPath = realPaths[i];
196
218
 
197
219
  // Skip silently if we've already loaded this exact file (via symlink)
@@ -285,6 +307,65 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
285
307
  }
286
308
  }
287
309
 
310
+ // Managed (auto-learn) skills resolve dead-last with first-wins. Source from
311
+ // result.all (pre-dedup): capability-level dedup runs BEFORE isSourceEnabled,
312
+ // so a managed skill can be shadowed by a higher-priority authored skill that
313
+ // is itself disabled here — managed must stay visible regardless of toggles.
314
+ // Validate the on-disk name (a hand-placed managed file could carry an unsafe
315
+ // frontmatter name) and re-sanitize the description on read. Descriptions and
316
+ // names both render unescaped into the system prompt.
317
+ const managedCandidates = result.all.filter(
318
+ capSkill =>
319
+ capSkill._source.provider === MANAGED_SKILLS_PROVIDER_ID &&
320
+ isValidManagedSkillName(capSkill.name) &&
321
+ !disabledSkillNames.has(capSkill.name) &&
322
+ !matchesIgnorePatterns(capSkill.name) &&
323
+ matchesIncludePatterns(capSkill.name),
324
+ );
325
+ // Names claimed by any ENABLED authored skill (from the pre-dedup superset).
326
+ // Managed defers to these even when capability dedup hid an enabled authored
327
+ // skill behind a disabled higher-priority one, so managed never masks it.
328
+ const enabledAuthoredNames = new Set(
329
+ result.all
330
+ .filter(
331
+ capSkill => capSkill._source.provider !== MANAGED_SKILLS_PROVIDER_ID && isSourceEnabled(capSkill._source),
332
+ )
333
+ .map(capSkill => capSkill.name),
334
+ );
335
+ const managedRealPaths = await Promise.all(
336
+ managedCandidates.map(async capSkill => {
337
+ try {
338
+ return await fs.realpath(capSkill.path);
339
+ } catch {
340
+ return capSkill.path;
341
+ }
342
+ }),
343
+ );
344
+ for (let i = 0; i < managedCandidates.length; i++) {
345
+ const capSkill = managedCandidates[i];
346
+ const resolvedPath = managedRealPaths[i];
347
+ if (realPathSet.has(resolvedPath)) continue;
348
+ if (enabledAuthoredNames.has(capSkill.name)) continue; // an enabled authored skill owns this name
349
+ // Already claimed — e.g. by a custom-directory skill. LOAD-BEARING: custom
350
+ // dirs never enter `result.all`, so they are absent from `enabledAuthoredNames`
351
+ // above; this map check is the ONLY veto that lets a custom-dir authored skill
352
+ // win over a same-named managed one. The custom-dir loop (which populates
353
+ // skillMap, ~30 lines up) MUST run before this block — do not reorder.
354
+ if (skillMap.has(capSkill.name)) continue;
355
+ const rawDescription =
356
+ typeof capSkill.frontmatter?.description === "string" ? capSkill.frontmatter.description : "";
357
+ skillMap.set(capSkill.name, {
358
+ name: capSkill.name,
359
+ description: sanitizeManagedDescription(rawDescription),
360
+ filePath: capSkill.path,
361
+ baseDir: capSkill.path.replace(/[\\/]SKILL\.md$/, ""),
362
+ source: `${capSkill._source.provider}:${capSkill.level}`,
363
+ hide: capSkill.frontmatter?.hide === true || capSkill.frontmatter?.disableModelInvocation === true,
364
+ _source: capSkill._source,
365
+ });
366
+ realPathSet.add(resolvedPath);
367
+ }
368
+
288
369
  const skills = Array.from(skillMap.values());
289
370
  // Deterministic ordering for prompt stability (case-insensitive, then exact name, then path).
290
371
  skills.sort((a, b) => compareSkillOrder(a.name, a.filePath, b.name, b.filePath));
@@ -0,0 +1,133 @@
1
+ import { instrumentedCompleteSimple, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
2
+ import type { Tool } from "@oh-my-pi/pi-ai";
3
+ import { prompt } from "@oh-my-pi/pi-utils";
4
+ import { extractTextContent, extractToolCall, parseJsonPayload } from "../commit/utils";
5
+ import guidedGoalInterviewPrompt from "../prompts/goals/guided-goal-interview.md" with { type: "text" };
6
+ import guidedGoalSystemPrompt from "../prompts/goals/guided-goal-system.md" with { type: "text" };
7
+ import type { AgentSession } from "../session/agent-session";
8
+ import { toReasoningEffort } from "../thinking";
9
+
10
+ const RESPOND_TOOL_NAME = "respond";
11
+
12
+ const RESPOND_TOOL: Tool = {
13
+ name: RESPOND_TOOL_NAME,
14
+ description: "Return the next guided-goal interview step.",
15
+ parameters: {
16
+ type: "object",
17
+ properties: {
18
+ kind: { type: "string", enum: ["question", "ready"] },
19
+ question: { type: "string" },
20
+ objective: { type: "string" },
21
+ },
22
+ required: ["kind"],
23
+ additionalProperties: false,
24
+ },
25
+ strict: false,
26
+ };
27
+
28
+ export interface GuidedGoalMessage {
29
+ role: "user" | "assistant";
30
+ content: string;
31
+ }
32
+
33
+ export type GuidedGoalTurnResult =
34
+ | { kind: "question"; question: string; objective?: string }
35
+ | { kind: "ready"; objective: string };
36
+
37
+ export interface GuidedGoalTurnOptions {
38
+ messages: readonly GuidedGoalMessage[];
39
+ signal?: AbortSignal;
40
+ }
41
+
42
+ function parseGuidedGoalPayload(value: unknown): GuidedGoalTurnResult {
43
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
44
+ throw new Error("guided goal returned an invalid response");
45
+ }
46
+ const payload = value as Record<string, unknown>;
47
+ if (payload.kind === "question" && typeof payload.question === "string" && payload.question.trim()) {
48
+ const question = payload.question.trim();
49
+ if (typeof payload.objective === "string" && payload.objective.trim()) {
50
+ return { kind: "question", question, objective: payload.objective.trim() };
51
+ }
52
+ return { kind: "question", question };
53
+ }
54
+ if (payload.kind === "ready" && typeof payload.objective === "string" && payload.objective.trim()) {
55
+ return { kind: "ready", objective: payload.objective.trim() };
56
+ }
57
+ throw new Error("guided goal returned an invalid response");
58
+ }
59
+
60
+ function parseToolArguments(value: unknown): unknown {
61
+ return typeof value === "string" ? parseJsonPayload(value) : value;
62
+ }
63
+
64
+ export async function runGuidedGoalTurn(
65
+ session: AgentSession,
66
+ options: GuidedGoalTurnOptions,
67
+ ): Promise<GuidedGoalTurnResult> {
68
+ const plan = session.resolveRoleModelWithThinking("plan");
69
+ const resolved = plan.model ? plan : session.resolveRoleModelWithThinking("slow");
70
+ if (!resolved.model) {
71
+ throw new Error("No plan or slow model is available for /guided-goal.");
72
+ }
73
+
74
+ const apiKey = await session.modelRegistry.getApiKey(resolved.model, session.sessionId);
75
+ if (!apiKey) {
76
+ throw new Error(`No API key for ${resolved.model.provider}/${resolved.model.id}`);
77
+ }
78
+
79
+ const userPrompt = prompt.render(guidedGoalInterviewPrompt, {
80
+ messages: options.messages.map(message => ({ label: message.role.toUpperCase(), content: message.content })),
81
+ });
82
+ // Secret obfuscation: route the user-authored transcript through the session obfuscator the
83
+ // same way normal turns do, so an API key / secret typed into the rough goal or an answer is
84
+ // never sent verbatim to the plan/slow provider. Deobfuscated again below before display/use.
85
+ const obfuscator = session.obfuscator;
86
+ const promptText = obfuscator?.hasSecrets() ? obfuscator.obfuscate(userPrompt) : userPrompt;
87
+ const response = await instrumentedCompleteSimple(
88
+ resolved.model,
89
+ {
90
+ systemPrompt: [prompt.render(guidedGoalSystemPrompt)],
91
+ messages: [{ role: "user", content: [{ type: "text", text: promptText }], timestamp: Date.now() }],
92
+ tools: [RESPOND_TOOL],
93
+ },
94
+ {
95
+ apiKey: session.modelRegistry.resolver(resolved.model, session.sessionId),
96
+ signal: options.signal,
97
+ reasoning: toReasoningEffort(resolved.thinkingLevel),
98
+ toolChoice: { type: "tool", name: RESPOND_TOOL_NAME },
99
+ },
100
+ { telemetry: resolveTelemetry(session.agent.telemetry, session.sessionId), oneshotKind: "guided_goal_setup" },
101
+ );
102
+
103
+ if (response.stopReason === "error") {
104
+ throw new Error(response.errorMessage ?? "guided goal request failed");
105
+ }
106
+ if (response.stopReason === "aborted") {
107
+ throw new Error("guided goal request aborted");
108
+ }
109
+
110
+ const call = extractToolCall(response, RESPOND_TOOL_NAME);
111
+ let result: GuidedGoalTurnResult;
112
+ if (call) {
113
+ result = parseGuidedGoalPayload(parseToolArguments(call.arguments));
114
+ } else {
115
+ const text = extractTextContent(response);
116
+ if (!text) {
117
+ throw new Error("guided goal returned an invalid response");
118
+ }
119
+ result = parseGuidedGoalPayload(parseJsonPayload(text));
120
+ }
121
+
122
+ // Reverse the obfuscation: restore any secret placeholders the model echoed back before the
123
+ // question/objective is shown or the goal is started.
124
+ if (!obfuscator?.hasSecrets()) return result;
125
+ if (result.kind === "question") {
126
+ return {
127
+ kind: "question",
128
+ question: obfuscator.deobfuscate(result.question),
129
+ objective: result.objective !== undefined ? obfuscator.deobfuscate(result.objective) : undefined,
130
+ };
131
+ }
132
+ return { kind: "ready", objective: obfuscator.deobfuscate(result.objective) };
133
+ }
@@ -1,4 +1,4 @@
1
- import type { UsageStatistics } from "../session/session-manager";
1
+ import type { UsageStatistics } from "../session/session-entries";
2
2
 
3
3
  export type GoalStatus = "active" | "paused" | "budget-limited" | "complete" | "dropped";
4
4
 
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { AssistantMessage } from "@oh-my-pi/pi-ai";
11
- import type { SessionEntry } from "../session/session-manager";
11
+ import type { SessionEntry } from "../session/session-entries";
12
12
  import type { HindsightMessage } from "./content";
13
13
 
14
14
  export interface ReadonlySessionManagerLike {