@selesai/code 0.3.0 → 0.3.1

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 (230) hide show
  1. package/dist/core/diagnostics.d.ts +1 -0
  2. package/dist/core/diagnostics.d.ts.map +1 -1
  3. package/dist/core/diagnostics.js.map +1 -1
  4. package/dist/core/package-manager.d.ts +10 -0
  5. package/dist/core/package-manager.d.ts.map +1 -1
  6. package/dist/core/package-manager.js +93 -8
  7. package/dist/core/package-manager.js.map +1 -1
  8. package/dist/core/package-manager.test.d.ts +2 -0
  9. package/dist/core/package-manager.test.d.ts.map +1 -0
  10. package/dist/core/package-manager.test.js +20 -0
  11. package/dist/core/package-manager.test.js.map +1 -0
  12. package/dist/core/resource-loader.d.ts +3 -0
  13. package/dist/core/resource-loader.d.ts.map +1 -1
  14. package/dist/core/resource-loader.js +10 -0
  15. package/dist/core/resource-loader.js.map +1 -1
  16. package/dist/core/settings-manager.d.ts +1 -0
  17. package/dist/core/settings-manager.d.ts.map +1 -1
  18. package/dist/core/settings-manager.js.map +1 -1
  19. package/dist/extensions/caveman/caveman-instructions.cjs +39 -0
  20. package/dist/extensions/caveman/index.js +116 -0
  21. package/dist/extensions/caveman/package.json +8 -0
  22. package/dist/extensions/caveman/test/extension.test.js +185 -0
  23. package/dist/extensions/caveman/test/helpers.test.js +67 -0
  24. package/dist/extensions/pi-intercom/broker/broker.ts +2 -3
  25. package/dist/extensions/pi-intercom/broker/paths.ts +10 -5
  26. package/dist/extensions/pi-intercom/broker/spawn.ts +2 -3
  27. package/dist/extensions/pi-intercom/config.ts +2 -2
  28. package/dist/extensions/pi-intercom/index.ts +2 -2
  29. package/dist/extensions/pi-intercom/package.json +2 -2
  30. package/dist/extensions/pi-intercom/ui/compose.ts +3 -3
  31. package/dist/extensions/pi-intercom/ui/inline-message.ts +3 -3
  32. package/dist/extensions/pi-intercom/ui/session-list.ts +3 -3
  33. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +2 -1
  34. package/dist/extensions/pi-powerline-footer/index.ts +9 -16
  35. package/dist/extensions/pi-powerline-footer/welcome.ts +23 -16
  36. package/dist/extensions/pi-powerline-footer/working-vibes.ts +2 -9
  37. package/dist/extensions/pi-rewind-hook/index.ts +1 -1
  38. package/dist/extensions/pi-rewind-hook/package.json +1 -1
  39. package/dist/extensions/pi-subagents/README.md +224 -52
  40. package/dist/extensions/pi-subagents/install.mjs +93 -0
  41. package/dist/extensions/pi-subagents/package.json +4 -5
  42. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  43. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  44. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +76 -13
  45. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +179 -2
  46. package/dist/extensions/pi-subagents/src/agents/agent-memory.ts +254 -0
  47. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +11 -0
  48. package/dist/extensions/pi-subagents/src/agents/agents.ts +193 -19
  49. package/dist/extensions/pi-subagents/src/agents/chain-serializer.ts +27 -2
  50. package/dist/extensions/pi-subagents/src/extension/config.ts +27 -4
  51. package/dist/extensions/pi-subagents/src/extension/doctor.ts +1 -7
  52. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -2
  53. package/dist/extensions/pi-subagents/src/extension/index.ts +73 -71
  54. package/dist/extensions/pi-subagents/src/extension/rpc.ts +369 -0
  55. package/dist/extensions/pi-subagents/src/extension/schemas.ts +54 -10
  56. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +200 -0
  57. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +21 -253
  58. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +519 -0
  59. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +1 -1
  60. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +195 -105
  61. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +88 -2
  62. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +67 -10
  63. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +34 -4
  64. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +166 -0
  65. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +156 -1
  66. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +515 -0
  67. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +161 -44
  68. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +1 -2
  69. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +3 -2
  70. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +167 -6
  71. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +514 -0
  72. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +28 -1
  73. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +873 -204
  74. package/dist/extensions/pi-subagents/src/runs/background/wait.ts +353 -0
  75. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +123 -27
  76. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +174 -70
  77. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +569 -81
  78. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +45 -22
  79. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +3 -1
  80. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +2 -2
  81. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +172 -21
  82. package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +128 -0
  83. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +89 -0
  84. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +50 -1
  85. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +35 -4
  86. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +63 -45
  87. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +2 -0
  88. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +125 -5
  89. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +74 -0
  90. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +52 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +28 -5
  92. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +16 -1
  93. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +15 -2
  94. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +212 -0
  95. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +133 -22
  96. package/dist/extensions/pi-subagents/src/shared/settings.ts +3 -1
  97. package/dist/extensions/pi-subagents/src/shared/types.ts +197 -4
  98. package/dist/extensions/pi-subagents/src/shared/utils.ts +100 -15
  99. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +330 -0
  100. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +135 -4
  101. package/dist/extensions/pi-subagents/src/tui/render.ts +32 -12
  102. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +94 -0
  103. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2772 -0
  104. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +1107 -0
  105. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +463 -0
  106. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +274 -0
  107. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1535 -0
  108. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +242 -0
  109. package/dist/extensions/pi-subagents/test/integration/doctor-executor.test.ts +128 -0
  110. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +252 -0
  111. package/dist/extensions/pi-subagents/test/integration/foreground-result-size.test.ts +188 -0
  112. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +1306 -0
  113. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +1033 -0
  114. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +397 -0
  115. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +583 -0
  116. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +691 -0
  117. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +892 -0
  118. package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +62 -0
  119. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1824 -0
  120. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1284 -0
  121. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +127 -0
  122. package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +299 -0
  123. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +37 -0
  124. package/dist/extensions/pi-subagents/test/support/helpers.ts +189 -0
  125. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +335 -0
  126. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +129 -0
  127. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +184 -0
  128. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +285 -0
  129. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +15 -0
  130. package/dist/extensions/pi-subagents/test/support/ts-loader.mjs +108 -0
  131. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +421 -0
  132. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +213 -0
  133. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +343 -0
  134. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +1199 -0
  135. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +556 -0
  136. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +420 -0
  137. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +579 -0
  138. package/dist/extensions/pi-subagents/test/unit/agent-scope.test.ts +20 -0
  139. package/dist/extensions/pi-subagents/test/unit/agent-selection.test.ts +88 -0
  140. package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +24 -0
  141. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +86 -0
  142. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +221 -0
  143. package/dist/extensions/pi-subagents/test/unit/async-permission-session.test.ts +98 -0
  144. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +285 -0
  145. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +112 -0
  146. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +206 -0
  147. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +159 -0
  148. package/dist/extensions/pi-subagents/test/unit/chain-serializer.test.ts +229 -0
  149. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +225 -0
  150. package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +101 -0
  151. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +228 -0
  152. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +36 -0
  153. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +152 -0
  154. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +72 -0
  155. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +327 -0
  156. package/dist/extensions/pi-subagents/test/unit/control-notices.test.ts +161 -0
  157. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +159 -0
  158. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +217 -0
  159. package/dist/extensions/pi-subagents/test/unit/extra-agent-dirs.test.ts +88 -0
  160. package/dist/extensions/pi-subagents/test/unit/file-coalescer.test.ts +68 -0
  161. package/dist/extensions/pi-subagents/test/unit/foreground-tool-call-compaction.test.ts +78 -0
  162. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +415 -0
  163. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +126 -0
  164. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +271 -0
  165. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +173 -0
  166. package/dist/extensions/pi-subagents/test/unit/jsonl-writer.test.ts +119 -0
  167. package/dist/extensions/pi-subagents/test/unit/live-async-resume.test.ts +170 -0
  168. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +346 -0
  169. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +62 -0
  170. package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +87 -0
  171. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +145 -0
  172. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +162 -0
  173. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +470 -0
  174. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +329 -0
  175. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +319 -0
  176. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +70 -0
  177. package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +247 -0
  178. package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +113 -0
  179. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +94 -0
  180. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +803 -0
  181. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +209 -0
  182. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +238 -0
  183. package/dist/extensions/pi-subagents/test/unit/proactive-skills.test.ts +118 -0
  184. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +299 -0
  185. package/dist/extensions/pi-subagents/test/unit/prompt-template-bridge.test.ts +408 -0
  186. package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +134 -0
  187. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +257 -0
  188. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +209 -0
  189. package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +226 -0
  190. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +294 -0
  191. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +147 -0
  192. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +997 -0
  193. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +478 -0
  194. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +562 -0
  195. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +213 -0
  196. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +438 -0
  197. package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +54 -0
  198. package/dist/extensions/pi-subagents/test/unit/slash-chain-groups.test.ts +464 -0
  199. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +255 -0
  200. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +19 -0
  201. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +245 -0
  202. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +517 -0
  203. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +72 -0
  204. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +52 -0
  205. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +237 -0
  206. package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +60 -0
  207. package/dist/extensions/pi-subagents/test/unit/ts-loader.test.ts +33 -0
  208. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +202 -0
  209. package/dist/extensions/pi-subagents/test/unit/types-fork-preamble.test.ts +23 -0
  210. package/dist/extensions/pi-subagents/test/unit/wait.test.ts +398 -0
  211. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +88 -0
  212. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +26 -0
  213. package/dist/extensions/pi-subagents/test/unit/workflow-graph.test.ts +134 -0
  214. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +479 -0
  215. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +5 -2
  216. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +6 -4
  217. package/dist/main.d.ts.map +1 -1
  218. package/dist/main.js +4 -0
  219. package/dist/main.js.map +1 -1
  220. package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
  221. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  222. package/dist/modes/interactive/components/first-time-setup.js +19 -5
  223. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  224. package/dist/skills/caveman/SKILL.md +49 -0
  225. package/dist/skills/handoff-text/SKILL.md +13 -0
  226. package/docs/docs.json +4 -0
  227. package/docs/extensions.md +2 -0
  228. package/docs/settings.md +15 -0
  229. package/docs/shared-host-extensions.md +109 -0
  230. package/package.json +1 -1
