@rexvane/piweb 0.3.16 → 0.3.17

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 (288) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +8 -11
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +6 -6
  5. package/.next/react-loadable-manifest.json +4 -16
  6. package/.next/required-server-files.js +1 -3
  7. package/.next/required-server-files.json +1 -3
  8. package/.next/routes-manifest.json +0 -20
  9. package/.next/server/app/_global-error/page.js +2 -2
  10. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_global-error.html +1 -1
  12. package/.next/server/app/_global-error.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page.js +1 -1
  17. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/_not-found.html +1 -1
  19. package/.next/server/app/_not-found.rsc +4 -7
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +4 -7
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -5
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -5
  23. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  24. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  25. package/.next/server/app/api/agent/[id]/route.js +1 -1
  26. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  27. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  28. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  29. package/.next/server/app/api/agent/new/route.js +1 -1
  30. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  31. package/.next/server/app/api/config/route.js +1 -1
  32. package/.next/server/app/api/config/route.js.nft.json +1 -1
  33. package/.next/server/app/api/files/route.js +1 -1
  34. package/.next/server/app/api/files/route.js.nft.json +1 -1
  35. package/.next/server/app/api/git/route.js +2 -2
  36. package/.next/server/app/api/git/route.js.nft.json +1 -1
  37. package/.next/server/app/api/growth/route.js +1 -1
  38. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  39. package/.next/server/app/api/health/route.js +1 -1
  40. package/.next/server/app/api/models/route.js +1 -1
  41. package/.next/server/app/api/models/route.js.nft.json +1 -1
  42. package/.next/server/app/api/pi-settings/route.js +1 -1
  43. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  44. package/.next/server/app/api/plugins/route.js +1 -1
  45. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  46. package/.next/server/app/api/prompts/route.js +1 -1
  47. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  48. package/.next/server/app/api/security/route.js +1 -1
  49. package/.next/server/app/api/security/route.js.nft.json +1 -1
  50. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  51. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  52. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  53. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  54. package/.next/server/app/api/sessions/import/route.js +3 -3
  55. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  56. package/.next/server/app/api/sessions/route.js +1 -1
  57. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  58. package/.next/server/app/api/skills/route.js +2 -2
  59. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  60. package/.next/server/app/api/update/route.js +2 -2
  61. package/.next/server/app/api/update/route.js.nft.json +1 -1
  62. package/.next/server/app/api/version/route.js +1 -1
  63. package/.next/server/app/api/version/route.js.nft.json +1 -1
  64. package/.next/server/app/api/web-auth/route.js +1 -1
  65. package/.next/server/app/api/workspaces/route.js +1 -1
  66. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  67. package/.next/server/app/icon.svg/route.js +1 -1
  68. package/.next/server/app/index.html +1 -1
  69. package/.next/server/app/index.rsc +6 -9
  70. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -7
  71. package/.next/server/app/index.segments/_full.segment.rsc +6 -9
  72. package/.next/server/app/index.segments/_tree.segment.rsc +2 -5
  73. package/.next/server/app/login/page.js +1 -1
  74. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app/login.html +1 -1
  76. package/.next/server/app/login.rsc +5 -8
  77. package/.next/server/app/login.segments/_full.segment.rsc +5 -8
  78. package/.next/server/app/login.segments/_tree.segment.rsc +2 -5
  79. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -6
  80. package/.next/server/app/page.js +1 -1
  81. package/.next/server/app/page_client-reference-manifest.js +1 -1
  82. package/.next/server/app-paths-manifest.json +8 -11
  83. package/.next/server/chunks/496.js +1 -1
  84. package/.next/server/chunks/532.js +1 -1
  85. package/.next/server/chunks/686.js +4 -8
  86. package/.next/server/middleware-build-manifest.js +1 -1
  87. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  88. package/.next/server/pages/404.html +1 -1
  89. package/.next/server/pages/500.html +1 -1
  90. package/.next/server/server-reference-manifest.json +1 -1
  91. package/.next/static/chunks/2608.ec8ff1f5767c9056.js +1 -0
  92. package/.next/static/chunks/2716.93be8ad5c970c516.js +1 -0
  93. package/.next/static/chunks/4632.112d2e893d48d188.js +1 -0
  94. package/.next/static/chunks/4673.c1fb1580efc6ace7.js +1 -0
  95. package/.next/static/chunks/4686-f4a037d609cea3f2.js +1 -0
  96. package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +1 -0
  97. package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +1 -0
  98. package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +1 -0
  99. package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +1 -0
  100. package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +1 -0
  101. package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +1 -0
  102. package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +1 -0
  103. package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +1 -0
  104. package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +1 -0
  105. package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +1 -0
  106. package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +1 -0
  107. package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +1 -0
  108. package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +1 -0
  109. package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +1 -0
  110. package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +1 -0
  111. package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +1 -0
  112. package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +1 -0
  113. package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +1 -0
  114. package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +1 -0
  115. package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +1 -0
  116. package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +1 -0
  117. package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +1 -0
  118. package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +1 -0
  119. package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +1 -0
  120. package/.next/static/chunks/app/page-b51087831582ee37.js +1 -0
  121. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +1 -0
  122. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +1 -0
  123. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +1 -0
  124. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +1 -0
  125. package/.next/static/chunks/webpack-c31263fd83d75ce6.js +1 -0
  126. package/.next/static/css/850c2fd130ffa2fc.css +1 -0
  127. package/.next/static/zsynuO_9Kx6nrXgkTUJ04/_buildManifest.js +1 -0
  128. package/.next/types/routes.d.ts +1 -4
  129. package/.next/types/validator.ts +0 -27
  130. package/README.md +17 -29
  131. package/assets/archive.jpg +0 -0
  132. package/assets/growth-1.jpg +0 -0
  133. package/assets/growth-2.jpg +0 -0
  134. package/assets/import.jpg +0 -0
  135. package/assets/main.jpg +0 -0
  136. package/assets/outline.jpg +0 -0
  137. package/assets/prompts-1.jpg +0 -0
  138. package/assets/prompts-2.jpg +0 -0
  139. package/assets/providers-1.jpg +0 -0
  140. package/assets/providers-2.jpg +0 -0
  141. package/assets/providers-3.jpg +0 -0
  142. package/assets/trace-1.jpg +0 -0
  143. package/assets/trace-2.png +0 -0
  144. package/assets/trace-3.png +0 -0
  145. package/assets/trace-4.png +0 -0
  146. package/assets/viewer-1.jpg +0 -0
  147. package/assets/viewer-2.jpg +0 -0
  148. package/bin/pi.js +50 -0
  149. package/next.config.ts +32 -7
  150. package/package.json +4 -9
  151. package/scripts/launcher.mjs +1 -1
  152. package/scripts/pick-folder.ps1 +2 -11
  153. package/scripts/release.mjs +31 -11
  154. package/scripts/verify-package.mjs +0 -53
  155. package/src/app/api/config/route.ts +1 -4
  156. package/src/app/api/growth/route.ts +14 -14
  157. package/src/app/api/models/route.ts +2 -1
  158. package/src/app/api/security/route.ts +3 -8
  159. package/src/app/api/update/route.ts +2 -8
  160. package/src/app/globals.css +90 -777
  161. package/src/app/layout.tsx +6 -13
  162. package/src/components/AppShell.tsx +229 -521
  163. package/src/components/ChatInput.tsx +26 -153
  164. package/src/components/ChatWindow.tsx +231 -51
  165. package/src/components/ContextMeter.tsx +17 -11
  166. package/src/components/FileViewer.tsx +11 -10
  167. package/src/components/GitPanel.tsx +11 -13
  168. package/src/components/ModelSelector.tsx +6 -19
  169. package/src/components/PiMark.tsx +38 -5
  170. package/src/components/ProjectPanel.tsx +111 -95
  171. package/src/components/PromptPanel.tsx +51 -132
  172. package/src/components/SessionImportSection.tsx +37 -53
  173. package/src/components/SessionSidebar.tsx +57 -159
  174. package/src/components/SettingsPanel.tsx +193 -330
  175. package/src/components/icons.tsx +0 -23
  176. package/src/hooks/useGrowth.ts +123 -156
  177. package/src/hooks/usePiWeb.ts +15 -122
  178. package/src/i18n.tsx +42 -98
  179. package/src/lib/agent-manager.ts +21 -187
  180. package/src/lib/command-validation.ts +1 -9
  181. package/src/lib/growth-service.ts +390 -166
  182. package/src/lib/growth-tracker.ts +64 -264
  183. package/src/lib/growth-turns.ts +31 -1
  184. package/src/lib/models/model-scope.ts +15 -0
  185. package/src/lib/models-service.ts +16 -0
  186. package/src/lib/pi-settings.ts +7 -0
  187. package/src/lib/pi.ts +2 -3
  188. package/src/lib/session-import/claude.ts +2 -13
  189. package/src/lib/session-import/codex.ts +8 -44
  190. package/src/lib/session-import/files.ts +0 -14
  191. package/src/lib/session-import/grok.ts +24 -54
  192. package/src/lib/session-import/index.ts +0 -5
  193. package/src/lib/session-import/types.ts +0 -12
  194. package/src/lib/session-import/writer.ts +3 -8
  195. package/src/lib/skills-service.ts +9 -16
  196. package/src/lib/theme.ts +1 -68
  197. package/src/lib/trajectory.ts +0 -1
  198. package/src/lib/types.ts +27 -36
  199. package/tsconfig.json +2 -1
  200. package/.next/server/app/api/swarm/[id]/route.js +0 -1
  201. package/.next/server/app/api/swarm/[id]/route.js.nft.json +0 -1
  202. package/.next/server/app/api/swarm/[id]/route_client-reference-manifest.js +0 -1
  203. package/.next/server/app/api/swarm/route.js +0 -1
  204. package/.next/server/app/api/swarm/route.js.nft.json +0 -1
  205. package/.next/server/app/api/swarm/route_client-reference-manifest.js +0 -1
  206. package/.next/server/app/api/tests/route.js +0 -2
  207. package/.next/server/app/api/tests/route.js.nft.json +0 -1
  208. package/.next/server/app/api/tests/route_client-reference-manifest.js +0 -1
  209. package/.next/server/chunks/757.js +0 -1
  210. package/.next/server/chunks/775.js +0 -3
  211. package/.next/static/0t1jBUAuVaTFTXxRyIo3q/_buildManifest.js +0 -1
  212. package/.next/static/chunks/2608.43c58236c9cce8e6.js +0 -1
  213. package/.next/static/chunks/2716.4ec42df1d51354c9.js +0 -1
  214. package/.next/static/chunks/4632.62139b565b657aac.js +0 -1
  215. package/.next/static/chunks/4673.7796104395deb8d7.js +0 -1
  216. package/.next/static/chunks/4686-17f21a006739a596.js +0 -1
  217. package/.next/static/chunks/7577.10af9bb661652da8.js +0 -1
  218. package/.next/static/chunks/8866.38ec4ac4ee5a1b74.js +0 -1
  219. package/.next/static/chunks/app/_global-error/page-5148173b3f55e415.js +0 -1
  220. package/.next/static/chunks/app/api/agent/[id]/events/route-5148173b3f55e415.js +0 -1
  221. package/.next/static/chunks/app/api/agent/[id]/route-5148173b3f55e415.js +0 -1
  222. package/.next/static/chunks/app/api/agent/[id]/tree/route-5148173b3f55e415.js +0 -1
  223. package/.next/static/chunks/app/api/agent/new/route-5148173b3f55e415.js +0 -1
  224. package/.next/static/chunks/app/api/config/route-5148173b3f55e415.js +0 -1
  225. package/.next/static/chunks/app/api/files/route-5148173b3f55e415.js +0 -1
  226. package/.next/static/chunks/app/api/git/route-5148173b3f55e415.js +0 -1
  227. package/.next/static/chunks/app/api/growth/route-5148173b3f55e415.js +0 -1
  228. package/.next/static/chunks/app/api/health/route-5148173b3f55e415.js +0 -1
  229. package/.next/static/chunks/app/api/models/route-5148173b3f55e415.js +0 -1
  230. package/.next/static/chunks/app/api/pi-settings/route-5148173b3f55e415.js +0 -1
  231. package/.next/static/chunks/app/api/plugins/route-5148173b3f55e415.js +0 -1
  232. package/.next/static/chunks/app/api/prompts/route-5148173b3f55e415.js +0 -1
  233. package/.next/static/chunks/app/api/security/route-5148173b3f55e415.js +0 -1
  234. package/.next/static/chunks/app/api/sessions/[id]/export/route-5148173b3f55e415.js +0 -1
  235. package/.next/static/chunks/app/api/sessions/[id]/route-5148173b3f55e415.js +0 -1
  236. package/.next/static/chunks/app/api/sessions/import/route-5148173b3f55e415.js +0 -1
  237. package/.next/static/chunks/app/api/sessions/route-5148173b3f55e415.js +0 -1
  238. package/.next/static/chunks/app/api/skills/route-5148173b3f55e415.js +0 -1
  239. package/.next/static/chunks/app/api/swarm/[id]/route-5148173b3f55e415.js +0 -1
  240. package/.next/static/chunks/app/api/swarm/route-5148173b3f55e415.js +0 -1
  241. package/.next/static/chunks/app/api/tests/route-5148173b3f55e415.js +0 -1
  242. package/.next/static/chunks/app/api/update/route-5148173b3f55e415.js +0 -1
  243. package/.next/static/chunks/app/api/version/route-5148173b3f55e415.js +0 -1
  244. package/.next/static/chunks/app/api/web-auth/route-5148173b3f55e415.js +0 -1
  245. package/.next/static/chunks/app/api/workspaces/route-5148173b3f55e415.js +0 -1
  246. package/.next/static/chunks/app/page-14379b8060d5cf07.js +0 -1
  247. package/.next/static/chunks/next/dist/client/components/builtin/app-error-5148173b3f55e415.js +0 -1
  248. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-5148173b3f55e415.js +0 -1
  249. package/.next/static/chunks/next/dist/client/components/builtin/not-found-5148173b3f55e415.js +0 -1
  250. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-5148173b3f55e415.js +0 -1
  251. package/.next/static/chunks/webpack-d1a69ad9edb38b11.js +0 -1
  252. package/.next/static/css/d0a0162cb481abe8.css +0 -1
  253. package/.next/types/app/api/swarm/[id]/route.ts +0 -353
  254. package/.next/types/app/api/swarm/route.ts +0 -353
  255. package/.next/types/app/api/tests/route.ts +0 -353
  256. package/assets/archive.png +0 -0
  257. package/assets/growth-1.png +0 -0
  258. package/assets/growth-2.png +0 -0
  259. package/assets/import.png +0 -0
  260. package/assets/main.png +0 -0
  261. package/assets/outline.png +0 -0
  262. package/assets/prompts-1.png +0 -0
  263. package/assets/prompts-2.png +0 -0
  264. package/assets/providers-1.png +0 -0
  265. package/assets/providers-2.png +0 -0
  266. package/assets/providers-3.png +0 -0
  267. package/assets/trace-1.png +0 -0
  268. package/assets/viewer-1.png +0 -0
  269. package/assets/viewer-2.png +0 -0
  270. package/scripts/managed-niubash-assets.mjs +0 -146
  271. package/scripts/prepare-niubash-packages.mjs +0 -182
  272. package/scripts/run-test-file.mjs +0 -34
  273. package/scripts/smoke-managed-niubash.mjs +0 -62
  274. package/scripts/verify-release-config.mjs +0 -24
  275. package/src/app/api/swarm/[id]/route.ts +0 -28
  276. package/src/app/api/swarm/route.ts +0 -24
  277. package/src/app/api/tests/route.ts +0 -27
  278. package/src/components/SwarmCoordinatorPod.tsx +0 -1021
  279. package/src/components/TestInspectorPod.tsx +0 -549
  280. package/src/lib/delegation.ts +0 -93
  281. package/src/lib/growth-rounds.ts +0 -84
  282. package/src/lib/managed-niubash.ts +0 -231
  283. package/src/lib/runtime-activity.ts +0 -27
  284. package/src/lib/session-import/lock.ts +0 -25
  285. package/src/lib/swarm-service.ts +0 -371
  286. package/src/lib/test-run-service.ts +0 -122
  287. package/src/lib/workflow.ts +0 -55
  288. /package/.next/static/{0t1jBUAuVaTFTXxRyIo3q → zsynuO_9Kx6nrXgkTUJ04}/_ssgManifest.js +0 -0
