@ilya-lesikov/pi-pi 0.1.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 (271) hide show
  1. package/3p/pi-ask-user/index.ts +1835 -0
  2. package/3p/pi-ask-user/package.json +50 -0
  3. package/3p/pi-ask-user/single-select-layout.ts +203 -0
  4. package/3p/pi-lsp/extensions/lsp/client.ts +542 -0
  5. package/3p/pi-lsp/extensions/lsp/config.ts +135 -0
  6. package/3p/pi-lsp/extensions/lsp/effects/command.ts +42 -0
  7. package/3p/pi-lsp/extensions/lsp/effects/filesystem.ts +50 -0
  8. package/3p/pi-lsp/extensions/lsp/effects/runtime.ts +21 -0
  9. package/3p/pi-lsp/extensions/lsp/errors.ts +113 -0
  10. package/3p/pi-lsp/extensions/lsp/formatting.ts +315 -0
  11. package/3p/pi-lsp/extensions/lsp/index.ts +231 -0
  12. package/3p/pi-lsp/extensions/lsp/protocol.ts +209 -0
  13. package/3p/pi-lsp/extensions/lsp/retry.ts +39 -0
  14. package/3p/pi-lsp/extensions/lsp/tools/programs.ts +293 -0
  15. package/3p/pi-lsp/extensions/lsp/tools.ts +89 -0
  16. package/3p/pi-lsp/extensions/lsp/types.ts +243 -0
  17. package/3p/pi-lsp/package.json +54 -0
  18. package/3p/pi-plannotator/AGENTS.md +583 -0
  19. package/3p/pi-plannotator/apps/pi-extension/README.md +228 -0
  20. package/3p/pi-plannotator/apps/pi-extension/assistant-message.ts +128 -0
  21. package/3p/pi-plannotator/apps/pi-extension/config.test.ts +166 -0
  22. package/3p/pi-plannotator/apps/pi-extension/config.ts +318 -0
  23. package/3p/pi-plannotator/apps/pi-extension/current-pi-session.ts +147 -0
  24. package/3p/pi-plannotator/apps/pi-extension/index.ts +1279 -0
  25. package/3p/pi-plannotator/apps/pi-extension/package.json +56 -0
  26. package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.test.ts +13 -0
  27. package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +616 -0
  28. package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +342 -0
  29. package/3p/pi-plannotator/apps/pi-extension/plannotator.json +12 -0
  30. package/3p/pi-plannotator/apps/pi-extension/server/agent-jobs.ts +515 -0
  31. package/3p/pi-plannotator/apps/pi-extension/server/ai-runtime.ts +169 -0
  32. package/3p/pi-plannotator/apps/pi-extension/server/annotations.ts +85 -0
  33. package/3p/pi-plannotator/apps/pi-extension/server/external-annotations.ts +189 -0
  34. package/3p/pi-plannotator/apps/pi-extension/server/handlers.ts +210 -0
  35. package/3p/pi-plannotator/apps/pi-extension/server/helpers.ts +78 -0
  36. package/3p/pi-plannotator/apps/pi-extension/server/ide.ts +46 -0
  37. package/3p/pi-plannotator/apps/pi-extension/server/integrations.ts +195 -0
  38. package/3p/pi-plannotator/apps/pi-extension/server/network.test.ts +158 -0
  39. package/3p/pi-plannotator/apps/pi-extension/server/network.ts +268 -0
  40. package/3p/pi-plannotator/apps/pi-extension/server/pr.ts +126 -0
  41. package/3p/pi-plannotator/apps/pi-extension/server/project.ts +64 -0
  42. package/3p/pi-plannotator/apps/pi-extension/server/reference.ts +362 -0
  43. package/3p/pi-plannotator/apps/pi-extension/server/serverAnnotate.ts +199 -0
  44. package/3p/pi-plannotator/apps/pi-extension/server/serverPlan.ts +505 -0
  45. package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +1229 -0
  46. package/3p/pi-plannotator/apps/pi-extension/server/vcs.ts +121 -0
  47. package/3p/pi-plannotator/apps/pi-extension/server.test.ts +966 -0
  48. package/3p/pi-plannotator/apps/pi-extension/server.ts +44 -0
  49. package/3p/pi-plannotator/apps/pi-extension/tool-scope.test.ts +88 -0
  50. package/3p/pi-plannotator/apps/pi-extension/tool-scope.ts +39 -0
  51. package/3p/pi-plannotator/apps/pi-extension/tsconfig.json +16 -0
  52. package/3p/pi-plannotator/apps/pi-extension/vendor.sh +48 -0
  53. package/3p/pi-plannotator/package.json +53 -0
  54. package/3p/pi-plannotator/packages/ai/ai.test.ts +1407 -0
  55. package/3p/pi-plannotator/packages/ai/base-session.ts +94 -0
  56. package/3p/pi-plannotator/packages/ai/context.ts +250 -0
  57. package/3p/pi-plannotator/packages/ai/endpoints.ts +326 -0
  58. package/3p/pi-plannotator/packages/ai/index.ts +105 -0
  59. package/3p/pi-plannotator/packages/ai/package.json +21 -0
  60. package/3p/pi-plannotator/packages/ai/provider.ts +103 -0
  61. package/3p/pi-plannotator/packages/ai/providers/claude-agent-sdk.ts +447 -0
  62. package/3p/pi-plannotator/packages/ai/providers/codex-sdk.ts +430 -0
  63. package/3p/pi-plannotator/packages/ai/providers/command-path.ts +115 -0
  64. package/3p/pi-plannotator/packages/ai/providers/opencode-sdk.ts +546 -0
  65. package/3p/pi-plannotator/packages/ai/providers/pi-events.ts +110 -0
  66. package/3p/pi-plannotator/packages/ai/providers/pi-sdk-node.ts +425 -0
  67. package/3p/pi-plannotator/packages/ai/providers/pi-sdk.ts +469 -0
  68. package/3p/pi-plannotator/packages/ai/session-manager.ts +195 -0
  69. package/3p/pi-plannotator/packages/ai/tsconfig.json +15 -0
  70. package/3p/pi-plannotator/packages/ai/types.ts +379 -0
  71. package/3p/pi-plannotator/packages/server/agent-jobs.ts +538 -0
  72. package/3p/pi-plannotator/packages/server/agent-review-message.test.ts +135 -0
  73. package/3p/pi-plannotator/packages/server/agent-review-message.ts +243 -0
  74. package/3p/pi-plannotator/packages/server/ai-runtime.ts +108 -0
  75. package/3p/pi-plannotator/packages/server/annotate.ts +401 -0
  76. package/3p/pi-plannotator/packages/server/browser.test.ts +87 -0
  77. package/3p/pi-plannotator/packages/server/browser.ts +245 -0
  78. package/3p/pi-plannotator/packages/server/claude-review.ts +352 -0
  79. package/3p/pi-plannotator/packages/server/code-nav.ts +73 -0
  80. package/3p/pi-plannotator/packages/server/codex-review-schema.json +41 -0
  81. package/3p/pi-plannotator/packages/server/codex-review.ts +330 -0
  82. package/3p/pi-plannotator/packages/server/config.ts +8 -0
  83. package/3p/pi-plannotator/packages/server/draft.ts +1 -0
  84. package/3p/pi-plannotator/packages/server/editor-annotations.ts +76 -0
  85. package/3p/pi-plannotator/packages/server/external-annotations.test.ts +18 -0
  86. package/3p/pi-plannotator/packages/server/external-annotations.ts +207 -0
  87. package/3p/pi-plannotator/packages/server/git.ts +141 -0
  88. package/3p/pi-plannotator/packages/server/goal-setup.test.ts +55 -0
  89. package/3p/pi-plannotator/packages/server/goal-setup.ts +248 -0
  90. package/3p/pi-plannotator/packages/server/ide.ts +43 -0
  91. package/3p/pi-plannotator/packages/server/image.test.ts +63 -0
  92. package/3p/pi-plannotator/packages/server/image.ts +66 -0
  93. package/3p/pi-plannotator/packages/server/index.ts +648 -0
  94. package/3p/pi-plannotator/packages/server/integrations.test.ts +172 -0
  95. package/3p/pi-plannotator/packages/server/integrations.ts +214 -0
  96. package/3p/pi-plannotator/packages/server/jj.test.ts +69 -0
  97. package/3p/pi-plannotator/packages/server/jj.ts +86 -0
  98. package/3p/pi-plannotator/packages/server/p4.ts +417 -0
  99. package/3p/pi-plannotator/packages/server/package.json +40 -0
  100. package/3p/pi-plannotator/packages/server/path-utils.ts +18 -0
  101. package/3p/pi-plannotator/packages/server/pr.ts +146 -0
  102. package/3p/pi-plannotator/packages/server/project.test.ts +115 -0
  103. package/3p/pi-plannotator/packages/server/project.ts +45 -0
  104. package/3p/pi-plannotator/packages/server/reference-handlers.ts +404 -0
  105. package/3p/pi-plannotator/packages/server/remote.test.ts +150 -0
  106. package/3p/pi-plannotator/packages/server/remote.ts +74 -0
  107. package/3p/pi-plannotator/packages/server/repo.ts +80 -0
  108. package/3p/pi-plannotator/packages/server/resolve-file.test.ts +310 -0
  109. package/3p/pi-plannotator/packages/server/review-workspace.test.ts +1032 -0
  110. package/3p/pi-plannotator/packages/server/review-workspace.ts +48 -0
  111. package/3p/pi-plannotator/packages/server/review.ts +1318 -0
  112. package/3p/pi-plannotator/packages/server/sessions.ts +111 -0
  113. package/3p/pi-plannotator/packages/server/share-url.ts +53 -0
  114. package/3p/pi-plannotator/packages/server/shared-handlers.test.ts +43 -0
  115. package/3p/pi-plannotator/packages/server/shared-handlers.ts +177 -0
  116. package/3p/pi-plannotator/packages/server/storage.test.ts +176 -0
  117. package/3p/pi-plannotator/packages/server/storage.ts +17 -0
  118. package/3p/pi-plannotator/packages/server/tour/tour-review.test.ts +146 -0
  119. package/3p/pi-plannotator/packages/server/tour/tour-review.ts +604 -0
  120. package/3p/pi-plannotator/packages/server/tsconfig.json +15 -0
  121. package/3p/pi-plannotator/packages/server/vcs.test.ts +48 -0
  122. package/3p/pi-plannotator/packages/server/vcs.ts +80 -0
  123. package/3p/pi-plannotator/packages/shared/agent-jobs.ts +132 -0
  124. package/3p/pi-plannotator/packages/shared/agents.ts +53 -0
  125. package/3p/pi-plannotator/packages/shared/annotate-args.test.ts +386 -0
  126. package/3p/pi-plannotator/packages/shared/annotate-args.ts +107 -0
  127. package/3p/pi-plannotator/packages/shared/at-reference.test.ts +99 -0
  128. package/3p/pi-plannotator/packages/shared/at-reference.ts +52 -0
  129. package/3p/pi-plannotator/packages/shared/checklist.ts +52 -0
  130. package/3p/pi-plannotator/packages/shared/code-file.test.ts +112 -0
  131. package/3p/pi-plannotator/packages/shared/code-file.ts +41 -0
  132. package/3p/pi-plannotator/packages/shared/code-nav.test.ts +515 -0
  133. package/3p/pi-plannotator/packages/shared/code-nav.ts +436 -0
  134. package/3p/pi-plannotator/packages/shared/compress.ts +51 -0
  135. package/3p/pi-plannotator/packages/shared/config.ts +262 -0
  136. package/3p/pi-plannotator/packages/shared/crypto.test.ts +172 -0
  137. package/3p/pi-plannotator/packages/shared/crypto.ts +97 -0
  138. package/3p/pi-plannotator/packages/shared/data-dir.ts +42 -0
  139. package/3p/pi-plannotator/packages/shared/diff-paths.test.ts +30 -0
  140. package/3p/pi-plannotator/packages/shared/diff-paths.ts +137 -0
  141. package/3p/pi-plannotator/packages/shared/draft.ts +64 -0
  142. package/3p/pi-plannotator/packages/shared/external-annotation.ts +397 -0
  143. package/3p/pi-plannotator/packages/shared/extract-code-paths.test.ts +59 -0
  144. package/3p/pi-plannotator/packages/shared/extract-code-paths.ts +66 -0
  145. package/3p/pi-plannotator/packages/shared/favicon.ts +5 -0
  146. package/3p/pi-plannotator/packages/shared/feedback-templates.test.ts +65 -0
  147. package/3p/pi-plannotator/packages/shared/feedback-templates.ts +29 -0
  148. package/3p/pi-plannotator/packages/shared/goal-setup.test.ts +231 -0
  149. package/3p/pi-plannotator/packages/shared/goal-setup.ts +336 -0
  150. package/3p/pi-plannotator/packages/shared/html-to-markdown.test.ts +62 -0
  151. package/3p/pi-plannotator/packages/shared/html-to-markdown.ts +32 -0
  152. package/3p/pi-plannotator/packages/shared/improvement-hooks.test.ts +135 -0
  153. package/3p/pi-plannotator/packages/shared/improvement-hooks.ts +115 -0
  154. package/3p/pi-plannotator/packages/shared/integrations-common.ts +243 -0
  155. package/3p/pi-plannotator/packages/shared/jj-core.test.ts +236 -0
  156. package/3p/pi-plannotator/packages/shared/jj-core.ts +433 -0
  157. package/3p/pi-plannotator/packages/shared/package.json +55 -0
  158. package/3p/pi-plannotator/packages/shared/pfm-reminder.test.ts +88 -0
  159. package/3p/pi-plannotator/packages/shared/pfm-reminder.ts +80 -0
  160. package/3p/pi-plannotator/packages/shared/pr-github.ts +661 -0
  161. package/3p/pi-plannotator/packages/shared/pr-gitlab.test.ts +202 -0
  162. package/3p/pi-plannotator/packages/shared/pr-gitlab.ts +620 -0
  163. package/3p/pi-plannotator/packages/shared/pr-provider.test.ts +266 -0
  164. package/3p/pi-plannotator/packages/shared/pr-provider.ts +123 -0
  165. package/3p/pi-plannotator/packages/shared/pr-stack.test.ts +104 -0
  166. package/3p/pi-plannotator/packages/shared/pr-stack.ts +194 -0
  167. package/3p/pi-plannotator/packages/shared/pr-types.ts +326 -0
  168. package/3p/pi-plannotator/packages/shared/project.ts +71 -0
  169. package/3p/pi-plannotator/packages/shared/prompts-integration.test.ts +421 -0
  170. package/3p/pi-plannotator/packages/shared/prompts.test.ts +504 -0
  171. package/3p/pi-plannotator/packages/shared/prompts.ts +247 -0
  172. package/3p/pi-plannotator/packages/shared/reference-common.ts +87 -0
  173. package/3p/pi-plannotator/packages/shared/repo.ts +71 -0
  174. package/3p/pi-plannotator/packages/shared/resolve-file.test.ts +113 -0
  175. package/3p/pi-plannotator/packages/shared/resolve-file.ts +509 -0
  176. package/3p/pi-plannotator/packages/shared/review-args.test.ts +64 -0
  177. package/3p/pi-plannotator/packages/shared/review-args.ts +85 -0
  178. package/3p/pi-plannotator/packages/shared/review-core.test.ts +286 -0
  179. package/3p/pi-plannotator/packages/shared/review-core.ts +895 -0
  180. package/3p/pi-plannotator/packages/shared/review-workspace-node.ts +230 -0
  181. package/3p/pi-plannotator/packages/shared/review-workspace.ts +436 -0
  182. package/3p/pi-plannotator/packages/shared/semantic-diff-types.ts +76 -0
  183. package/3p/pi-plannotator/packages/shared/semantic-diff.test.ts +322 -0
  184. package/3p/pi-plannotator/packages/shared/semantic-diff.ts +520 -0
  185. package/3p/pi-plannotator/packages/shared/storage.ts +378 -0
  186. package/3p/pi-plannotator/packages/shared/tour.ts +61 -0
  187. package/3p/pi-plannotator/packages/shared/tsconfig.json +15 -0
  188. package/3p/pi-plannotator/packages/shared/types.ts +29 -0
  189. package/3p/pi-plannotator/packages/shared/url-to-markdown.test.ts +177 -0
  190. package/3p/pi-plannotator/packages/shared/url-to-markdown.ts +351 -0
  191. package/3p/pi-plannotator/packages/shared/vcs-core.test.ts +332 -0
  192. package/3p/pi-plannotator/packages/shared/vcs-core.ts +482 -0
  193. package/3p/pi-plannotator/packages/shared/worktree-pool.test.ts +162 -0
  194. package/3p/pi-plannotator/packages/shared/worktree-pool.ts +103 -0
  195. package/3p/pi-plannotator/packages/shared/worktree.ts +119 -0
  196. package/3p/pi-subagents/package.json +50 -0
  197. package/3p/pi-subagents/src/agent-manager.ts +413 -0
  198. package/3p/pi-subagents/src/agent-runner.ts +502 -0
  199. package/3p/pi-subagents/src/agent-types.ts +248 -0
  200. package/3p/pi-subagents/src/context.ts +58 -0
  201. package/3p/pi-subagents/src/cross-extension-rpc.ts +135 -0
  202. package/3p/pi-subagents/src/custom-agents.ts +137 -0
  203. package/3p/pi-subagents/src/default-agents.ts +144 -0
  204. package/3p/pi-subagents/src/env.ts +33 -0
  205. package/3p/pi-subagents/src/group-join.ts +141 -0
  206. package/3p/pi-subagents/src/index.ts +1811 -0
  207. package/3p/pi-subagents/src/invocation-config.ts +40 -0
  208. package/3p/pi-subagents/src/memory.ts +165 -0
  209. package/3p/pi-subagents/src/model-resolver.ts +81 -0
  210. package/3p/pi-subagents/src/output-file.ts +77 -0
  211. package/3p/pi-subagents/src/prompts.ts +85 -0
  212. package/3p/pi-subagents/src/skill-loader.ts +79 -0
  213. package/3p/pi-subagents/src/types.ts +111 -0
  214. package/3p/pi-subagents/src/ui/agent-widget.ts +496 -0
  215. package/3p/pi-subagents/src/ui/conversation-viewer.ts +250 -0
  216. package/3p/pi-subagents/src/worktree.ts +162 -0
  217. package/3p/pi-tasks/package.json +52 -0
  218. package/3p/pi-tasks/src/auto-clear.ts +91 -0
  219. package/3p/pi-tasks/src/index.ts +1150 -0
  220. package/3p/pi-tasks/src/process-tracker.ts +140 -0
  221. package/3p/pi-tasks/src/reminder-cadence.ts +90 -0
  222. package/3p/pi-tasks/src/task-store.ts +324 -0
  223. package/3p/pi-tasks/src/tasks-config.ts +27 -0
  224. package/3p/pi-tasks/src/types.ts +40 -0
  225. package/3p/pi-tasks/src/ui/settings-menu.ts +152 -0
  226. package/3p/pi-tasks/src/ui/task-widget.ts +296 -0
  227. package/AGENTS.md +28 -0
  228. package/LICENSE +201 -0
  229. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +77 -0
  230. package/extensions/orchestrator/agents/code-reviewer.ts +89 -0
  231. package/extensions/orchestrator/agents/explore.ts +33 -0
  232. package/extensions/orchestrator/agents/librarian.ts +43 -0
  233. package/extensions/orchestrator/agents/plan-reviewer.ts +75 -0
  234. package/extensions/orchestrator/agents/planner.ts +61 -0
  235. package/extensions/orchestrator/agents/registry.ts +156 -0
  236. package/extensions/orchestrator/agents/task.ts +50 -0
  237. package/extensions/orchestrator/agents/tool-routing.ts +84 -0
  238. package/extensions/orchestrator/ast-search.ts +85 -0
  239. package/extensions/orchestrator/cbm.ts +330 -0
  240. package/extensions/orchestrator/command-handlers.test.ts +163 -0
  241. package/extensions/orchestrator/command-handlers.ts +116 -0
  242. package/extensions/orchestrator/commands.test.ts +81 -0
  243. package/extensions/orchestrator/commands.ts +75 -0
  244. package/extensions/orchestrator/config.test.ts +221 -0
  245. package/extensions/orchestrator/config.ts +230 -0
  246. package/extensions/orchestrator/context.test.ts +293 -0
  247. package/extensions/orchestrator/context.ts +206 -0
  248. package/extensions/orchestrator/event-handlers.test.ts +190 -0
  249. package/extensions/orchestrator/event-handlers.ts +1416 -0
  250. package/extensions/orchestrator/exa.ts +104 -0
  251. package/extensions/orchestrator/flant-infra.ts +486 -0
  252. package/extensions/orchestrator/index.ts +80 -0
  253. package/extensions/orchestrator/integration.test.ts +1214 -0
  254. package/extensions/orchestrator/orchestrator.test.ts +252 -0
  255. package/extensions/orchestrator/orchestrator.ts +538 -0
  256. package/extensions/orchestrator/phases/brainstorm.test.ts +15 -0
  257. package/extensions/orchestrator/phases/brainstorm.ts +273 -0
  258. package/extensions/orchestrator/phases/implementation.ts +40 -0
  259. package/extensions/orchestrator/phases/machine.test.ts +293 -0
  260. package/extensions/orchestrator/phases/machine.ts +209 -0
  261. package/extensions/orchestrator/phases/planning.ts +255 -0
  262. package/extensions/orchestrator/phases/review.ts +193 -0
  263. package/extensions/orchestrator/plannotator.ts +56 -0
  264. package/extensions/orchestrator/pp-menu.ts +866 -0
  265. package/extensions/orchestrator/state.test.ts +343 -0
  266. package/extensions/orchestrator/state.ts +237 -0
  267. package/extensions/orchestrator/validate-artifacts.test.ts +88 -0
  268. package/extensions/orchestrator/validate-artifacts.ts +272 -0
  269. package/extensions/orchestrator/vendor.d.ts +26 -0
  270. package/package.json +73 -0
  271. package/scripts/postinstall.sh +18 -0
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Worktree Pool — manages a set of per-PR git worktrees for a review session.
3
+ *
4
+ * Runtime-agnostic. Uses ReviewGitRuntime for all git operations.
5
+ * Both Bun and Pi servers import this module (Pi via vendor.sh).
6
+ *
7
+ * Each PR visited during a session gets its own worktree, created on first
8
+ * access and cached for the session lifetime. Agents run in their PR's
9
+ * worktree undisturbed by PR switches.
10
+ */
11
+
12
+ import { join } from "node:path";
13
+ import type { ReviewGitRuntime } from "./review-core";
14
+ import type { PRMetadata } from "./pr-types";
15
+ import { createWorktree, removeWorktree, fetchRef, ensureObjectAvailable } from "./worktree";
16
+
17
+ export interface PoolEntry {
18
+ path: string;
19
+ prUrl: string;
20
+ number: number;
21
+ ready: boolean;
22
+ }
23
+
24
+ export interface WorktreePoolConfig {
25
+ sessionDir: string;
26
+ repoDir: string;
27
+ isSameRepo: boolean;
28
+ }
29
+
30
+ export interface WorktreePool {
31
+ get(prUrl: string): PoolEntry | undefined;
32
+ has(prUrl: string): boolean;
33
+ resolve(prUrl: string): string | undefined;
34
+ ensure(runtime: ReviewGitRuntime, metadata: PRMetadata): Promise<PoolEntry>;
35
+ entries(): IterableIterator<PoolEntry>;
36
+ cleanup(runtime: ReviewGitRuntime): Promise<void>;
37
+ }
38
+
39
+ export function createWorktreePool(config: WorktreePoolConfig, initial?: PoolEntry): WorktreePool {
40
+ const pool = new Map<string, PoolEntry>();
41
+ const pending = new Map<string, Promise<PoolEntry>>();
42
+ if (initial) pool.set(initial.prUrl, initial);
43
+
44
+ return {
45
+ get(prUrl) { return pool.get(prUrl); },
46
+ has(prUrl) { return pool.has(prUrl); },
47
+ resolve(prUrl) {
48
+ const entry = pool.get(prUrl);
49
+ return entry?.ready ? entry.path : undefined;
50
+ },
51
+
52
+ async ensure(runtime, metadata) {
53
+ const existing = pool.get(metadata.url);
54
+ if (existing?.ready) return existing;
55
+
56
+ const inflight = pending.get(metadata.url);
57
+ if (inflight) return inflight;
58
+
59
+ if (!config.isSameRepo) {
60
+ throw new Error("Cross-repo pool cannot create worktrees for other PRs");
61
+ }
62
+
63
+ const promise = (async (): Promise<PoolEntry> => {
64
+ const number = metadata.platform === "github" ? metadata.number : metadata.iid;
65
+ const worktreePath = join(config.sessionDir, "pool", `pr-${number}`);
66
+ const refSpec = metadata.platform === "github"
67
+ ? `refs/pull/${number}/head`
68
+ : `refs/merge-requests/${number}/head`;
69
+
70
+ await fetchRef(runtime, metadata.baseBranch, { cwd: config.repoDir });
71
+ await ensureObjectAvailable(runtime, metadata.baseSha, { cwd: config.repoDir });
72
+ await fetchRef(runtime, refSpec, { cwd: config.repoDir });
73
+
74
+ await createWorktree(runtime, {
75
+ ref: "FETCH_HEAD",
76
+ path: worktreePath,
77
+ detach: true,
78
+ cwd: config.repoDir,
79
+ });
80
+
81
+ const entry: PoolEntry = { path: worktreePath, prUrl: metadata.url, number, ready: true };
82
+ pool.set(metadata.url, entry);
83
+ return entry;
84
+ })();
85
+
86
+ pending.set(metadata.url, promise);
87
+ try {
88
+ return await promise;
89
+ } finally {
90
+ pending.delete(metadata.url);
91
+ }
92
+ },
93
+
94
+ entries() { return pool.values(); },
95
+
96
+ async cleanup(runtime) {
97
+ for (const entry of pool.values()) {
98
+ await removeWorktree(runtime, entry.path, { force: true, cwd: config.repoDir });
99
+ }
100
+ pool.clear();
101
+ },
102
+ };
103
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Worktree — runtime-agnostic git worktree primitives.
3
+ *
4
+ * Uses ReviewGitRuntime so both Bun and Node runtimes can use the same logic.
5
+ * Lives in packages/shared/ and gets vendored to Pi via vendor.sh.
6
+ *
7
+ * Designed as composable primitives, not tied to any specific use case.
8
+ * PR local checkout, agent sandboxes, parallel sessions — all compose from these.
9
+ */
10
+
11
+ import type { ReviewGitRuntime } from "./review-core";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Types
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export interface CreateWorktreeOptions {
18
+ /** Git ref to check out (branch name, SHA, FETCH_HEAD, etc.) */
19
+ ref: string;
20
+ /** Absolute path where the worktree will be created. */
21
+ path: string;
22
+ /** Create in detached HEAD mode (no branch). Default: false. */
23
+ detach?: boolean;
24
+ /** CWD of the source repository. Defaults to process.cwd(). */
25
+ cwd?: string;
26
+ }
27
+
28
+ export interface RemoveWorktreeOptions {
29
+ /** Force removal even if the worktree has modifications. Default: false. */
30
+ force?: boolean;
31
+ /** CWD of the source repository. Required if the worktree was created from a different cwd. */
32
+ cwd?: string;
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Primitives
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /**
40
+ * Fetch a ref from origin.
41
+ * Runs: `git fetch origin <ref>`
42
+ * Throws on failure.
43
+ */
44
+ export async function fetchRef(
45
+ runtime: ReviewGitRuntime,
46
+ ref: string,
47
+ options?: { cwd?: string },
48
+ ): Promise<void> {
49
+ const result = await runtime.runGit(["fetch", "origin", "--", ref], { cwd: options?.cwd });
50
+ if (result.exitCode !== 0) {
51
+ throw new Error(`git fetch origin ${ref} failed: ${result.stderr.trim() || `exit code ${result.exitCode}`}`);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Ensure a git object (commit SHA) is available locally.
57
+ * Checks with `git cat-file -t`, fetches from origin if missing.
58
+ * Returns true if the object is available after the attempt.
59
+ */
60
+ export async function ensureObjectAvailable(
61
+ runtime: ReviewGitRuntime,
62
+ sha: string,
63
+ options?: { cwd?: string },
64
+ ): Promise<boolean> {
65
+ const check = await runtime.runGit(["cat-file", "-t", sha], { cwd: options?.cwd });
66
+ if (check.exitCode === 0) return true;
67
+
68
+ // Object missing locally — try fetching it
69
+ const fetch = await runtime.runGit(["fetch", "origin", "--", sha], { cwd: options?.cwd });
70
+ if (fetch.exitCode !== 0) return false;
71
+
72
+ // Verify it's now available
73
+ const recheck = await runtime.runGit(["cat-file", "-t", sha], { cwd: options?.cwd });
74
+ return recheck.exitCode === 0;
75
+ }
76
+
77
+ /**
78
+ * Create a git worktree.
79
+ * Runs: `git worktree add [--detach] <path> <ref>`
80
+ * Throws on failure with a descriptive error.
81
+ */
82
+ export async function createWorktree(
83
+ runtime: ReviewGitRuntime,
84
+ options: CreateWorktreeOptions,
85
+ ): Promise<{ worktreePath: string }> {
86
+ const args = ["worktree", "add"];
87
+ if (options.detach) args.push("--detach");
88
+ args.push(options.path, options.ref);
89
+
90
+ const result = await runtime.runGit(args, { cwd: options.cwd });
91
+ if (result.exitCode !== 0) {
92
+ throw new Error(`git worktree add failed: ${result.stderr.trim() || `exit code ${result.exitCode}`}`);
93
+ }
94
+
95
+ return { worktreePath: options.path };
96
+ }
97
+
98
+ /**
99
+ * Remove a git worktree. Best-effort — logs errors but does not throw.
100
+ * Runs: `git worktree remove [--force] <path>`
101
+ */
102
+ export async function removeWorktree(
103
+ runtime: ReviewGitRuntime,
104
+ worktreePath: string,
105
+ options?: RemoveWorktreeOptions,
106
+ ): Promise<void> {
107
+ const args = ["worktree", "remove"];
108
+ if (options?.force) args.push("--force");
109
+ args.push(worktreePath);
110
+
111
+ try {
112
+ const result = await runtime.runGit(args, { cwd: options?.cwd });
113
+ if (result.exitCode !== 0) {
114
+ console.error(`Warning: git worktree remove failed for ${worktreePath}: ${result.stderr.trim()}`);
115
+ }
116
+ } catch (err) {
117
+ console.error(`Warning: worktree cleanup error: ${err instanceof Error ? err.message : String(err)}`);
118
+ }
119
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@tintinweb/pi-subagents",
3
+ "version": "0.5.2",
4
+ "description": "A pi extension extension that brings smart Claude Code-style autonomous sub-agents to pi.",
5
+ "author": "tintinweb",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/tintinweb/pi-subagents.git"
10
+ },
11
+ "homepage": "https://github.com/tintinweb/pi-subagents#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/tintinweb/pi-subagents/issues"
14
+ },
15
+ "keywords": [
16
+ "pi",
17
+ "pi-extension",
18
+ "subagent",
19
+ "agent",
20
+ "autonomous"
21
+ ],
22
+ "dependencies": {
23
+ "@earendil-works/pi-ai": "^0.75.5",
24
+ "@earendil-works/pi-coding-agent": "^0.75.5",
25
+ "@earendil-works/pi-tui": "^0.75.5",
26
+ "@sinclair/typebox": "latest"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc",
30
+ "prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build",
31
+ "test": "vitest run",
32
+ "test:watch": "vitest",
33
+ "typecheck": "tsc --noEmit",
34
+ "lint": "biome check src/ test/",
35
+ "lint:fix": "biome check --fix src/ test/"
36
+ },
37
+ "devDependencies": {
38
+ "@biomejs/biome": "^2.3.5",
39
+ "@types/node": "^25.5.0",
40
+ "typescript": "^5.0.0",
41
+ "vitest": "^4.0.18"
42
+ },
43
+ "pi": {
44
+ "extensions": [
45
+ "./src/index.ts"
46
+ ],
47
+ "video": "https://github.com/tintinweb/pi-subagents/raw/master/media/demo.mp4",
48
+ "image": "https://github.com/tintinweb/pi-subagents/raw/master/media/screenshot.png"
49
+ }
50
+ }
@@ -0,0 +1,413 @@
1
+ /**
2
+ * agent-manager.ts — Tracks agents, background execution, resume support.
3
+ *
4
+ * Background agents are subject to a configurable concurrency limit (default: 4).
5
+ * Excess agents are queued and auto-started as running agents complete.
6
+ * Foreground agents bypass the queue (they block the parent anyway).
7
+ */
8
+
9
+ import { randomUUID } from "node:crypto";
10
+ import type { Model } from "@earendil-works/pi-ai";
11
+ import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
12
+ import { resumeAgent, runAgent, type ToolActivity } from "./agent-runner.js";
13
+ import type { AgentRecord, IsolationMode, SubagentType, ThinkingLevel } from "./types.js";
14
+ import { cleanupWorktree, createWorktree, pruneWorktrees, } from "./worktree.js";
15
+
16
+ export type OnAgentComplete = (record: AgentRecord) => void;
17
+ export type OnAgentStart = (record: AgentRecord) => void;
18
+
19
+ /** Default max concurrent background agents. */
20
+ const DEFAULT_MAX_CONCURRENT = 4;
21
+
22
+ interface SpawnArgs {
23
+ pi: ExtensionAPI;
24
+ ctx: ExtensionContext;
25
+ type: SubagentType;
26
+ prompt: string;
27
+ options: SpawnOptions;
28
+ }
29
+
30
+ interface SpawnOptions {
31
+ description: string;
32
+ model?: Model<any>;
33
+ maxTurns?: number;
34
+ isolated?: boolean;
35
+ inheritContext?: boolean;
36
+ thinkingLevel?: ThinkingLevel;
37
+ isBackground?: boolean;
38
+ /** Isolation mode — "worktree" creates a temp git worktree for the agent. */
39
+ isolation?: IsolationMode;
40
+ /** Called on tool start/end with activity info (for streaming progress to UI). */
41
+ onToolActivity?: (activity: ToolActivity) => void;
42
+ /** Called on streaming text deltas from the assistant response. */
43
+ onTextDelta?: (delta: string, fullText: string) => void;
44
+ /** Called when the agent session is created (for accessing session stats). */
45
+ onSessionCreated?: (session: AgentSession) => void;
46
+ /** Called at the end of each agentic turn with the cumulative count. */
47
+ onTurnEnd?: (turnCount: number) => void;
48
+ validateCompletion?: () => string | undefined;
49
+ maxValidationRetries?: number;
50
+ }
51
+
52
+ export class AgentManager {
53
+ private agents = new Map<string, AgentRecord>();
54
+ private cleanupInterval: ReturnType<typeof setInterval>;
55
+ private onComplete?: OnAgentComplete;
56
+ private onStart?: OnAgentStart;
57
+ private maxConcurrent: number;
58
+
59
+ /** Queue of background agents waiting to start. */
60
+ private queue: { id: string; args: SpawnArgs }[] = [];
61
+ /** Number of currently running background agents. */
62
+ private runningBackground = 0;
63
+
64
+ constructor(onComplete?: OnAgentComplete, maxConcurrent = DEFAULT_MAX_CONCURRENT, onStart?: OnAgentStart) {
65
+ this.onComplete = onComplete;
66
+ this.onStart = onStart;
67
+ this.maxConcurrent = maxConcurrent;
68
+ // Cleanup completed agents after 10 minutes (but keep sessions for resume)
69
+ this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
70
+ }
71
+
72
+ /** Update the max concurrent background agents limit. */
73
+ setMaxConcurrent(n: number) {
74
+ this.maxConcurrent = Math.max(1, n);
75
+ // Start queued agents if the new limit allows
76
+ this.drainQueue();
77
+ }
78
+
79
+ getMaxConcurrent(): number {
80
+ return this.maxConcurrent;
81
+ }
82
+
83
+ /**
84
+ * Spawn an agent and return its ID immediately (for background use).
85
+ * If the concurrency limit is reached, the agent is queued.
86
+ */
87
+ spawn(
88
+ pi: ExtensionAPI,
89
+ ctx: ExtensionContext,
90
+ type: SubagentType,
91
+ prompt: string,
92
+ options: SpawnOptions,
93
+ ): string {
94
+ const id = randomUUID().slice(0, 17);
95
+ const abortController = new AbortController();
96
+ const record: AgentRecord = {
97
+ id,
98
+ type,
99
+ description: options.description,
100
+ status: options.isBackground ? "queued" : "running",
101
+ toolUses: 0,
102
+ startedAt: Date.now(),
103
+ abortController,
104
+ };
105
+ this.agents.set(id, record);
106
+
107
+ const args: SpawnArgs = { pi, ctx, type, prompt, options };
108
+
109
+ if (options.isBackground && this.runningBackground >= this.maxConcurrent) {
110
+ // Queue it — will be started when a running agent completes
111
+ this.queue.push({ id, args });
112
+ return id;
113
+ }
114
+
115
+ this.startAgent(id, record, args);
116
+ return id;
117
+ }
118
+
119
+ /** Actually start an agent (called immediately or from queue drain). */
120
+ private startAgent(id: string, record: AgentRecord, { pi, ctx, type, prompt, options }: SpawnArgs) {
121
+ record.status = "running";
122
+ record.startedAt = Date.now();
123
+ if (options.isBackground) this.runningBackground++;
124
+ this.onStart?.(record);
125
+
126
+ // Worktree isolation: create a temporary git worktree if requested
127
+ let worktreeCwd: string | undefined;
128
+ let worktreeWarning = "";
129
+ if (options.isolation === "worktree") {
130
+ const wt = createWorktree(ctx.cwd, id);
131
+ if (wt) {
132
+ record.worktree = wt;
133
+ worktreeCwd = wt.path;
134
+ } else {
135
+ worktreeWarning = "\n\n[WARNING: Worktree isolation was requested but failed (not a git repo, or no commits yet). Running in the main working directory instead.]";
136
+ }
137
+ }
138
+
139
+ // Prepend worktree warning to prompt if isolation failed
140
+ const effectivePrompt = worktreeWarning ? worktreeWarning + "\n\n" + prompt : prompt;
141
+
142
+ const promise = runAgent(ctx, type, effectivePrompt, {
143
+ pi,
144
+ model: options.model,
145
+ maxTurns: options.maxTurns,
146
+ isolated: options.isolated,
147
+ inheritContext: options.inheritContext,
148
+ thinkingLevel: options.thinkingLevel,
149
+ cwd: worktreeCwd,
150
+ signal: record.abortController!.signal,
151
+ onToolActivity: (activity) => {
152
+ if (activity.type === "end") record.toolUses++;
153
+ options.onToolActivity?.(activity);
154
+ },
155
+ onTurnEnd: options.onTurnEnd,
156
+ onTextDelta: options.onTextDelta,
157
+ validateCompletion: options.validateCompletion,
158
+ maxValidationRetries: options.maxValidationRetries,
159
+ onSessionCreated: (session) => {
160
+ record.session = session;
161
+ // Flush any steers that arrived before the session was ready
162
+ if (record.pendingSteers?.length) {
163
+ for (const msg of record.pendingSteers) {
164
+ session.steer(msg).catch(() => {});
165
+ }
166
+ record.pendingSteers = undefined;
167
+ }
168
+ options.onSessionCreated?.(session);
169
+ },
170
+ })
171
+ .then(({ responseText, session, aborted, steered }) => {
172
+ // Don't overwrite status if externally stopped via abort()
173
+ if (record.status !== "stopped") {
174
+ record.status = aborted ? "aborted" : steered ? "steered" : "completed";
175
+ }
176
+ record.result = responseText;
177
+ record.session = session;
178
+ record.completedAt ??= Date.now();
179
+
180
+ // Final flush of streaming output file
181
+ if (record.outputCleanup) {
182
+ try { record.outputCleanup(); } catch { /* ignore */ }
183
+ record.outputCleanup = undefined;
184
+ }
185
+
186
+ // Clean up worktree if used
187
+ if (record.worktree) {
188
+ const wtResult = cleanupWorktree(ctx.cwd, record.worktree, options.description);
189
+ record.worktreeResult = wtResult;
190
+ if (wtResult.hasChanges && wtResult.branch) {
191
+ record.result = (record.result ?? "") +
192
+ `\n\n---\nChanges saved to branch \`${wtResult.branch}\`. Merge with: \`git merge ${wtResult.branch}\``;
193
+ }
194
+ }
195
+
196
+ if (options.isBackground) {
197
+ this.runningBackground--;
198
+ this.onComplete?.(record);
199
+ this.drainQueue();
200
+ }
201
+ return responseText;
202
+ })
203
+ .catch((err) => {
204
+ // Don't overwrite status if externally stopped via abort()
205
+ if (record.status !== "stopped") {
206
+ record.status = "error";
207
+ }
208
+ record.error = err instanceof Error ? err.message : String(err);
209
+ record.completedAt ??= Date.now();
210
+
211
+ // Final flush of streaming output file on error
212
+ if (record.outputCleanup) {
213
+ try { record.outputCleanup(); } catch { /* ignore */ }
214
+ record.outputCleanup = undefined;
215
+ }
216
+
217
+ // Best-effort worktree cleanup on error
218
+ if (record.worktree) {
219
+ try {
220
+ const wtResult = cleanupWorktree(ctx.cwd, record.worktree, options.description);
221
+ record.worktreeResult = wtResult;
222
+ } catch { /* ignore cleanup errors */ }
223
+ }
224
+
225
+ if (options.isBackground) {
226
+ this.runningBackground--;
227
+ this.onComplete?.(record);
228
+ this.drainQueue();
229
+ }
230
+ return "";
231
+ });
232
+
233
+ record.promise = promise;
234
+ }
235
+
236
+ /** Start queued agents up to the concurrency limit. */
237
+ private drainQueue() {
238
+ while (this.queue.length > 0 && this.runningBackground < this.maxConcurrent) {
239
+ const next = this.queue.shift()!;
240
+ const record = this.agents.get(next.id);
241
+ if (!record || record.status !== "queued") continue;
242
+ this.startAgent(next.id, record, next.args);
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Spawn an agent and wait for completion (foreground use).
248
+ * Foreground agents bypass the concurrency queue.
249
+ */
250
+ async spawnAndWait(
251
+ pi: ExtensionAPI,
252
+ ctx: ExtensionContext,
253
+ type: SubagentType,
254
+ prompt: string,
255
+ options: Omit<SpawnOptions, "isBackground">,
256
+ ): Promise<AgentRecord> {
257
+ const id = this.spawn(pi, ctx, type, prompt, { ...options, isBackground: false });
258
+ const record = this.agents.get(id)!;
259
+ await record.promise;
260
+ return record;
261
+ }
262
+
263
+ /**
264
+ * Resume an existing agent session with a new prompt.
265
+ */
266
+ async resume(
267
+ id: string,
268
+ prompt: string,
269
+ signal?: AbortSignal,
270
+ ): Promise<AgentRecord | undefined> {
271
+ const record = this.agents.get(id);
272
+ if (!record?.session) return undefined;
273
+
274
+ record.status = "running";
275
+ record.startedAt = Date.now();
276
+ record.completedAt = undefined;
277
+ record.result = undefined;
278
+ record.error = undefined;
279
+
280
+ try {
281
+ const responseText = await resumeAgent(record.session, prompt, {
282
+ onToolActivity: (activity) => {
283
+ if (activity.type === "end") record.toolUses++;
284
+ },
285
+ signal,
286
+ });
287
+ record.status = "completed";
288
+ record.result = responseText;
289
+ record.completedAt = Date.now();
290
+ } catch (err) {
291
+ record.status = "error";
292
+ record.error = err instanceof Error ? err.message : String(err);
293
+ record.completedAt = Date.now();
294
+ }
295
+
296
+ return record;
297
+ }
298
+
299
+ getRecord(id: string): AgentRecord | undefined {
300
+ return this.agents.get(id);
301
+ }
302
+
303
+ listAgents(): AgentRecord[] {
304
+ return [...this.agents.values()].sort(
305
+ (a, b) => b.startedAt - a.startedAt,
306
+ );
307
+ }
308
+
309
+ abort(id: string): boolean {
310
+ const record = this.agents.get(id);
311
+ if (!record) return false;
312
+
313
+ // Remove from queue if queued
314
+ if (record.status === "queued") {
315
+ this.queue = this.queue.filter(q => q.id !== id);
316
+ record.status = "stopped";
317
+ record.completedAt = Date.now();
318
+ return true;
319
+ }
320
+
321
+ if (record.status !== "running") return false;
322
+ record.abortController?.abort();
323
+ record.status = "stopped";
324
+ record.completedAt = Date.now();
325
+ return true;
326
+ }
327
+
328
+ /** Dispose a record's session and remove it from the map. */
329
+ private removeRecord(id: string, record: AgentRecord): void {
330
+ record.session?.dispose?.();
331
+ record.session = undefined;
332
+ this.agents.delete(id);
333
+ }
334
+
335
+ private cleanup() {
336
+ const cutoff = Date.now() - 10 * 60_000;
337
+ for (const [id, record] of this.agents) {
338
+ if (record.status === "running" || record.status === "queued") continue;
339
+ if ((record.completedAt ?? 0) >= cutoff) continue;
340
+ this.removeRecord(id, record);
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Remove all completed/stopped/errored records immediately.
346
+ * Called on session start/switch so tasks from a prior session don't persist.
347
+ */
348
+ clearCompleted(): void {
349
+ for (const [id, record] of this.agents) {
350
+ if (record.status === "running" || record.status === "queued") continue;
351
+ this.removeRecord(id, record);
352
+ }
353
+ }
354
+
355
+ /** Whether any agents are still running or queued. */
356
+ hasRunning(): boolean {
357
+ return [...this.agents.values()].some(
358
+ r => r.status === "running" || r.status === "queued",
359
+ );
360
+ }
361
+
362
+ /** Abort all running and queued agents immediately. */
363
+ abortAll(): number {
364
+ let count = 0;
365
+ // Clear queued agents first
366
+ for (const queued of this.queue) {
367
+ const record = this.agents.get(queued.id);
368
+ if (record) {
369
+ record.status = "stopped";
370
+ record.completedAt = Date.now();
371
+ count++;
372
+ }
373
+ }
374
+ this.queue = [];
375
+ // Abort running agents
376
+ for (const record of this.agents.values()) {
377
+ if (record.status === "running") {
378
+ record.abortController?.abort();
379
+ record.status = "stopped";
380
+ record.completedAt = Date.now();
381
+ count++;
382
+ }
383
+ }
384
+ return count;
385
+ }
386
+
387
+ /** Wait for all running and queued agents to complete (including queued ones). */
388
+ async waitForAll(): Promise<void> {
389
+ // Loop because drainQueue respects the concurrency limit — as running
390
+ // agents finish they start queued ones, which need awaiting too.
391
+ while (true) {
392
+ this.drainQueue();
393
+ const pending = [...this.agents.values()]
394
+ .filter(r => r.status === "running" || r.status === "queued")
395
+ .map(r => r.promise)
396
+ .filter(Boolean);
397
+ if (pending.length === 0) break;
398
+ await Promise.allSettled(pending);
399
+ }
400
+ }
401
+
402
+ dispose() {
403
+ clearInterval(this.cleanupInterval);
404
+ // Clear queue
405
+ this.queue = [];
406
+ for (const record of this.agents.values()) {
407
+ record.session?.dispose();
408
+ }
409
+ this.agents.clear();
410
+ // Prune any orphaned git worktrees (crash recovery)
411
+ try { pruneWorktrees(process.cwd()); } catch { /* ignore */ }
412
+ }
413
+ }