@@ -0,0 +1,479 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawnSync } from "node:child_process";
3
+ import * as fs from "node:fs";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ import { describe, it } from "node:test";
7
+ import {
8
+ cleanupWorktrees,
9
+ createWorktrees,
10
+ diffWorktrees,
11
+ findWorktreeTaskCwdConflict,
12
+ formatWorktreeDiffSummary,
13
+ resolveExpectedWorktreeAgentCwd,
14
+ type WorktreeSetup,
15
+ } from "../../src/runs/shared/worktree.ts";
16
+
17
+ function git(cwd: string, args: string[]): string {
18
+ const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8" });
19
+ if (result.status !== 0) {
20
+ const message = result.stderr.trim() || result.stdout.trim() || `git ${args.join(" ")} failed`;
21
+ throw new Error(message);
22
+ }
23
+ return result.stdout.trim();
24
+ }
25
+
26
+ function createRepo(prefix: string): string {
27
+ const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
28
+ git(repoDir, ["init"]);
29
+ git(repoDir, ["config", "user.email", "tests@example.com"]);
30
+ git(repoDir, ["config", "user.name", "Worktree Tests"]);
31
+ fs.writeFileSync(path.join(repoDir, ".gitignore"), "node_modules/\n", "utf-8");
32
+ fs.writeFileSync(path.join(repoDir, "tracked.txt"), "initial\n", "utf-8");
33
+ git(repoDir, ["add", "-A"]);
34
+ git(repoDir, ["commit", "-m", "initial commit"]);
35
+ return repoDir;
36
+ }
37
+
38
+ function cleanupRepo(repoDir: string): void {
39
+ try { fs.rmSync(repoDir, { recursive: true, force: true }); } catch {}
40
+ }
41
+
42
+ function createHookScript(_repoDir: string, fileName: string, source: string): string {
43
+ const hooksDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-worktree-hook-script-"));
44
+ const hookPath = path.join(hooksDir, fileName);
45
+ fs.writeFileSync(hookPath, `#!/usr/bin/env node\n${source}\n`, "utf-8");
46
+ fs.chmodSync(hookPath, 0o755);
47
+ return hookPath;
48
+ }
49
+
50
+ const hookScriptSkip = process.platform === "win32"
51
+ ? "Hook script execution differs on Windows CI environments."
52
+ : undefined;
53
+
54
+ describe("worktree", () => {
55
+ it("createWorktrees returns expected structure", () => {
56
+ const repoDir = createRepo("pi-worktree-structure-");
57
+ let setup: WorktreeSetup | undefined;
58
+ try {
59
+ setup = createWorktrees(repoDir, "structure", 2);
60
+ assert.equal(setup.worktrees.length, 2);
61
+ assert.equal(setup.cwd, git(repoDir, ["rev-parse", "--show-toplevel"]));
62
+ for (let i = 0; i < setup.worktrees.length; i++) {
63
+ const worktree = setup.worktrees[i]!;
64
+ assert.equal(worktree.branch, `pi-parallel-structure-${i}`);
65
+ assert.equal(worktree.index, i);
66
+ assert.equal(worktree.agentCwd, worktree.path);
67
+ assert.equal(worktree.nodeModulesLinked, false);
68
+ assert.deepEqual(worktree.syntheticPaths, []);
69
+ assert.ok(fs.existsSync(worktree.path), `worktree path missing: ${worktree.path}`);
70
+ }
71
+ } finally {
72
+ if (setup) cleanupWorktrees(setup);
73
+ cleanupRepo(repoDir);
74
+ }
75
+ });
76
+
77
+ it("createWorktrees maps subdirectory cwd to each agentCwd", () => {
78
+ const repoDir = createRepo("pi-worktree-subdir-");
79
+ const nestedDir = path.join(repoDir, "packages", "app");
80
+ fs.mkdirSync(nestedDir, { recursive: true });
81
+ fs.writeFileSync(path.join(nestedDir, "index.ts"), "export const value = 1;\n", "utf-8");
82
+ git(repoDir, ["add", "-A"]);
83
+ git(repoDir, ["commit", "-m", "add nested dir"]);
84
+
85
+ let setup: WorktreeSetup | undefined;
86
+ try {
87
+ setup = createWorktrees(nestedDir, "subdir", 1);
88
+ assert.equal(setup.worktrees[0]!.agentCwd, path.join(setup.worktrees[0]!.path, "packages", "app"));
89
+ } finally {
90
+ if (setup) cleanupWorktrees(setup);
91
+ cleanupRepo(repoDir);
92
+ }
93
+ });
94
+
95
+ it("previews expected worktree agent cwd for repository subdirectories", () => {
96
+ const repoDir = createRepo("pi-worktree-preview-");
97
+ const nestedDir = path.join(repoDir, "packages", "app");
98
+ fs.mkdirSync(nestedDir, { recursive: true });
99
+ fs.writeFileSync(path.join(nestedDir, "index.ts"), "export const value = 1;\n", "utf-8");
100
+ git(repoDir, ["add", "-A"]);
101
+ git(repoDir, ["commit", "-m", "add nested dir"]);
102
+
103
+ try {
104
+ assert.equal(
105
+ resolveExpectedWorktreeAgentCwd(nestedDir, "preview", 2),
106
+ path.join(os.tmpdir(), "pi-worktree-preview-2", "packages", "app"),
107
+ );
108
+ } finally {
109
+ cleanupRepo(repoDir);
110
+ }
111
+ });
112
+
113
+ it("creates worktrees under a configured base directory", () => {
114
+ const repoDir = createRepo("pi-worktree-base-dir-");
115
+ const baseDir = path.join(os.tmpdir(), `pi-worktree-base-${Date.now().toString(36)}`, "nested");
116
+ let setup: WorktreeSetup | undefined;
117
+ try {
118
+ setup = createWorktrees(repoDir, "base-dir", 1, { baseDir });
119
+ assert.equal(setup.worktrees[0]!.path, path.join(baseDir, "pi-worktree-base-dir-0"));
120
+ assert.ok(fs.existsSync(baseDir), "configured base directory should be created");
121
+ } finally {
122
+ if (setup) cleanupWorktrees(setup);
123
+ cleanupRepo(repoDir);
124
+ fs.rmSync(path.dirname(baseDir), { recursive: true, force: true });
125
+ }
126
+ });
127
+
128
+ it("uses SELESAI_SUBAGENTS_WORKTREE_DIR when no base directory is configured", () => {
129
+ const repoDir = createRepo("pi-worktree-env-base-dir-");
130
+ const previous = process.env.SELESAI_SUBAGENTS_WORKTREE_DIR;
131
+ const baseDir = path.join(os.tmpdir(), `pi-worktree-env-base-${Date.now().toString(36)}`);
132
+ let setup: WorktreeSetup | undefined;
133
+ try {
134
+ process.env.SELESAI_SUBAGENTS_WORKTREE_DIR = baseDir;
135
+ setup = createWorktrees(repoDir, "env-base-dir", 1);
136
+ assert.equal(setup.worktrees[0]!.path, path.join(baseDir, "pi-worktree-env-base-dir-0"));
137
+ } finally {
138
+ if (setup) cleanupWorktrees(setup);
139
+ if (previous === undefined) {
140
+ delete process.env.SELESAI_SUBAGENTS_WORKTREE_DIR;
141
+ } else {
142
+ process.env.SELESAI_SUBAGENTS_WORKTREE_DIR = previous;
143
+ }
144
+ cleanupRepo(repoDir);
145
+ fs.rmSync(baseDir, { recursive: true, force: true });
146
+ }
147
+ });
148
+
149
+ it("createWorktrees rejects dirty repositories", () => {
150
+ const repoDir = createRepo("pi-worktree-dirty-");
151
+ try {
152
+ fs.writeFileSync(path.join(repoDir, "tracked.txt"), "dirty\n", "utf-8");
153
+ assert.throws(
154
+ () => createWorktrees(repoDir, "dirty", 1),
155
+ /worktree isolation requires a clean git working tree/i,
156
+ );
157
+ } finally {
158
+ cleanupRepo(repoDir);
159
+ }
160
+ });
161
+
162
+ it("findWorktreeTaskCwdConflict allows omitted or matching task cwd values", () => {
163
+ const sharedCwd = path.join("/tmp", "repo");
164
+ assert.equal(
165
+ findWorktreeTaskCwdConflict(
166
+ [
167
+ { agent: "worker-a" },
168
+ { agent: "worker-b", cwd: sharedCwd },
169
+ ],
170
+ sharedCwd,
171
+ ),
172
+ undefined,
173
+ );
174
+ });
175
+
176
+ it("findWorktreeTaskCwdConflict treats relative task cwd values as relative to the shared cwd", () => {
177
+ const sharedCwd = path.join("/tmp", "repo");
178
+ assert.equal(
179
+ findWorktreeTaskCwdConflict(
180
+ [{ agent: "worker-a", cwd: "." }],
181
+ sharedCwd,
182
+ ),
183
+ undefined,
184
+ );
185
+ });
186
+
187
+ it("findWorktreeTaskCwdConflict returns the first conflicting task cwd", () => {
188
+ const sharedCwd = path.join("/tmp", "repo");
189
+ const conflict = findWorktreeTaskCwdConflict(
190
+ [
191
+ { agent: "worker-a", cwd: sharedCwd },
192
+ { agent: "worker-b", cwd: path.join(sharedCwd, "packages", "app") },
193
+ ],
194
+ sharedCwd,
195
+ );
196
+ assert.deepEqual(conflict, {
197
+ index: 1,
198
+ agent: "worker-b",
199
+ cwd: path.join(sharedCwd, "packages", "app"),
200
+ });
201
+ });
202
+
203
+ it("diffWorktrees captures committed, modified, and new files without staging the node_modules symlink", () => {
204
+ const repoDir = createRepo("pi-worktree-diff-");
205
+ const nodeModulesDir = path.join(repoDir, "node_modules");
206
+ fs.mkdirSync(nodeModulesDir, { recursive: true });
207
+ fs.writeFileSync(path.join(nodeModulesDir, "fixture.txt"), "fixture\n", "utf-8");
208
+
209
+ let setup: WorktreeSetup | undefined;
210
+ try {
211
+ setup = createWorktrees(repoDir, "diff", 1);
212
+ const worktree = setup.worktrees[0]!;
213
+ fs.writeFileSync(path.join(worktree.path, "committed.ts"), "export const committed = true;\n", "utf-8");
214
+ git(worktree.path, ["add", "committed.ts"]);
215
+ git(worktree.path, ["commit", "-m", "committed change"]);
216
+ fs.writeFileSync(path.join(worktree.path, "tracked.txt"), "modified\n", "utf-8");
217
+ fs.writeFileSync(path.join(worktree.path, "new-file.ts"), "export const added = true;\n", "utf-8");
218
+
219
+ const diffsDir = path.join(repoDir, "artifacts", "worktree-diffs");
220
+ const diffs = diffWorktrees(setup, ["agent-a"], diffsDir);
221
+ assert.equal(diffs.length, 1);
222
+ assert.equal(diffs[0]!.agent, "agent-a");
223
+ assert.equal(diffs[0]!.filesChanged, 3, `expected 3 files, got ${diffs[0]!.filesChanged}`);
224
+ assert.ok(diffs[0]!.insertions > 0, "expected insertions > 0");
225
+ assert.ok(fs.existsSync(diffs[0]!.patchPath), "expected patch file to exist");
226
+
227
+ const patch = fs.readFileSync(diffs[0]!.patchPath, "utf-8");
228
+ assert.match(patch, /committed\.ts/);
229
+ assert.match(patch, /tracked\.txt/);
230
+ assert.match(patch, /new-file\.ts/);
231
+ assert.doesNotMatch(patch, /diff --git a\/node_modules b\/node_modules/);
232
+
233
+ const summary = formatWorktreeDiffSummary(diffs);
234
+ assert.match(summary, /=== Worktree Changes ===/);
235
+ assert.match(summary, /Full patches:/);
236
+ } finally {
237
+ if (setup) cleanupWorktrees(setup);
238
+ cleanupRepo(repoDir);
239
+ }
240
+ });
241
+
242
+ it("cleanupWorktrees removes worktrees and branches", () => {
243
+ const repoDir = createRepo("pi-worktree-cleanup-");
244
+ let setup: WorktreeSetup | undefined;
245
+ try {
246
+ setup = createWorktrees(repoDir, "cleanup", 2);
247
+ const worktreePaths = setup.worktrees.map((worktree) => worktree.path);
248
+ const branches = setup.worktrees.map((worktree) => worktree.branch);
249
+ cleanupWorktrees(setup);
250
+ setup = undefined;
251
+
252
+ for (const worktreePath of worktreePaths) {
253
+ assert.equal(fs.existsSync(worktreePath), false, `worktree path still exists: ${worktreePath}`);
254
+ }
255
+ for (const branch of branches) {
256
+ const branchResult = git(repoDir, ["branch", "--list", branch]);
257
+ assert.equal(branchResult.trim(), "", `branch still exists: ${branch}`);
258
+ }
259
+ } finally {
260
+ if (setup) cleanupWorktrees(setup);
261
+ cleanupRepo(repoDir);
262
+ }
263
+ });
264
+
265
+ it("createWorktrees creates node_modules symlink when node_modules exists", {
266
+ skip: process.platform === "win32" ? "Symlink behavior differs on Windows CI environments." : undefined,
267
+ }, () => {
268
+ const repoDir = createRepo("pi-worktree-node-modules-");
269
+ const nodeModulesDir = path.join(repoDir, "node_modules");
270
+ fs.mkdirSync(nodeModulesDir, { recursive: true });
271
+ fs.writeFileSync(path.join(nodeModulesDir, "fixture.txt"), "fixture\n", "utf-8");
272
+
273
+ let setup: WorktreeSetup | undefined;
274
+ try {
275
+ setup = createWorktrees(repoDir, "node-modules", 1);
276
+ const symlinkPath = path.join(setup.worktrees[0]!.path, "node_modules");
277
+ assert.equal(setup.worktrees[0]!.nodeModulesLinked, true);
278
+ assert.deepEqual(setup.worktrees[0]!.syntheticPaths, ["node_modules"]);
279
+ assert.ok(fs.existsSync(symlinkPath), "node_modules link should exist");
280
+ assert.equal(fs.lstatSync(symlinkPath).isSymbolicLink(), true, "node_modules should be a symlink");
281
+ assert.equal(fs.realpathSync(symlinkPath), fs.realpathSync(nodeModulesDir));
282
+ } finally {
283
+ if (setup) cleanupWorktrees(setup);
284
+ cleanupRepo(repoDir);
285
+ }
286
+ });
287
+
288
+ it("diffWorktrees preserves a tracked node_modules symlink", {
289
+ skip: process.platform === "win32" ? "Symlink behavior differs on Windows CI environments." : undefined,
290
+ }, () => {
291
+ const repoDir = createRepo("pi-worktree-tracked-node-modules-");
292
+ const vendorDir = path.join(repoDir, "vendor-modules");
293
+ fs.mkdirSync(vendorDir, { recursive: true });
294
+ fs.writeFileSync(path.join(vendorDir, "fixture.txt"), "fixture\n", "utf-8");
295
+ fs.symlinkSync("vendor-modules", path.join(repoDir, "node_modules"));
296
+ git(repoDir, ["add", "vendor-modules", "-f", "node_modules"]);
297
+ git(repoDir, ["commit", "-m", "track node_modules symlink"]);
298
+
299
+ let setup: WorktreeSetup | undefined;
300
+ try {
301
+ setup = createWorktrees(repoDir, "tracked-node-modules", 1);
302
+ assert.equal(setup.worktrees[0]!.nodeModulesLinked, false);
303
+ assert.deepEqual(setup.worktrees[0]!.syntheticPaths, []);
304
+ fs.writeFileSync(path.join(setup.worktrees[0]!.path, "tracked.txt"), "modified\n", "utf-8");
305
+
306
+ const diffsDir = path.join(repoDir, "artifacts", "tracked-node-modules-diffs");
307
+ const diffs = diffWorktrees(setup, ["agent-a"], diffsDir);
308
+ const patch = fs.readFileSync(diffs[0]!.patchPath, "utf-8");
309
+ assert.doesNotMatch(patch, /diff --git a\/node_modules b\/node_modules/);
310
+ assert.equal(fs.lstatSync(path.join(setup.worktrees[0]!.path, "node_modules")).isSymbolicLink(), true);
311
+ } finally {
312
+ if (setup) cleanupWorktrees(setup);
313
+ cleanupRepo(repoDir);
314
+ }
315
+ });
316
+
317
+ it("runs a repo-relative worktree setup hook and records synthetic paths", { skip: hookScriptSkip }, () => {
318
+ const repoDir = createRepo("pi-worktree-hook-relative-");
319
+ const hookPath = createHookScript(repoDir, "setup-hook.mjs", `
320
+ import * as fs from "node:fs";
321
+ import * as path from "node:path";
322
+ const payload = JSON.parse(fs.readFileSync(0, "utf-8"));
323
+ fs.mkdirSync(path.join(payload.worktreePath, ".venv"), { recursive: true });
324
+ fs.writeFileSync(path.join(payload.worktreePath, ".venv", "pyvenv.cfg"), "home=/tmp\\n", "utf-8");
325
+ process.stdout.write(JSON.stringify({ syntheticPaths: [".venv"] }));
326
+ `);
327
+
328
+ let setup: WorktreeSetup | undefined;
329
+ try {
330
+ setup = createWorktrees(repoDir, "hook-relative", 1, {
331
+ setupHook: { hookPath: path.relative(repoDir, hookPath) },
332
+ });
333
+ assert.ok(setup.worktrees[0]!.syntheticPaths.includes(".venv"));
334
+ } finally {
335
+ if (setup) cleanupWorktrees(setup);
336
+ cleanupRepo(repoDir);
337
+ }
338
+ });
339
+
340
+ it("runs an absolute worktree setup hook path", { skip: hookScriptSkip }, () => {
341
+ const repoDir = createRepo("pi-worktree-hook-absolute-");
342
+ const hookPath = createHookScript(repoDir, "setup-hook.mjs", `
343
+ import * as fs from "node:fs";
344
+ JSON.parse(fs.readFileSync(0, "utf-8"));
345
+ process.stdout.write(JSON.stringify({ syntheticPaths: [] }));
346
+ `);
347
+
348
+ let setup: WorktreeSetup | undefined;
349
+ try {
350
+ setup = createWorktrees(repoDir, "hook-absolute", 1, {
351
+ setupHook: { hookPath },
352
+ });
353
+ assert.equal(setup.worktrees.length, 1);
354
+ } finally {
355
+ if (setup) cleanupWorktrees(setup);
356
+ cleanupRepo(repoDir);
357
+ }
358
+ });
359
+
360
+ it("rejects bare command names for worktree setup hooks", () => {
361
+ const repoDir = createRepo("pi-worktree-hook-bare-");
362
+ try {
363
+ assert.throws(
364
+ () => createWorktrees(repoDir, "hook-bare", 1, { setupHook: { hookPath: "node" } }),
365
+ /worktree setup hook must be an absolute path or a repo-relative path/i,
366
+ );
367
+ } finally {
368
+ cleanupRepo(repoDir);
369
+ }
370
+ });
371
+
372
+ it("rejects tracked synthetic paths from hook output", { skip: hookScriptSkip }, () => {
373
+ const repoDir = createRepo("pi-worktree-hook-tracked-");
374
+ const hookPath = createHookScript(repoDir, "tracked-hook.mjs", `
375
+ import * as fs from "node:fs";
376
+ JSON.parse(fs.readFileSync(0, "utf-8"));
377
+ process.stdout.write(JSON.stringify({ syntheticPaths: ["tracked.txt"] }));
378
+ `);
379
+ const runId = `hook-tracked-${Date.now().toString(36)}`;
380
+ try {
381
+ assert.throws(
382
+ () => createWorktrees(repoDir, runId, 1, { setupHook: { hookPath: path.relative(repoDir, hookPath) } }),
383
+ /cannot mark tracked paths as synthetic/i,
384
+ );
385
+ } finally {
386
+ cleanupRepo(repoDir);
387
+ }
388
+ });
389
+
390
+ it("rejects absolute synthetic paths from hook output", { skip: hookScriptSkip }, () => {
391
+ const repoDir = createRepo("pi-worktree-hook-absolute-synthetic-");
392
+ const hookPath = createHookScript(repoDir, "absolute-path-hook.mjs", `
393
+ import * as fs from "node:fs";
394
+ const payload = JSON.parse(fs.readFileSync(0, "utf-8"));
395
+ process.stdout.write(JSON.stringify({ syntheticPaths: [payload.worktreePath + "/.venv"] }));
396
+ `);
397
+ const runId = `hook-absolute-synthetic-${Date.now().toString(36)}`;
398
+ try {
399
+ assert.throws(
400
+ () => createWorktrees(repoDir, runId, 1, { setupHook: { hookPath: path.relative(repoDir, hookPath) } }),
401
+ /synthetic path must be relative/i,
402
+ );
403
+ } finally {
404
+ cleanupRepo(repoDir);
405
+ }
406
+ });
407
+
408
+ it("excludes hook-created synthetic files from captured patch output", { skip: hookScriptSkip }, () => {
409
+ const repoDir = createRepo("pi-worktree-hook-diff-");
410
+ const hookPath = createHookScript(repoDir, "setup-copy-hook.mjs", `
411
+ import * as fs from "node:fs";
412
+ import * as path from "node:path";
413
+ const payload = JSON.parse(fs.readFileSync(0, "utf-8"));
414
+ fs.writeFileSync(path.join(payload.worktreePath, ".env.local"), "TOKEN=secret\\n", "utf-8");
415
+ process.stdout.write(JSON.stringify({ syntheticPaths: [".env.local"] }));
416
+ `);
417
+
418
+ let setup: WorktreeSetup | undefined;
419
+ try {
420
+ setup = createWorktrees(repoDir, "hook-diff", 1, {
421
+ setupHook: { hookPath: path.relative(repoDir, hookPath) },
422
+ });
423
+ fs.writeFileSync(path.join(setup.worktrees[0]!.path, "tracked.txt"), "modified-by-agent\n", "utf-8");
424
+ const diffs = diffWorktrees(setup, ["agent-a"], path.join(repoDir, "artifacts", "hook-diff"));
425
+ const patch = fs.readFileSync(diffs[0]!.patchPath, "utf-8");
426
+ assert.match(patch, /tracked\.txt/);
427
+ assert.doesNotMatch(patch, /\.env\.local/);
428
+ } finally {
429
+ if (setup) cleanupWorktrees(setup);
430
+ cleanupRepo(repoDir);
431
+ }
432
+ });
433
+
434
+ it("cleans up created worktrees when a later hook setup fails", { skip: hookScriptSkip }, () => {
435
+ const repoDir = createRepo("pi-worktree-hook-cleanup-");
436
+ const runId = `hook-cleanup-${Date.now().toString(36)}`;
437
+ const hookPath = createHookScript(repoDir, "flaky-hook.mjs", `
438
+ import * as fs from "node:fs";
439
+ const payload = JSON.parse(fs.readFileSync(0, "utf-8"));
440
+ if (payload.index === 1) {
441
+ console.error("intentional failure");
442
+ process.exit(1);
443
+ }
444
+ process.stdout.write(JSON.stringify({ syntheticPaths: [] }));
445
+ `);
446
+ try {
447
+ assert.throws(
448
+ () => createWorktrees(repoDir, runId, 2, { setupHook: { hookPath: path.relative(repoDir, hookPath) } }),
449
+ /worktree setup hook failed with exit code 1/i,
450
+ );
451
+ const branchList = git(repoDir, ["branch", "--list", `pi-parallel-${runId}-*`]);
452
+ assert.equal(branchList.trim(), "", "temporary branches should be cleaned up after setup failure");
453
+ } finally {
454
+ cleanupRepo(repoDir);
455
+ }
456
+ });
457
+
458
+ it("fails when the hook exceeds the configured timeout", { skip: hookScriptSkip }, () => {
459
+ const repoDir = createRepo("pi-worktree-hook-timeout-");
460
+ const hookPath = createHookScript(repoDir, "slow-hook.mjs", `
461
+ import * as fs from "node:fs";
462
+ JSON.parse(fs.readFileSync(0, "utf-8"));
463
+ setTimeout(() => {
464
+ process.stdout.write(JSON.stringify({ syntheticPaths: [] }));
465
+ }, 1000);
466
+ `);
467
+ const runId = `hook-timeout-${Date.now().toString(36)}`;
468
+ try {
469
+ assert.throws(
470
+ () => createWorktrees(repoDir, runId, 1, {
471
+ setupHook: { hookPath: path.relative(repoDir, hookPath), timeoutMs: 50 },
472
+ }),
473
+ /timed out/i,
474
+ );
475
+ } finally {
476
+ cleanupRepo(repoDir);
477
+ }
478
+ });
479
+ });
@@ -1,6 +1,7 @@
1
1
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { getAgentDir } from '@selesai/code';
4
5
 