@@ -1,371 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
- import { promisify } from "node:util";
5
- import { randomUUID } from "node:crypto";
6
- import { DefaultResourceLoader, SessionManager, createAgentSession, defineTool, type AgentSession, type ToolDefinition } from "@earendil-works/pi-coding-agent";
7
- import { Type } from "typebox";
8
- import { BoundaryError, isPathInside, resolveWorkspacePath, samePath } from "./path-security";
9
- import { getAgentDir, getModelRuntime, getSettingsManager, resolveProjectTrust, withResourceLock } from "./pi";
10
- import { beginActivity } from "./runtime-activity";
11
-
12
- const exec = promisify(execFile);
13
- const STORE = path.join(getAgentDir(), "piweb-swarms");
14
- const MAX_TASKS = 3;
15
- const MAX_FILE_BYTES = 200_000;
16
- const MAX_PATCH_BYTES = 2_000_000;
17
- const TASK_TIMEOUT_MS = 10 * 60_000;
18
- const jobs = new Map<string, SwarmJob>();
19
- const controllers = new Map<string, AbortController>();
20
- const saveQueues = new Map<string, Promise<void>>();
21
- const accepting = new Set<string>();
22
- const worktreeQueues = new Map<string, Promise<void>>();
23
-
24
- export interface SwarmTaskInput { title: string; instruction: string }
25
- export interface SwarmTask extends SwarmTaskInput {
26
- status: "pending" | "running" | "completed" | "failed" | "cancelled";
27
- report?: string;
28
- patch?: string;
29
- error?: string;
30
- accepted?: boolean;
31
- }
32
- export interface SwarmJob {
33
- id: string;
34
- cwd: string;
35
- root: string;
36
- base: string;
37
- status: "running" | "completed" | "failed" | "cancelled" | "interrupted";
38
- createdAt: number;
39
- finishedAt?: number;
40
- tasks: SwarmTask[];
41
- }
42
-
43
- function publicJob(job: SwarmJob): SwarmJob { return structuredClone(job); }
44
- function jobFile(id: string): string {
45
- if (!/^[0-9a-f-]{36}$/.test(id)) throw new BoundaryError("invalid swarm id");
46
- return path.join(STORE, "jobs", `${id}.json`);
47
- }
48
- function save(job: SwarmJob): Promise<void> {
49
- const previous = saveQueues.get(job.id) ?? Promise.resolve();
50
- const next = previous.catch(() => undefined).then(async () => {
51
- const file = jobFile(job.id);
52
- await fs.mkdir(path.dirname(file), { recursive: true });
53
- const tmp = `${file}.${randomUUID()}.tmp`;
54
- await fs.writeFile(tmp, JSON.stringify(job), "utf8");
55
- await fs.rename(tmp, file);
56
- });
57
- saveQueues.set(job.id, next);
58
- return next;
59
- }
60
- async function git(cwd: string, args: string[], maxBuffer = 4_000_000): Promise<string> {
61
- const result = await exec("git", args, {
62
- cwd, timeout: 30_000, maxBuffer, windowsHide: true,
63
- env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" },
64
- });
65
- return result.stdout;
66
- }
67
- function worktreeGit(root: string, args: string[]): Promise<string> {
68
- const previous = worktreeQueues.get(root) ?? Promise.resolve();
69
- const next = previous.catch(() => undefined).then(() => git(root, args));
70
- worktreeQueues.set(root, next.then(() => undefined, () => undefined));
71
- return next;
72
- }
73
- function errorText(error: unknown): string {
74
- const value = error as { stderr?: unknown; message?: unknown };
75
- return String(value?.stderr || value?.message || error).trim().slice(0, 1000);
76
- }
77
- async function workspace(cwdValue: unknown): Promise<{ cwd: string; root: string; base: string }> {
78
- const cwd = await resolveWorkspacePath(cwdValue);
79
- const root = path.resolve((await git(cwd, ["rev-parse", "--show-toplevel"]).catch(() => {
80
- throw new BoundaryError("workspace is not a Git repository");
81
- })).trim());
82
- if (!isPathInside(root, cwd)) throw new BoundaryError("invalid repository root");
83
- if (!resolveProjectTrust(root).trusted) throw new BoundaryError("trust this project before running swarm tasks");
84
- const status = await git(root, ["status", "--porcelain", "-z"]);
85
- if (status) throw new BoundaryError("commit or stash workspace changes before starting a swarm");
86
- return { cwd, root, base: (await git(root, ["rev-parse", "HEAD"])).trim() };
87
- }
88
- function checkedInput(value: unknown): SwarmTaskInput[] {
89
- if (!Array.isArray(value) || value.length < 1 || value.length > MAX_TASKS) throw new BoundaryError("provide one to three tasks");
90
- return value.map((item) => {
91
- const title = item?.title;
92
- const instruction = item?.instruction;
93
- if (typeof title !== "string" || !title.trim() || title.length > 80 || typeof instruction !== "string" || !instruction.trim() || instruction.length > 4000) {
94
- throw new BoundaryError("invalid swarm task");
95
- }
96
- return { title: title.trim(), instruction: instruction.trim() };
97
- });
98
- }
99
- function protectedComponent(part: string): boolean {
100
- const name = process.platform === "win32" ? part.toLowerCase().split(":")[0].replace(/[. ]+$/, "") : part;
101
- return name === ".git" || name === "node_modules";
102
- }
103
- function assertToolTarget(root: string, target: string): void {
104
- if (!isPathInside(root, target)) throw new BoundaryError("path escapes isolated worktree");
105
- if (path.relative(root, target).split(path.sep).some(protectedComponent)) throw new BoundaryError("protected worktree path");
106
- }
107
- export async function resolveSwarmToolPath(root: string, value: string): Promise<string> {
108
- if (typeof value !== "string" || !value.trim() || value.length > 4096) throw new BoundaryError("invalid file path");
109
- const target = path.resolve(root, value);
110
- assertToolTarget(root, target);
111
- const canonicalRoot = await fs.realpath(root);
112
- let ancestor = target;
113
- for (;;) {
114
- const real = await fs.realpath(ancestor).catch((error: NodeJS.ErrnoException) => {
115
- if (error.code === "ENOENT") return null;
116
- throw error;
117
- });
118
- if (real) {
119
- assertToolTarget(canonicalRoot, real);
120
- break;
121
- }
122
- const parent = path.dirname(ancestor);
123
- if (parent === ancestor) throw new BoundaryError("invalid worktree path");
124
- ancestor = parent;
125
- }
126
- return target;
127
- }
128
- function toolText(text: string) { return { content: [{ type: "text" as const, text }], details: undefined }; }
129
- function fileTools(root: string): ToolDefinition[] {
130
- return [
131
- defineTool({
132
- name: "swarm_read", label: "Read isolated file", description: "Read a UTF-8 file inside this isolated worktree.",
133
- parameters: Type.Object({ path: Type.String() }),
134
- async execute(_id, args) {
135
- const file = await resolveSwarmToolPath(root, args.path);
136
- const stat = await fs.stat(file);
137
- if (!stat.isFile() || stat.size > MAX_FILE_BYTES) throw new BoundaryError("file is not readable or is too large");
138
- return toolText(await fs.readFile(file, "utf8"));
139
- },
140
- }),
141
- defineTool({
142
- name: "swarm_list", label: "List isolated directory", description: "List files and directories inside this isolated worktree.",
143
- parameters: Type.Object({ path: Type.String() }),
144
- async execute(_id, args) {
145
- const dir = await resolveSwarmToolPath(root, args.path);
146
- const entries = (await fs.readdir(dir, { withFileTypes: true }))
147
- .filter((entry) => !protectedComponent(entry.name))
148
- .slice(0, 200).map((entry) => `${entry.isDirectory() ? "dir " : "file"} ${entry.name}`);
149
- return toolText(entries.join("\n") || "(empty)");
150
- },
151
- }),
152
- defineTool({
153
- name: "swarm_search", label: "Search isolated files", description: "Find a literal text string in tracked source files inside this isolated worktree.",
154
- parameters: Type.Object({ query: Type.String(), path: Type.Optional(Type.String()) }),
155
- async execute(_id, args) {
156
- if (!args.query || args.query.length > 200) throw new BoundaryError("invalid search query");
157
- const start = await resolveSwarmToolPath(root, args.path || ".");
158
- const found: string[] = [];
159
- let scanned = 0;
160
- async function walk(dir: string): Promise<void> {
161
- if (scanned >= 1000 || found.length >= 100) return;
162
- for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
163
- if (protectedComponent(entry.name) || entry.isSymbolicLink()) continue;
164
- const file = await resolveSwarmToolPath(root, path.join(dir, entry.name));
165
- if (entry.isDirectory()) await walk(file);
166
- else if (entry.isFile()) {
167
- scanned += 1;
168
- if ((await fs.stat(file)).size > MAX_FILE_BYTES) continue;
169
- const content = await fs.readFile(file, "utf8");
170
- content.split("\n").forEach((line, index) => {
171
- if (found.length < 100 && line.includes(args.query)) found.push(`${path.relative(root, file)}:${index + 1}: ${line.slice(0, 300)}`);
172
- });
173
- }
174
- if (scanned >= 1000 || found.length >= 100) break;
175
- }
176
- }
177
- const stat = await fs.stat(start);
178
- if (stat.isDirectory()) await walk(start);
179
- else if (stat.isFile()) {
180
- const content = await fs.readFile(start, "utf8");
181
- content.split("\n").forEach((line, index) => {
182
- if (found.length < 100 && line.includes(args.query)) found.push(`${path.relative(root, start)}:${index + 1}: ${line.slice(0, 300)}`);
183
- });
184
- }
185
- return toolText(found.join("\n") || "No matches");
186
- },
187
- }),
188
- defineTool({
189
- name: "swarm_edit", label: "Edit isolated file", description: "Replace one exact, unique text occurrence in an isolated file.",
190
- executionMode: "sequential",
191
- parameters: Type.Object({ path: Type.String(), oldText: Type.String(), newText: Type.String() }),
192
- async execute(_id, args) {
193
- const file = await resolveSwarmToolPath(root, args.path);
194
- if (!args.oldText || args.newText.length > MAX_FILE_BYTES) throw new BoundaryError("invalid edit");
195
- const stat = await fs.stat(file);
196
- if (!stat.isFile() || stat.size > MAX_FILE_BYTES) throw new BoundaryError("file is not editable or is too large");
197
- const before = await fs.readFile(file, "utf8");
198
- const offset = before.indexOf(args.oldText);
199
- if (offset < 0 || before.indexOf(args.oldText, offset + 1) >= 0) throw new BoundaryError("oldText must match exactly once");
200
- const after = before.slice(0, offset) + args.newText + before.slice(offset + args.oldText.length);
201
- if (Buffer.byteLength(after) > MAX_FILE_BYTES) throw new BoundaryError("edited file is too large");
202
- await fs.writeFile(file, after, "utf8");
203
- return toolText(`Updated ${path.relative(root, file)}`);
204
- },
205
- }),
206
- defineTool({
207
- name: "swarm_write", label: "Write isolated file", description: "Create or replace a UTF-8 file inside this isolated worktree.",
208
- executionMode: "sequential",
209
- parameters: Type.Object({ path: Type.String(), content: Type.String() }),
210
- async execute(_id, args) {
211
- if (Buffer.byteLength(args.content) > MAX_FILE_BYTES) throw new BoundaryError("file is too large");
212
- const file = await resolveSwarmToolPath(root, args.path);
213
- await fs.mkdir(path.dirname(file), { recursive: true });
214
- await fs.writeFile(file, args.content, "utf8");
215
- return toolText(`Wrote ${path.relative(root, file)}`);
216
- },
217
- }),
218
- ];
219
- }
220
- function reportOf(session: AgentSession): string {
221
- const answer = [...session.messages].reverse().find((message) => message.role === "assistant");
222
- if (!answer || answer.role !== "assistant") throw new Error("agent returned no assistant response");
223
- if (answer.stopReason !== "stop") throw new Error(answer.errorMessage || `agent stopped: ${answer.stopReason}`);
224
- return answer.content.filter((part) => part.type === "text").map((part) => part.text).join("\n").slice(0, 20_000);
225
- }
226
- async function runTask(job: SwarmJob, index: number, signal: AbortSignal): Promise<void> {
227
- const task = job.tasks[index];
228
- const worktree = path.join(STORE, "worktrees", job.id, String(index));
229
- let created = false;
230
- let child: AgentSession | undefined;
231
- let timer: ReturnType<typeof setTimeout> | undefined;
232
- let timedOut = false;
233
- try {
234
- if (signal.aborted) throw new Error("cancelled");
235
- task.status = "running";
236
- await save(job);
237
- await fs.mkdir(path.dirname(worktree), { recursive: true });
238
- await worktreeGit(job.root, ["worktree", "add", "--detach", worktree, job.base]);
239
- created = true;
240
- if (signal.aborted) throw new Error("cancelled");
241
- const loader = new DefaultResourceLoader({
242
- cwd: worktree, agentDir: getAgentDir(), settingsManager: getSettingsManager(worktree),
243
- noExtensions: true, noPromptTemplates: true, noThemes: true,
244
- appendSystemPrompt: ["You are a PiWeb swarm worker in an isolated Git worktree. Use only swarm_* tools. Never access other directories. Make only the assigned changes, then report what changed and what you could not verify. Do not claim tests ran; no command execution is available."],
245
- });
246
- try {
247
- await withResourceLock(() => loader.reload());
248
- if (signal.aborted) throw new Error("cancelled");
249
- const result = await createAgentSession({
250
- cwd: worktree, agentDir: getAgentDir(), sessionManager: SessionManager.inMemory(worktree),
251
- modelRuntime: await getModelRuntime(), settingsManager: getSettingsManager(worktree),
252
- resourceLoader: loader, tools: ["swarm_read", "swarm_list", "swarm_search", "swarm_edit", "swarm_write"],
253
- customTools: fileTools(worktree),
254
- });
255
- child = result.session;
256
- if (signal.aborted) throw new Error("cancelled");
257
- const abort = () => { void child?.abort().catch(() => undefined); };
258
- signal.addEventListener("abort", abort, { once: true });
259
- timer = setTimeout(() => { timedOut = true; abort(); }, TASK_TIMEOUT_MS);
260
- try { await child.prompt(`${task.title}\n\n${task.instruction}`, { source: "rpc", expandPromptTemplates: false }); }
261
- finally { signal.removeEventListener("abort", abort); }
262
- if (timedOut) throw new Error("task timed out after ten minutes");
263
- if (signal.aborted) throw new Error("cancelled");
264
- task.report = reportOf(child);
265
- await git(worktree, ["add", "-N", "--", "."]);
266
- const patch = await git(worktree, ["diff", "--binary", "HEAD", "--", "."], MAX_PATCH_BYTES + 1024);
267
- if (Buffer.byteLength(patch) > MAX_PATCH_BYTES) throw new Error("patch exceeds 2 MB limit");
268
- task.patch = patch;
269
- task.status = "completed";
270
- } finally {
271
- child?.dispose();
272
- loader.getExtensions().runtime.invalidate();
273
- }
274
- } catch (error) {
275
- task.status = signal.aborted ? "cancelled" : "failed";
276
- task.error = errorText(error);
277
- } finally {
278
- if (timer) clearTimeout(timer);
279
- if (created) await worktreeGit(job.root, ["worktree", "remove", "--force", worktree]).catch(() => undefined);
280
- if (isPathInside(path.join(STORE, "worktrees"), worktree)) await fs.rm(worktree, { recursive: true, force: true }).catch(() => undefined);
281
- await save(job);
282
- }
283
- }
284
- export async function startSwarm(cwdValue: unknown, taskValue: unknown): Promise<SwarmJob> {
285
- const releaseActivity = beginActivity("swarm");
286
- try {
287
- return await startSwarmActive(cwdValue, taskValue, releaseActivity);
288
- } catch (error) {
289
- releaseActivity();
290
- throw error;
291
- }
292
- }
293
- async function startSwarmActive(cwdValue: unknown, taskValue: unknown, releaseActivity: () => void): Promise<SwarmJob> {
294
- const tasks = checkedInput(taskValue);
295
- const { cwd, root, base } = await workspace(cwdValue);
296
- if ([...jobs.values()].some((job) => samePath(job.root, root) && job.status === "running")) throw new BoundaryError("a swarm is already running for this repository");
297
- const job: SwarmJob = { id: randomUUID(), cwd, root, base, status: "running", createdAt: Date.now(), tasks: tasks.map((task) => ({ ...task, status: "pending" })) };
298
- jobs.set(job.id, job);
299
- const controller = new AbortController();
300
- controllers.set(job.id, controller);
301
- try {
302
- await save(job);
303
- } catch (error) {
304
- controllers.delete(job.id);
305
- jobs.delete(job.id);
306
- throw error;
307
- }
308
- void Promise.all(job.tasks.map((_, index) => runTask(job, index, controller.signal))).then(async () => {
309
- job.status = controller.signal.aborted ? "cancelled" : job.tasks.every((task) => task.status === "completed") ? "completed" : "failed";
310
- job.finishedAt = Date.now();
311
- controllers.delete(job.id);
312
- await save(job);
313
- }).catch(async () => {
314
- job.status = "failed";
315
- job.finishedAt = Date.now();
316
- controllers.delete(job.id);
317
- await save(job).catch(() => undefined);
318
- }).finally(releaseActivity);
319
- return publicJob(job);
320
- }
321
- export async function getSwarm(id: string): Promise<SwarmJob> {
322
- const live = jobs.get(id);
323
- if (live) return publicJob(live);
324
- const raw = await fs.readFile(jobFile(id), "utf8").catch(() => { throw new BoundaryError("swarm not found"); });
325
- const job = JSON.parse(raw) as SwarmJob;
326
- if (job.status === "running") job.status = "interrupted";
327
- return publicJob(job);
328
- }
329
- export async function listSwarms(cwdValue: unknown): Promise<SwarmJob[]> {
330
- const cwd = await resolveWorkspacePath(cwdValue);
331
- const files = await fs.readdir(path.join(STORE, "jobs")).catch(() => []);
332
- const loaded = await Promise.all(files.filter((name) => /^[0-9a-f-]{36}\.json$/.test(name)).map((name) => getSwarm(name.slice(0, -5)).catch(() => null)));
333
- return loaded.filter((job): job is SwarmJob => !!job && samePath(job.cwd, cwd)).sort((a, b) => b.createdAt - a.createdAt).slice(0, 20);
334
- }
335
- export async function cancelSwarm(id: string, cwdValue: unknown): Promise<SwarmJob> {
336
- const job = await getSwarm(id);
337
- const cwd = await resolveWorkspacePath(cwdValue);
338
- if (!samePath(job.cwd, cwd)) throw new BoundaryError("swarm belongs to another workspace");
339
- const controller = controllers.get(id);
340
- if (!controller || job.status !== "running") throw new BoundaryError("swarm is not running");
341
- controller.abort();
342
- return getSwarm(id);
343
- }
344
- export async function acceptSwarmTask(id: string, index: number, cwdValue: unknown): Promise<SwarmJob> {
345
- const job = jobs.get(id) ?? await getSwarm(id);
346
- const cwd = await resolveWorkspacePath(cwdValue);
347
- if (!samePath(job.cwd, cwd)) throw new BoundaryError("swarm belongs to another workspace");
348
- if (job.status === "running") throw new BoundaryError("wait for all swarm tasks to finish before accepting changes");
349
- if (!resolveProjectTrust(job.root).trusted) throw new BoundaryError("trust this project before accepting changes");
350
- if (!Number.isInteger(index) || index < 0 || index >= job.tasks.length) throw new BoundaryError("invalid task index");
351
- const task = job.tasks[index];
352
- if (task.status !== "completed" || !task.patch || task.accepted) throw new BoundaryError("task has no unapplied patch");
353
- if (accepting.has(job.root)) throw new BoundaryError("another patch is being accepted");
354
- accepting.add(job.root);
355
- const patchFile = path.join(STORE, `accept-${randomUUID()}.patch`);
356
- try {
357
- const head = (await git(job.root, ["rev-parse", "HEAD"])).trim();
358
- if (head !== job.base) throw new BoundaryError("repository HEAD changed; review the patch again before applying");
359
- await fs.mkdir(STORE, { recursive: true });
360
- await fs.writeFile(patchFile, task.patch, "utf8");
361
- await git(job.root, ["apply", "--check", "--binary", patchFile]);
362
- await git(job.root, ["apply", "--binary", patchFile]);
363
- task.accepted = true;
364
- jobs.set(id, job);
365
- await save(job);
366
- return publicJob(job);
367
- } finally {
368
- accepting.delete(job.root);
369
- if (isPathInside(STORE, patchFile)) await fs.rm(patchFile, { force: true }).catch(() => undefined);
370
- }
371
- }
@@ -1,122 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import fs from "node:fs/promises";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { randomUUID } from "node:crypto";
6
- import { promisify } from "node:util";
7
- import { BoundaryError, isPathInside, resolveWorkspacePath } from "./path-security";
8
- import { resolveProjectTrust } from "./pi";
9
- import { APP_ROOT } from "./app-root";
10
- import { beginActivity } from "./runtime-activity";
11
-
12
- const exec = promisify(execFile);
13
- const running = new Set<string>();
14
- const TEST_NAME = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
15
- const MAX_SOURCE_BYTES = 200_000;
16
-
17
- export interface TestRunResult {
18
- file: string;
19
- passed: number;
20
- failed: number;
21
- skipped: number;
22
- durationMs: number;
23
- success: boolean;
24
- tests: Array<{ name: string; status: string; durationMs: number }>;
25
- output: string;
26
- }
27
-
28
- async function testRoot(cwdValue: unknown): Promise<{ cwd: string; root: string }> {
29
- const cwd = await resolveWorkspacePath(cwdValue);
30
- const root = path.join(cwd, "tests");
31
- const real = await fs.realpath(root).catch(() => { throw new BoundaryError("tests directory not found"); });
32
- if (!isPathInside(cwd, real)) throw new BoundaryError("tests directory escapes workspace");
33
- return { cwd, root: real };
34
- }
35
- async function testFile(cwdValue: unknown, fileValue: unknown): Promise<{ cwd: string; file: string; relative: string }> {
36
- const { cwd, root } = await testRoot(cwdValue);
37
- if (typeof fileValue !== "string" || !TEST_NAME.test(fileValue) || fileValue.length > 4096) throw new BoundaryError("invalid test file");
38
- const file = await fs.realpath(path.resolve(cwd, fileValue)).catch(() => { throw new BoundaryError("test file not found"); });
39
- if (!isPathInside(root, file)) throw new BoundaryError("test file escapes tests directory");
40
- const stat = await fs.stat(file);
41
- if (!stat.isFile() || stat.size > MAX_SOURCE_BYTES) throw new BoundaryError("test file is too large");
42
- return { cwd, file, relative: path.relative(cwd, file).replace(/\\/g, "/") };
43
- }
44
- export async function listTestFiles(cwdValue: unknown): Promise<string[]> {
45
- const { cwd, root } = await testRoot(cwdValue);
46
- const files: string[] = [];
47
- async function walk(dir: string): Promise<void> {
48
- for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
49
- if (files.length >= 500) break;
50
- if (entry.isSymbolicLink()) continue;
51
- const candidate = path.join(dir, entry.name);
52
- if (entry.isDirectory()) await walk(candidate);
53
- else if (entry.isFile() && TEST_NAME.test(entry.name)) files.push(path.relative(cwd, candidate).replace(/\\/g, "/"));
54
- }
55
- }
56
- await walk(root);
57
- return files.sort();
58
- }
59
- export async function readTestFile(cwdValue: unknown, fileValue: unknown): Promise<{ file: string; content: string }> {
60
- const { file, relative } = await testFile(cwdValue, fileValue);
61
- return { file: relative, content: await fs.readFile(file, "utf8") };
62
- }
63
- export function summarizeVitestReport(data: any, stdout: string, stderr: string): Pick<TestRunResult, "passed" | "failed" | "skipped" | "tests" | "output"> {
64
- const suites = Array.isArray(data?.testResults) ? data.testResults : [];
65
- const tests = suites.flatMap((suite: any) => Array.isArray(suite?.assertionResults)
66
- ? suite.assertionResults.map((test: any) => ({ name: String(test.fullName ?? test.title ?? ""), status: String(test.status ?? "unknown"), durationMs: Number(test.duration ?? 0) }))
67
- : []);
68
- const failures = suites.flatMap((suite: any) => [
69
- ...(suite?.message ? [String(suite.message)] : []),
70
- ...(Array.isArray(suite?.assertionResults) ? suite.assertionResults.flatMap((test: any) => Array.isArray(test?.failureMessages) ? test.failureMessages.map(String) : []) : []),
71
- ]);
72
- return {
73
- passed: Number(data?.numPassedTests ?? tests.filter((test: { status: string }) => test.status === "passed").length),
74
- failed: Number(data?.numFailedTests ?? tests.filter((test: { status: string }) => test.status === "failed").length),
75
- skipped: Number(data?.numPendingTests ?? 0),
76
- tests,
77
- output: `${data ? failures.join("\n\n") : stdout}\n${stderr}`.trim().slice(-20_000),
78
- };
79
- }
80
- export async function runTestFile(cwdValue: unknown, fileValue: unknown): Promise<TestRunResult> {
81
- const { cwd, file, relative } = await testFile(cwdValue, fileValue);
82
- if (!resolveProjectTrust(cwd).trusted) throw new BoundaryError("trust this project before running tests");
83
- if (running.has(cwd)) throw new BoundaryError("a test run is already active for this workspace");
84
- const releaseActivity = beginActivity("tests");
85
- running.add(cwd);
86
- const started = Date.now();
87
- const reportFile = path.join(os.tmpdir(), `piweb-vitest-${randomUUID()}.json`);
88
- let stdout = "";
89
- let stderr = "";
90
- let exitCode = 0;
91
- try {
92
- const vitest = await fs.realpath(path.join(cwd, "node_modules", "vitest", "vitest.mjs")).catch(() => {
93
- throw new BoundaryError("local Vitest is not installed in this workspace");
94
- });
95
- if (!isPathInside(path.join(cwd, "node_modules"), vitest)) throw new BoundaryError("Vitest entry escapes node_modules");
96
- try {
97
- const result = await exec(process.execPath, [path.join(APP_ROOT, "scripts/run-test-file.mjs"), cwd, file, reportFile], {
98
- cwd, timeout: 180_000, maxBuffer: 2_000_000, windowsHide: true,
99
- env: { ...process.env, CI: "1", FORCE_COLOR: "0", NO_COLOR: "1" },
100
- });
101
- stdout = result.stdout;
102
- stderr = result.stderr;
103
- } catch (error) {
104
- const failure = error as { stdout?: string; stderr?: string; code?: number | string };
105
- stdout = failure.stdout ?? "";
106
- stderr = failure.stderr ?? String(error);
107
- exitCode = typeof failure.code === "number" ? failure.code : 1;
108
- }
109
- let data: any = null;
110
- try { data = JSON.parse(await fs.readFile(reportFile, "utf8")); } catch { /* preserve raw output below */ }
111
- return {
112
- file: relative,
113
- durationMs: Date.now() - started,
114
- success: exitCode === 0 && data?.success === true,
115
- ...summarizeVitestReport(data, stdout, stderr),
116
- };
117
- } finally {
118
- running.delete(cwd);
119
- releaseActivity();
120
- await fs.rm(reportFile, { force: true }).catch(() => undefined);
121
- }
122
- }
@@ -1,55 +0,0 @@
1
- import type { SessionManager } from "@earendil-works/pi-coding-agent";
2
- import type { WorkflowMode, WorkflowState } from "./types";
3
-
4
- export type { WorkflowMode, WorkflowState } from "./types";
5
-
6
- const ENTRY_TYPE = "piweb.workflow.v1";
7
- const DEFAULT_STATE: WorkflowState = { mode: "agent", planStatus: "idle", goal: "" };
8
-
9
- export function isWorkflowMode(value: unknown): value is WorkflowMode {
10
- return value === "agent" || value === "plan" || value === "goal";
11
- }
12
-
13
- export function readWorkflow(sm: SessionManager): WorkflowState {
14
- for (const entry of sm.getBranch().slice().reverse()) {
15
- if (entry.type !== "custom" || entry.customType !== ENTRY_TYPE) continue;
16
- const data = entry.data as Partial<WorkflowState> | undefined;
17
- if (!data || !isWorkflowMode(data.mode)) break;
18
- return {
19
- mode: data.mode,
20
- planStatus: data.mode === "plan" && data.planStatus === "ready" ? "ready" : "idle",
21
- planId: data.mode === "plan" && typeof data.planId === "string" ? data.planId : undefined,
22
- goal: data.mode === "goal" && typeof data.goal === "string" ? data.goal.slice(0, 8_000) : "",
23
- };
24
- }
25
- return { ...DEFAULT_STATE };
26
- }
27
-
28
- export function saveWorkflow(sm: SessionManager, state: WorkflowState): void {
29
- sm.appendCustomEntry(ENTRY_TYPE, state);
30
- }
31
-
32
- /** 最近一条工作流记录之后还没有用户消息:下一条(含编辑重发的)用户消息就是 Goal 的目标。 */
33
- export function awaitingGoalMessage(sm: SessionManager): boolean {
34
- for (const entry of sm.getBranch().slice().reverse()) {
35
- if (entry.type === "custom" && entry.customType === ENTRY_TYPE) return true;
36
- if (entry.type === "message" && entry.message.role === "user") return false;
37
- }
38
- return true;
39
- }
40
-
41
- export function workflowInstruction(state: WorkflowState): string {
42
- if (state.mode === "plan") {
43
- return state.planStatus === "executing"
44
- ? "The user approved the plan in this conversation. Execute it now, verify the result, and report any unfinished work. Respect the active tool permissions."
45
- : "Planning mode: investigate using read-only tools. Do not edit files, run commands, or cause side effects. Produce a concrete implementation plan with verification steps, then stop and wait for explicit approval in PiWeb.";
46
- }
47
- if (state.mode === "goal") {
48
- return `Goal mode: work toward the user's objective, verify the result before claiming completion, and clearly report blockers or unverified assumptions. Do not silently change the objective. Objective: ${state.goal || "Use the next user message as the objective."}`;
49
- }
50
- return "";
51
- }
52
-
53
- export function planCanRun(state: WorkflowState): boolean {
54
- return state.mode === "plan" && state.planStatus === "ready";
55
- }