@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,343 @@
1
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
2
+ import { tmpdir } from "os";
3
+ import { join } from "path";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import lockfile from "proper-lockfile";
6
+ import { createTask, getActiveTask, listTasks, loadTask, saveTask, taskAge, taskName, validateFromPath, type TaskState } from "./state.js";
7
+
8
+ const tempDirs: string[] = [];
9
+
10
+ function makeCwd(): string {
11
+ const dir = mkdtempSync(join(tmpdir(), "pi-pi-state-"));
12
+ tempDirs.push(dir);
13
+ return dir;
14
+ }
15
+
16
+ afterEach(() => {
17
+ vi.restoreAllMocks();
18
+ while (tempDirs.length > 0) {
19
+ const dir = tempDirs.pop();
20
+ if (dir) rmSync(dir, { recursive: true, force: true });
21
+ }
22
+ });
23
+
24
+ describe("createTask", () => {
25
+ it("creates implement task directory and initial state", () => {
26
+ const cwd = makeCwd();
27
+ const taskDir = createTask(cwd, "implement", "Build New Feature");
28
+ const state = loadTask(taskDir);
29
+
30
+ expect(taskDir).toContain(join(cwd, ".pp", "state", "implement"));
31
+ expect(state.phase).toBe("brainstorm");
32
+ expect(state.from).toBeNull();
33
+ expect(state.description).toBe("Build New Feature");
34
+ expect(new Date(state.startedAt).toString()).not.toBe("Invalid Date");
35
+
36
+ const raw = readFileSync(join(taskDir, "state.json"), "utf-8");
37
+ expect(raw.endsWith("\n")).toBe(true);
38
+ });
39
+
40
+ it("uses debug as initial phase for debug", () => {
41
+ const cwd = makeCwd();
42
+ const taskDir = createTask(cwd, "debug", "Fix timeout issue");
43
+ const state = loadTask(taskDir);
44
+ expect(state.phase).toBe("debug");
45
+ expect(state.step).toBe("llm_work");
46
+ expect(state.reviewCycle).toBeNull();
47
+ expect(state.reviewPass).toBe(0);
48
+ });
49
+
50
+ it("uses brainstorm as initial phase for brainstorm", () => {
51
+ const cwd = makeCwd();
52
+ const taskDir = createTask(cwd, "brainstorm", "Explore ideas");
53
+ const state = loadTask(taskDir);
54
+ expect(state.phase).toBe("brainstorm");
55
+ expect(state.step).toBe("llm_work");
56
+ expect(state.reviewCycle).toBeNull();
57
+ expect(state.reviewPass).toBe(0);
58
+ });
59
+ });
60
+
61
+ describe("loadTask", () => {
62
+ it("reads and parses state.json", () => {
63
+ const cwd = makeCwd();
64
+ const taskDir = createTask(cwd, "implement", "Readable state");
65
+ const state = loadTask(taskDir);
66
+ expect(state.description).toBe("Readable state");
67
+ });
68
+
69
+ it("throws descriptive error on corrupt json", () => {
70
+ const cwd = makeCwd();
71
+ const taskDir = createTask(cwd, "implement", "Corrupt me");
72
+ writeFileSync(join(taskDir, "state.json"), "{ this-is-not-json", "utf-8");
73
+
74
+ expect(() => loadTask(taskDir)).toThrowError(/Failed to parse .*state\.json:/);
75
+ });
76
+ });
77
+
78
+ describe("saveTask", () => {
79
+ it("writes state.json correctly", () => {
80
+ const cwd = makeCwd();
81
+ const taskDir = createTask(cwd, "implement", "Initial");
82
+ const state: TaskState = {
83
+ phase: "implement",
84
+ step: "user_gate",
85
+ reviewCycle: { kind: "auto", step: "await_reviewers", pass: 2 },
86
+ reviewPass: 1,
87
+ from: "implement/some-task",
88
+ description: "Updated",
89
+ startedAt: "2026-04-20T00:00:00.000Z",
90
+ };
91
+
92
+ saveTask(taskDir, state);
93
+
94
+ const loaded = loadTask(taskDir);
95
+ expect(loaded).toEqual(state);
96
+ });
97
+ });
98
+
99
+ describe("listTasks", () => {
100
+ it("lists only non-done tasks and respects type filter", () => {
101
+ const cwd = makeCwd();
102
+ const implementTask = createTask(cwd, "implement", "Implement feature");
103
+ const debugTask = createTask(cwd, "debug", "Debug crash");
104
+ const brainstormTask = createTask(cwd, "brainstorm", "Idea storm");
105
+
106
+ const doneState = loadTask(debugTask);
107
+ doneState.phase = "done";
108
+ saveTask(debugTask, doneState);
109
+
110
+ const all = listTasks(cwd);
111
+ const allDirs = all.map((t) => t.dir);
112
+ expect(allDirs).toContain(implementTask);
113
+ expect(allDirs).toContain(brainstormTask);
114
+ expect(allDirs).not.toContain(debugTask);
115
+
116
+ const brainstormOnly = listTasks(cwd, "brainstorm");
117
+ expect(brainstormOnly).toHaveLength(1);
118
+ expect(brainstormOnly[0].dir).toBe(brainstormTask);
119
+ expect(brainstormOnly[0].type).toBe("brainstorm");
120
+ });
121
+
122
+ it("skips corrupt task entries", () => {
123
+ const cwd = makeCwd();
124
+ createTask(cwd, "implement", "Healthy task");
125
+ const corrupt = createTask(cwd, "implement", "Broken task");
126
+ writeFileSync(join(corrupt, "state.json"), "{ nope", "utf-8");
127
+
128
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
129
+ const tasks = listTasks(cwd, "implement");
130
+
131
+ expect(tasks).toHaveLength(1);
132
+ expect(tasks[0].state.description).toBe("Healthy task");
133
+ expect(errorSpy).toHaveBeenCalled();
134
+ });
135
+ });
136
+
137
+ describe("validateFromPath", () => {
138
+ it("rejects paths containing dot-dot", () => {
139
+ const cwd = makeCwd();
140
+ expect(validateFromPath(cwd, "implement/../x")).toEqual({
141
+ ok: false,
142
+ reason: "Path must not contain '..'",
143
+ });
144
+ });
145
+
146
+ it("rejects escaping .pp/state", () => {
147
+ const cwd = makeCwd();
148
+ const result = validateFromPath(cwd, "/tmp");
149
+ expect(result).toEqual({
150
+ ok: false,
151
+ reason: "Path escapes .pp/state/ directory",
152
+ });
153
+ });
154
+
155
+ it("rejects missing source directories", () => {
156
+ const cwd = makeCwd();
157
+ expect(validateFromPath(cwd, "implement/missing")).toEqual({
158
+ ok: false,
159
+ reason: "Source task not found: implement/missing",
160
+ });
161
+ });
162
+
163
+ it("rejects directories without state.json", () => {
164
+ const cwd = makeCwd();
165
+ const relative = "implement/no-state";
166
+ mkdirSync(join(cwd, ".pp", "state", relative), { recursive: true });
167
+
168
+ expect(validateFromPath(cwd, relative)).toEqual({
169
+ ok: false,
170
+ reason: "No state.json found at implement/no-state — not a valid task directory",
171
+ });
172
+ });
173
+
174
+ it("accepts valid task directories", () => {
175
+ const cwd = makeCwd();
176
+ const taskDir = createTask(cwd, "implement", "Valid source");
177
+ const relative = taskDir.replace(join(cwd, ".pp", "state") + "/", "");
178
+
179
+ expect(validateFromPath(cwd, relative)).toEqual({ ok: true, dir: taskDir });
180
+ });
181
+
182
+ it("allows fromPath resolving to state root and then fails state.json validation", () => {
183
+ const cwd = makeCwd();
184
+ mkdirSync(join(cwd, ".pp", "state"), { recursive: true });
185
+
186
+ expect(validateFromPath(cwd, ".")).toEqual({
187
+ ok: false,
188
+ reason: "No state.json found at . — not a valid task directory",
189
+ });
190
+ });
191
+ });
192
+
193
+ describe("taskName", () => {
194
+ it("returns description from state.json", () => {
195
+ const cwd = makeCwd();
196
+ const taskDir = createTask(cwd, "implement", "My readable task name");
197
+ expect(taskName(taskDir)).toBe("My readable task name");
198
+ });
199
+
200
+ it("falls back to parsed directory name", () => {
201
+ const cwd = makeCwd();
202
+ const taskDir = join(cwd, ".pp", "state", "implement", "123456789012_my-fallback-name");
203
+ mkdirSync(taskDir, { recursive: true });
204
+
205
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
206
+ expect(taskName(taskDir)).toBe("my fallback name");
207
+ expect(errorSpy).toHaveBeenCalled();
208
+ });
209
+ });
210
+
211
+ describe("taskAge", () => {
212
+ it("formats minutes", () => {
213
+ const state: TaskState = {
214
+ phase: "brainstorm",
215
+ step: null,
216
+ reviewCycle: null,
217
+ reviewPass: 0,
218
+ from: null,
219
+ description: "x",
220
+ startedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
221
+ };
222
+ expect(taskAge(state)).toBe("5m");
223
+ });
224
+
225
+ it("formats hours", () => {
226
+ const state: TaskState = {
227
+ phase: "brainstorm",
228
+ step: null,
229
+ reviewCycle: null,
230
+ reviewPass: 0,
231
+ from: null,
232
+ description: "x",
233
+ startedAt: new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString(),
234
+ };
235
+ expect(taskAge(state)).toBe("3h");
236
+ });
237
+
238
+ it("formats days", () => {
239
+ const state: TaskState = {
240
+ phase: "brainstorm",
241
+ step: null,
242
+ reviewCycle: null,
243
+ reviewPass: 0,
244
+ from: null,
245
+ description: "x",
246
+ startedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
247
+ };
248
+ expect(taskAge(state)).toBe("2d");
249
+ });
250
+
251
+ it("returns '?' for empty startedAt", () => {
252
+ const state: TaskState = {
253
+ phase: "brainstorm",
254
+ step: null,
255
+ reviewCycle: null,
256
+ reviewPass: 0,
257
+ from: null,
258
+ description: "x",
259
+ startedAt: "",
260
+ };
261
+ expect(taskAge(state)).toBe("?");
262
+ });
263
+
264
+ it("returns '?' for non-date startedAt", () => {
265
+ const state: TaskState = {
266
+ phase: "brainstorm",
267
+ step: null,
268
+ reviewCycle: null,
269
+ reviewPass: 0,
270
+ from: null,
271
+ description: "x",
272
+ startedAt: "not-a-date",
273
+ };
274
+ expect(taskAge(state)).toBe("?");
275
+ });
276
+
277
+ it("returns '?' for undefined startedAt", () => {
278
+ const state = {
279
+ phase: "brainstorm",
280
+ step: null,
281
+ reviewCycle: null,
282
+ reviewPass: 0,
283
+ from: null,
284
+ description: "x",
285
+ startedAt: undefined,
286
+ } as any;
287
+ expect(taskAge(state)).toBe("?");
288
+ });
289
+ });
290
+
291
+ describe("getActiveTask", () => {
292
+ it("returns single unlocked task for restoration", () => {
293
+ const cwd = makeCwd();
294
+ const taskDir = createTask(cwd, "implement", "Abandoned task");
295
+ const checkSpy = vi.spyOn(lockfile, "checkSync").mockReturnValue(false);
296
+
297
+ const result = getActiveTask(cwd);
298
+
299
+ expect(checkSpy).toHaveBeenCalledWith(join(taskDir, "state.json"), expect.objectContaining({ stale: 600000 }));
300
+ expect(result?.dir).toBe(taskDir);
301
+ });
302
+
303
+ it("returns null when task is locked by another process", () => {
304
+ const cwd = makeCwd();
305
+ createTask(cwd, "implement", "Active elsewhere");
306
+ vi.spyOn(lockfile, "checkSync").mockReturnValue(true);
307
+
308
+ expect(getActiveTask(cwd)).toBeNull();
309
+ });
310
+
311
+ it("returns null when multiple unlocked tasks exist (ambiguous)", () => {
312
+ const cwd = makeCwd();
313
+ createTask(cwd, "implement", "First abandoned");
314
+ createTask(cwd, "debug", "Second abandoned");
315
+ vi.spyOn(lockfile, "checkSync").mockReturnValue(false);
316
+
317
+ expect(getActiveTask(cwd)).toBeNull();
318
+ });
319
+
320
+ it("returns the single unlocked task when others are locked", () => {
321
+ const cwd = makeCwd();
322
+ const lockedTask = createTask(cwd, "implement", "Locked by other");
323
+ const unlockedTask = createTask(cwd, "debug", "Abandoned");
324
+
325
+ vi.spyOn(lockfile, "checkSync").mockImplementation((path: string, _options?: { stale?: number }) => {
326
+ if (path === join(lockedTask, "state.json")) return true;
327
+ return false;
328
+ });
329
+
330
+ const result = getActiveTask(cwd);
331
+ expect(result?.dir).toBe(unlockedTask);
332
+ });
333
+
334
+ it("uses custom lockStale for stale detection", () => {
335
+ const cwd = makeCwd();
336
+ const taskDir = createTask(cwd, "implement", "Stale test");
337
+ const checkSpy = vi.spyOn(lockfile, "checkSync").mockReturnValue(false);
338
+
339
+ getActiveTask(cwd, 1234);
340
+
341
+ expect(checkSpy).toHaveBeenCalledWith(join(taskDir, "state.json"), expect.objectContaining({ stale: 1234 }));
342
+ });
343
+ });
@@ -0,0 +1,237 @@
1
+ import { appendFileSync, readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
2
+ import { join, basename, resolve } from "path";
3
+ import lockfile from "proper-lockfile";
4
+
5
+ function getLockfileFs(): typeof import("fs") | undefined {
6
+ try {
7
+ const fs = require("fs");
8
+ fs.statSync(process.cwd());
9
+ return fs;
10
+ } catch {
11
+ return undefined;
12
+ }
13
+ }
14
+ import type { TimeoutConfig } from "./config.js";
15
+
16
+ export type TaskType = "implement" | "debug" | "brainstorm";
17
+
18
+ export type ImplementPhase = "brainstorm" | "plan" | "implement" | "done";
19
+ export type DebugPhase = "debug" | "plan" | "implement" | "done";
20
+ export type BrainstormPhase = "brainstorm" | "plan" | "implement" | "done";
21
+ export type Phase = ImplementPhase | DebugPhase | BrainstormPhase;
22
+
23
+ export interface TaskState {
24
+ phase: Phase;
25
+ step: string | null;
26
+ reviewCycle: { kind: string; step: string; pass: number } | null;
27
+ reviewPass: number;
28
+ reviewPassByKind?: Record<string, number>;
29
+ modifiedFiles?: string[];
30
+ repoCwd?: string;
31
+ from: string | null;
32
+ description: string;
33
+ startedAt: string;
34
+ }
35
+
36
+ export interface TaskInfo {
37
+ dir: string;
38
+ state: TaskState;
39
+ type: TaskType;
40
+ }
41
+
42
+ function stateDir(cwd: string): string {
43
+ return join(cwd, ".pp", "state");
44
+ }
45
+
46
+ function taskStatePath(taskDir: string): string {
47
+ return join(taskDir, "state.json");
48
+ }
49
+
50
+ export function createTask(cwd: string, type: TaskType, description: string): string {
51
+ const id = crypto.randomUUID().slice(0, 12);
52
+ const safeName = description
53
+ .toLowerCase()
54
+ .replace(/[^a-z0-9]+/g, "-")
55
+ .replace(/^-|-$/g, "")
56
+ .slice(0, 40);
57
+ const dirName = `${id}_${safeName}`;
58
+ const taskDir = join(stateDir(cwd), type, dirName);
59
+
60
+ mkdirSync(taskDir, { recursive: true });
61
+
62
+ const state: TaskState = {
63
+ phase: type === "implement" ? "brainstorm" : type === "debug" ? "debug" : "brainstorm",
64
+ step: "llm_work",
65
+ reviewCycle: null,
66
+ reviewPass: 0,
67
+ from: null,
68
+ description,
69
+ startedAt: new Date().toISOString(),
70
+ };
71
+
72
+ writeFileSync(taskStatePath(taskDir), JSON.stringify(state, null, 2) + "\n", "utf-8");
73
+ return taskDir;
74
+ }
75
+
76
+ export function loadTask(taskDir: string): TaskState {
77
+ const sp = taskStatePath(taskDir);
78
+ const raw = readFileSync(sp, "utf-8");
79
+ try {
80
+ const state = JSON.parse(raw) as TaskState & { phase?: string };
81
+ if (state.phase === "planning") {
82
+ state.phase = "plan";
83
+ }
84
+ return state;
85
+ } catch (err: any) {
86
+ throw new Error(`Failed to parse ${sp}: ${err.message}`);
87
+ }
88
+ }
89
+
90
+ export function saveTask(taskDir: string, state: TaskState): void {
91
+ writeFileSync(taskStatePath(taskDir), JSON.stringify(state, null, 2) + "\n", "utf-8");
92
+ }
93
+
94
+ export function listTasks(cwd: string, type?: TaskType): TaskInfo[] {
95
+ const base = stateDir(cwd);
96
+ if (!existsSync(base)) return [];
97
+
98
+ const types: TaskType[] = type ? [type] : ["implement", "debug", "brainstorm"];
99
+ const results: TaskInfo[] = [];
100
+
101
+ for (const t of types) {
102
+ const typeDir = join(base, t);
103
+ if (!existsSync(typeDir)) continue;
104
+
105
+ for (const entry of readdirSync(typeDir, { withFileTypes: true })) {
106
+ if (!entry.isDirectory()) continue;
107
+ const dir = join(typeDir, entry.name);
108
+ const sp = taskStatePath(dir);
109
+ if (!existsSync(sp)) continue;
110
+
111
+ try {
112
+ const state = loadTask(dir);
113
+ if (state.phase !== "done") {
114
+ results.push({ dir, state, type: t });
115
+ }
116
+ } catch {
117
+ console.error(`[pi-pi] Skipping corrupt task at ${dir}`);
118
+ }
119
+ }
120
+ }
121
+
122
+ results.sort((a, b) => {
123
+ const aTime = a.state.startedAt ? new Date(a.state.startedAt).getTime() : 0;
124
+ const bTime = b.state.startedAt ? new Date(b.state.startedAt).getTime() : 0;
125
+ return bTime - aTime;
126
+ });
127
+
128
+ return results;
129
+ }
130
+
131
+ export async function lockTask(taskDir: string, timeouts: TimeoutConfig): Promise<() => Promise<void>> {
132
+ const sp = taskStatePath(taskDir);
133
+ if (!existsSync(sp)) {
134
+ throw new Error(`Task state file not found: ${sp}`);
135
+ }
136
+ const lockFs = getLockfileFs();
137
+ const release = await lockfile.lock(sp, {
138
+ ...(lockFs && { fs: lockFs }),
139
+ stale: timeouts.lockStale,
140
+ update: timeouts.lockUpdate,
141
+ retries: { retries: 3, minTimeout: 200, maxTimeout: 1000 },
142
+ onCompromised: (err: Error) => {
143
+ console.error(`[pi-pi] Lock compromised for ${sp}: ${err.message}`);
144
+ },
145
+ });
146
+ return release;
147
+ }
148
+
149
+ export function getActiveTask(cwd: string, lockStale?: number): TaskInfo | null {
150
+ const tasks = listTasks(cwd);
151
+ if (tasks.length === 0) return null;
152
+
153
+ const stale = lockStale ?? 600000;
154
+ const unlocked: TaskInfo[] = [];
155
+ for (const task of tasks) {
156
+ try {
157
+ const checkFs = getLockfileFs();
158
+ if (!lockfile.checkSync(taskStatePath(task.dir), { ...(checkFs && { fs: checkFs }), stale })) {
159
+ unlocked.push(task);
160
+ }
161
+ } catch {
162
+ console.error(`[pi-pi] Failed to check lock for ${task.dir}`);
163
+ }
164
+ }
165
+
166
+ if (unlocked.length !== 1) return null;
167
+ return unlocked[0];
168
+ }
169
+
170
+ export function validateFromPath(cwd: string, fromPath: string): { ok: true; dir: string } | { ok: false; reason: string } {
171
+ if (fromPath.includes("..")) {
172
+ return { ok: false, reason: "Path must not contain '..'" };
173
+ }
174
+
175
+ const stateRoot = resolve(stateDir(cwd));
176
+ const resolved = resolve(stateRoot, fromPath);
177
+
178
+ if (resolved !== stateRoot && !resolved.startsWith(stateRoot + "/")) {
179
+ return { ok: false, reason: "Path escapes .pp/state/ directory" };
180
+ }
181
+
182
+ if (!existsSync(resolved)) {
183
+ return { ok: false, reason: `Source task not found: ${fromPath}` };
184
+ }
185
+
186
+ const sp = taskStatePath(resolved);
187
+ if (!existsSync(sp)) {
188
+ return { ok: false, reason: `No state.json found at ${fromPath} — not a valid task directory` };
189
+ }
190
+
191
+ return { ok: true, dir: resolved };
192
+ }
193
+
194
+ export function taskName(taskDir: string): string {
195
+ try {
196
+ const state = loadTask(taskDir);
197
+ let desc = state.description ?? "";
198
+
199
+ if (["implement", "debug", "brainstorm"].includes(desc)) {
200
+ const urPath = join(taskDir, "USER_REQUEST.md");
201
+ if (existsSync(urPath)) {
202
+ const content = readFileSync(urPath, "utf-8");
203
+ const lines = content.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
204
+ const firstContent = lines.find((l) => !l.startsWith("#"));
205
+ if (firstContent) desc = firstContent;
206
+ }
207
+ }
208
+
209
+ if (desc) {
210
+ desc = desc.replace(/\s+/g, " ").trim();
211
+ if (desc.length > 60) desc = desc.slice(0, 57) + "...";
212
+ return desc;
213
+ }
214
+ } catch {
215
+ console.error(`[pi-pi] Failed to read task name from ${taskDir}`);
216
+ }
217
+ const dir = basename(taskDir);
218
+ const match = dir.match(/^\d+_(.+)$/);
219
+ return match ? match[1].replace(/-/g, " ") : dir;
220
+ }
221
+
222
+ export function taskAge(state: TaskState): string {
223
+ const ms = Date.now() - new Date(state.startedAt).getTime();
224
+ if (isNaN(ms)) return "?";
225
+ const minutes = Math.floor(ms / 60000);
226
+ if (minutes < 60) return `${minutes}m`;
227
+ const hours = Math.floor(minutes / 60);
228
+ if (hours < 24) return `${hours}h`;
229
+ return `${Math.floor(hours / 24)}d`;
230
+ }
231
+
232
+ export function appendTaskLog(taskDir: string, fileName: string, entry: Record<string, unknown>): string {
233
+ mkdirSync(taskDir, { recursive: true });
234
+ const filePath = join(taskDir, fileName);
235
+ appendFileSync(filePath, JSON.stringify(entry) + "\n", "utf-8");
236
+ return filePath;
237
+ }
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { validatePlan, validateUserRequest } from "./validate-artifacts.js";
3
+
4
+ describe("validateUserRequest", () => {
5
+ it("rejects placeholder distillation and constraints content", () => {
6
+ const content = `# User Request
7
+ TBD
8
+
9
+ ## Problem
10
+ Real problem statement.
11
+
12
+ ## Constraints
13
+ -
14
+ `;
15
+
16
+ const result = validateUserRequest(content);
17
+ expect(result.ok).toBe(false);
18
+ if (result.ok) return;
19
+ expect(result.errors).toContain(
20
+ "Distillation is missing between # User Request and the first ## section. Expected 1-3 non-empty sentences.",
21
+ );
22
+ expect(result.errors).toContain("Section ## Constraints is empty. Expected non-empty constraints.");
23
+ });
24
+ });
25
+
26
+ describe("validatePlan", () => {
27
+ it("ignores checkbox items outside Checklist section", () => {
28
+ const content = `# Plan
29
+
30
+ ## Scope
31
+ Ship minimal fix.
32
+
33
+ ## Checklist
34
+ - [ ] Implement fix — Done when: tests pass
35
+
36
+ ## Blockers
37
+ - [ ] external dependency not resolved
38
+ `;
39
+
40
+ const result = validatePlan(content);
41
+ expect(result.ok).toBe(true);
42
+ });
43
+
44
+ it("accepts multiline Done when continuation", () => {
45
+ const content = `# Plan
46
+
47
+ ## Scope
48
+ Ship minimal fix.
49
+
50
+ ## Checklist
51
+ - [ ] Implement fix
52
+ Done when: tests pass and docs updated
53
+ `;
54
+
55
+ const result = validatePlan(content);
56
+ expect(result.ok).toBe(true);
57
+ });
58
+
59
+ it("accepts section names with trailing colon or inline text", () => {
60
+ const content = `# Plan
61
+
62
+ ## Scope: Handle panics gracefully
63
+ Ship minimal fix.
64
+
65
+ ## Checklist:
66
+ - [ ] Implement fix — Done when: tests pass
67
+ `;
68
+
69
+ const result = validatePlan(content);
70
+ expect(result.ok).toBe(true);
71
+ });
72
+
73
+ it("rejects placeholder scope", () => {
74
+ const content = `# Plan
75
+
76
+ ## Scope
77
+ ...
78
+
79
+ ## Checklist
80
+ - [ ] Implement fix — Done when: tests pass
81
+ `;
82
+
83
+ const result = validatePlan(content);
84
+ expect(result.ok).toBe(false);
85
+ if (result.ok) return;
86
+ expect(result.errors).toContain("Section ## Scope is empty. Expected 2-4 lines summarizing scope and constraints.");
87
+ });
88
+ });