5
6
  export type ChangelogEntry = {
6
7
  version: string;
@@ -64,8 +65,10 @@ function defaultPackageRoot() {
64
65
  }
65
66
 
66
67
  function defaultStatePath() {
67
- const homeDir = process.env.USERPROFILE ?? process.env.HOME ?? '';
68
- return path.join(homeDir, '.pi', 'agent', 'extensions', 'pi-web-agent', 'state.json');
68
+ // ponytail: route through host getAgentDir() so the config dir respects the
69
+ // active CONFIG_DIR_NAME (e.g. .selesai). Ceiling: state layout assumed fixed;
70
+ // upgrade path = upstream pi-web-agent accepting a config-dir resolver.
71
+ return path.join(getAgentDir(), 'extensions', 'pi-web-agent', 'state.json');
69
72
  }
70
73
 
71
74
  async function readPackageVersion(packageRoot: string): Promise<string | undefined> {
@@ -1,6 +1,6 @@
1
1
  import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { getAgentDir } from '@selesai/code';
3
+ import { CONFIG_DIR_NAME, getAgentDir } from '@selesai/code';
4
4
  import {
5
5
  DEFAULT_BACKEND_CONFIG,
6
6
  extractBackendConfigOverride,
@@ -46,15 +46,17 @@ export function getPresentationConfigPaths(options: PresentationConfigStoreOptio
46
46
  // CONFIG_DIR_NAME (e.g. .selesai) instead of upstream's hardcoded .pi. Ceiling:
47
47
  // if selesai changes the extensions config layout, update here; upgrade path =
48
48
  // upstream pi-web-agent accepting a config-dir resolver.
49
- const homeDir = options.homeDir ?? process.env.USERPROFILE ?? process.env.HOME ?? '';
50
49
  const projectDir = options.projectDir ?? process.cwd();
51
50
  const agentDir = getAgentDir();
52
- // agentDir is already <homeDir>/<CONFIG_DIR_NAME>/agent; derive the home-relative config dir.
53
51
  const globalConfigRoot = path.join(agentDir, 'extensions', 'pi-web-agent', 'config.json');
54
52
 
55
53
  return {
56
54
  globalPath: globalConfigRoot,
57
- projectPath: path.join(projectDir, '.pi', 'extensions', 'pi-web-agent', 'config.json')
55
+ // ponytail: project config dir is CONFIG_DIR_NAME (e.g. .selesai), not the
56
+ // upstream hardcoded .pi. Ceiling: assumes the project-local extension
57
+ // config layout matches the global one; upgrade path = upstream accepting a
58
+ // config-dir resolver for project paths too.
59
+ projectPath: path.join(projectDir, CONFIG_DIR_NAME, 'extensions', 'pi-web-agent', 'config.json')
58
60
  };
59
61
  }
60
62