@opengsd/gsd-pi 1.0.2-dev.50223bc → 1.0.2-dev.5961fbf

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 (251) hide show
  1. package/dist/resource-loader.d.ts +5 -0
  2. package/dist/resource-loader.js +24 -8
  3. package/dist/resources/.managed-resources-content-hash +1 -1
  4. package/dist/resources/extensions/gsd/auto/loop.js +19 -0
  5. package/dist/resources/extensions/gsd/auto/phases.js +1 -1
  6. package/dist/resources/extensions/gsd/auto-worktree.js +2 -54
  7. package/dist/resources/extensions/gsd/worktree-post-create-hook.js +117 -0
  8. package/dist/web/standalone/.next/BUILD_ID +1 -1
  9. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  10. package/dist/web/standalone/.next/build-manifest.json +2 -2
  11. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  12. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  13. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  29. package/dist/web/standalone/.next/server/app/api/session/events/route.js +1 -1
  30. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  31. package/dist/web/standalone/.next/server/app/index.html +1 -1
  32. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  39. package/dist/web/standalone/.next/server/chunks/1834.js +1 -1
  40. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  41. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  42. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  43. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  44. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  45. package/dist/web/standalone/package.json +0 -1
  46. package/dist/worktree-cli.d.ts +0 -2
  47. package/dist/worktree-cli.js +21 -9
  48. package/package.json +9 -4
  49. package/packages/cloud-mcp-gateway/bin/gsd-cloud-mcp-gateway.js +14 -0
  50. package/packages/cloud-mcp-gateway/package.json +5 -4
  51. package/packages/contracts/package.json +2 -2
  52. package/packages/daemon/bin/gsd-daemon.js +14 -0
  53. package/packages/daemon/bin/gsd-mcp-runtime.js +14 -0
  54. package/packages/daemon/bin/gsd-mcp.js +14 -0
  55. package/packages/daemon/dist/channel-manager.d.ts +53 -0
  56. package/packages/daemon/dist/channel-manager.d.ts.map +1 -0
  57. package/packages/daemon/dist/channel-manager.js +167 -0
  58. package/packages/daemon/dist/channel-manager.js.map +1 -0
  59. package/packages/daemon/dist/cli.d.ts +3 -0
  60. package/packages/daemon/dist/cli.d.ts.map +1 -0
  61. package/packages/daemon/dist/cli.js +94 -0
  62. package/packages/daemon/dist/cli.js.map +1 -0
  63. package/packages/daemon/dist/cloud-cli.d.ts +7 -0
  64. package/packages/daemon/dist/cloud-cli.d.ts.map +1 -0
  65. package/packages/daemon/dist/cloud-cli.js +96 -0
  66. package/packages/daemon/dist/cloud-cli.js.map +1 -0
  67. package/packages/daemon/dist/cloud-config.d.ts +18 -0
  68. package/packages/daemon/dist/cloud-config.d.ts.map +1 -0
  69. package/packages/daemon/dist/cloud-config.js +209 -0
  70. package/packages/daemon/dist/cloud-config.js.map +1 -0
  71. package/packages/daemon/dist/cloud-config.test.d.ts +2 -0
  72. package/packages/daemon/dist/cloud-config.test.d.ts.map +1 -0
  73. package/packages/daemon/dist/cloud-config.test.js +132 -0
  74. package/packages/daemon/dist/cloud-config.test.js.map +1 -0
  75. package/packages/daemon/dist/cloud-runtime.d.ts +26 -0
  76. package/packages/daemon/dist/cloud-runtime.d.ts.map +1 -0
  77. package/packages/daemon/dist/cloud-runtime.js +180 -0
  78. package/packages/daemon/dist/cloud-runtime.js.map +1 -0
  79. package/packages/daemon/dist/cloud-runtime.test.d.ts +2 -0
  80. package/packages/daemon/dist/cloud-runtime.test.d.ts.map +1 -0
  81. package/packages/daemon/dist/cloud-runtime.test.js +28 -0
  82. package/packages/daemon/dist/cloud-runtime.test.js.map +1 -0
  83. package/packages/daemon/dist/cloud-token.d.ts +3 -0
  84. package/packages/daemon/dist/cloud-token.d.ts.map +1 -0
  85. package/packages/daemon/dist/cloud-token.js +37 -0
  86. package/packages/daemon/dist/cloud-token.js.map +1 -0
  87. package/packages/daemon/dist/commands.d.ts +25 -0
  88. package/packages/daemon/dist/commands.d.ts.map +1 -0
  89. package/packages/daemon/dist/commands.js +81 -0
  90. package/packages/daemon/dist/commands.js.map +1 -0
  91. package/packages/daemon/dist/config.d.ts +17 -0
  92. package/packages/daemon/dist/config.d.ts.map +1 -0
  93. package/packages/daemon/dist/config.js +146 -0
  94. package/packages/daemon/dist/config.js.map +1 -0
  95. package/packages/daemon/dist/daemon.d.ts +38 -0
  96. package/packages/daemon/dist/daemon.d.ts.map +1 -0
  97. package/packages/daemon/dist/daemon.js +194 -0
  98. package/packages/daemon/dist/daemon.js.map +1 -0
  99. package/packages/daemon/dist/daemon.test.d.ts +2 -0
  100. package/packages/daemon/dist/daemon.test.d.ts.map +1 -0
  101. package/packages/daemon/dist/daemon.test.js +692 -0
  102. package/packages/daemon/dist/daemon.test.js.map +1 -0
  103. package/packages/daemon/dist/discord-bot.d.ts +70 -0
  104. package/packages/daemon/dist/discord-bot.d.ts.map +1 -0
  105. package/packages/daemon/dist/discord-bot.js +433 -0
  106. package/packages/daemon/dist/discord-bot.js.map +1 -0
  107. package/packages/daemon/dist/discord-bot.test.d.ts +2 -0
  108. package/packages/daemon/dist/discord-bot.test.d.ts.map +1 -0
  109. package/packages/daemon/dist/discord-bot.test.js +667 -0
  110. package/packages/daemon/dist/discord-bot.test.js.map +1 -0
  111. package/packages/daemon/dist/event-bridge.d.ts +72 -0
  112. package/packages/daemon/dist/event-bridge.d.ts.map +1 -0
  113. package/packages/daemon/dist/event-bridge.js +366 -0
  114. package/packages/daemon/dist/event-bridge.js.map +1 -0
  115. package/packages/daemon/dist/event-bridge.test.d.ts +9 -0
  116. package/packages/daemon/dist/event-bridge.test.d.ts.map +1 -0
  117. package/packages/daemon/dist/event-bridge.test.js +528 -0
  118. package/packages/daemon/dist/event-bridge.test.js.map +1 -0
  119. package/packages/daemon/dist/event-formatter.d.ts +34 -0
  120. package/packages/daemon/dist/event-formatter.d.ts.map +1 -0
  121. package/packages/daemon/dist/event-formatter.js +355 -0
  122. package/packages/daemon/dist/event-formatter.js.map +1 -0
  123. package/packages/daemon/dist/event-formatter.test.d.ts +2 -0
  124. package/packages/daemon/dist/event-formatter.test.d.ts.map +1 -0
  125. package/packages/daemon/dist/event-formatter.test.js +333 -0
  126. package/packages/daemon/dist/event-formatter.test.js.map +1 -0
  127. package/packages/daemon/dist/index.d.ts +25 -0
  128. package/packages/daemon/dist/index.d.ts.map +1 -0
  129. package/packages/daemon/dist/index.js +17 -0
  130. package/packages/daemon/dist/index.js.map +1 -0
  131. package/packages/daemon/dist/launchd.d.ts +49 -0
  132. package/packages/daemon/dist/launchd.d.ts.map +1 -0
  133. package/packages/daemon/dist/launchd.js +188 -0
  134. package/packages/daemon/dist/launchd.js.map +1 -0
  135. package/packages/daemon/dist/launchd.test.d.ts +2 -0
  136. package/packages/daemon/dist/launchd.test.d.ts.map +1 -0
  137. package/packages/daemon/dist/launchd.test.js +296 -0
  138. package/packages/daemon/dist/launchd.test.js.map +1 -0
  139. package/packages/daemon/dist/local-tool-executor.d.ts +22 -0
  140. package/packages/daemon/dist/local-tool-executor.d.ts.map +1 -0
  141. package/packages/daemon/dist/local-tool-executor.js +307 -0
  142. package/packages/daemon/dist/local-tool-executor.js.map +1 -0
  143. package/packages/daemon/dist/local-tool-executor.test.d.ts +2 -0
  144. package/packages/daemon/dist/local-tool-executor.test.d.ts.map +1 -0
  145. package/packages/daemon/dist/local-tool-executor.test.js +111 -0
  146. package/packages/daemon/dist/local-tool-executor.test.js.map +1 -0
  147. package/packages/daemon/dist/logger.d.ts +25 -0
  148. package/packages/daemon/dist/logger.d.ts.map +1 -0
  149. package/packages/daemon/dist/logger.js +72 -0
  150. package/packages/daemon/dist/logger.js.map +1 -0
  151. package/packages/daemon/dist/mcp-cli.d.ts +3 -0
  152. package/packages/daemon/dist/mcp-cli.d.ts.map +1 -0
  153. package/packages/daemon/dist/mcp-cli.js +8 -0
  154. package/packages/daemon/dist/mcp-cli.js.map +1 -0
  155. package/packages/daemon/dist/mcp-cli.test.d.ts +2 -0
  156. package/packages/daemon/dist/mcp-cli.test.d.ts.map +1 -0
  157. package/packages/daemon/dist/mcp-cli.test.js +13 -0
  158. package/packages/daemon/dist/mcp-cli.test.js.map +1 -0
  159. package/packages/daemon/dist/mcp-runtime-cli.d.ts +3 -0
  160. package/packages/daemon/dist/mcp-runtime-cli.d.ts.map +1 -0
  161. package/packages/daemon/dist/mcp-runtime-cli.js +8 -0
  162. package/packages/daemon/dist/mcp-runtime-cli.js.map +1 -0
  163. package/packages/daemon/dist/message-batcher.d.ts +78 -0
  164. package/packages/daemon/dist/message-batcher.d.ts.map +1 -0
  165. package/packages/daemon/dist/message-batcher.js +173 -0
  166. package/packages/daemon/dist/message-batcher.js.map +1 -0
  167. package/packages/daemon/dist/message-batcher.test.d.ts +2 -0
  168. package/packages/daemon/dist/message-batcher.test.d.ts.map +1 -0
  169. package/packages/daemon/dist/message-batcher.test.js +242 -0
  170. package/packages/daemon/dist/message-batcher.test.js.map +1 -0
  171. package/packages/daemon/dist/orchestrator.d.ts +98 -0
  172. package/packages/daemon/dist/orchestrator.d.ts.map +1 -0
  173. package/packages/daemon/dist/orchestrator.js +359 -0
  174. package/packages/daemon/dist/orchestrator.js.map +1 -0
  175. package/packages/daemon/dist/orchestrator.test.d.ts +8 -0
  176. package/packages/daemon/dist/orchestrator.test.d.ts.map +1 -0
  177. package/packages/daemon/dist/orchestrator.test.js +425 -0
  178. package/packages/daemon/dist/orchestrator.test.js.map +1 -0
  179. package/packages/daemon/dist/project-scanner.d.ts +18 -0
  180. package/packages/daemon/dist/project-scanner.d.ts.map +1 -0
  181. package/packages/daemon/dist/project-scanner.js +90 -0
  182. package/packages/daemon/dist/project-scanner.js.map +1 -0
  183. package/packages/daemon/dist/project-scanner.test.d.ts +5 -0
  184. package/packages/daemon/dist/project-scanner.test.d.ts.map +1 -0
  185. package/packages/daemon/dist/project-scanner.test.js +183 -0
  186. package/packages/daemon/dist/project-scanner.test.js.map +1 -0
  187. package/packages/daemon/dist/session-manager.d.ts +70 -0
  188. package/packages/daemon/dist/session-manager.d.ts.map +1 -0
  189. package/packages/daemon/dist/session-manager.js +358 -0
  190. package/packages/daemon/dist/session-manager.js.map +1 -0
  191. package/packages/daemon/dist/session-manager.test.d.ts +9 -0
  192. package/packages/daemon/dist/session-manager.test.d.ts.map +1 -0
  193. package/packages/daemon/dist/session-manager.test.js +616 -0
  194. package/packages/daemon/dist/session-manager.test.js.map +1 -0
  195. package/packages/daemon/dist/types.d.ts +133 -0
  196. package/packages/daemon/dist/types.d.ts.map +1 -0
  197. package/packages/daemon/dist/types.js +8 -0
  198. package/packages/daemon/dist/types.js.map +1 -0
  199. package/packages/daemon/dist/verbosity.d.ts +27 -0
  200. package/packages/daemon/dist/verbosity.d.ts.map +1 -0
  201. package/packages/daemon/dist/verbosity.js +86 -0
  202. package/packages/daemon/dist/verbosity.js.map +1 -0
  203. package/packages/daemon/dist/verbosity.test.d.ts +2 -0
  204. package/packages/daemon/dist/verbosity.test.d.ts.map +1 -0
  205. package/packages/daemon/dist/verbosity.test.js +136 -0
  206. package/packages/daemon/dist/verbosity.test.js.map +1 -0
  207. package/packages/daemon/package.json +9 -8
  208. package/packages/gsd-agent-core/package.json +6 -6
  209. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  210. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +3 -1
  211. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  212. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
  213. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +0 -1
  214. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  215. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts +1 -0
  216. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
  217. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -0
  218. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
  219. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  220. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +2 -1
  221. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
  222. package/packages/gsd-agent-modes/package.json +8 -8
  223. package/packages/mcp-server/bin/gsd-mcp-server.js +14 -0
  224. package/packages/mcp-server/package.json +6 -5
  225. package/packages/native/package.json +3 -3
  226. package/packages/pi-agent-core/package.json +4 -4
  227. package/packages/pi-ai/bin/pi-ai.js +14 -0
  228. package/packages/pi-ai/dist/models.generated.d.ts +0 -17
  229. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  230. package/packages/pi-ai/dist/models.generated.js +18 -35
  231. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  232. package/packages/pi-ai/package.json +5 -4
  233. package/packages/pi-coding-agent/dist/core/tools/read.d.ts +2 -2
  234. package/packages/pi-coding-agent/dist/core/tools/read.d.ts.map +1 -1
  235. package/packages/pi-coding-agent/dist/core/tools/read.js +5 -3
  236. package/packages/pi-coding-agent/dist/core/tools/read.js.map +1 -1
  237. package/packages/pi-coding-agent/package.json +9 -9
  238. package/packages/pi-tui/package.json +2 -2
  239. package/packages/rpc-client/package.json +3 -3
  240. package/pkg/package.json +1 -1
  241. package/scripts/ensure-workspace-builds.cjs +4 -4
  242. package/scripts/install/deps.js +10 -0
  243. package/src/resources/extensions/gsd/auto/loop.ts +22 -0
  244. package/src/resources/extensions/gsd/auto/phases.ts +1 -1
  245. package/src/resources/extensions/gsd/auto-worktree.ts +2 -56
  246. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +64 -0
  247. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +141 -1
  248. package/src/resources/extensions/gsd/worktree-post-create-hook.ts +127 -0
  249. package/dist/tsconfig.extensions.tsbuildinfo +0 -1
  250. /package/dist/web/standalone/.next/static/{JP7xjsa5zSaO76XhE-mFJ → spUYLkQXoHJyxYOMH9VQy}/_buildManifest.js +0 -0
  251. /package/dist/web/standalone/.next/static/{JP7xjsa5zSaO76XhE-mFJ → spUYLkQXoHJyxYOMH9VQy}/_ssgManifest.js +0 -0
@@ -13,7 +13,7 @@ import { mkdtempSync, mkdirSync, rmSync, existsSync, writeFileSync, readFileSync
13
13
  import { join } from "node:path";
14
14
  import { tmpdir } from "node:os";
15
15
 
16
- import { runWorktreePostCreateHook } from "../auto-worktree.ts";
16
+ import { runWorktreePostCreateHook } from "../worktree-post-create-hook.ts";
17
17
 
18
18
  function makeTmpDir(): string {
19
19
  return mkdtempSync(join(tmpdir(), "gsd-wt-hook-test-"));
@@ -45,10 +45,150 @@ function writeNodeHookScript(filePath: string, code: string): void {
45
45
  test("returns null when no hook path is provided", () => {
46
46
  const src = makeTmpDir();
47
47
  const wt = makeTmpDir();
48
+ const previousGsdHome = process.env.GSD_HOME;
48
49
  try {
50
+ process.env.GSD_HOME = join(src, "empty-gsd-home");
49
51
  const result = runWorktreePostCreateHook(src, wt, undefined);
50
52
  assert.equal(result, null);
51
53
  } finally {
54
+ if (previousGsdHome === undefined) delete process.env.GSD_HOME;
55
+ else process.env.GSD_HOME = previousGsdHome;
56
+ rmSync(src, { recursive: true, force: true });
57
+ rmSync(wt, { recursive: true, force: true });
58
+ }
59
+ });
60
+
61
+ test("reads git.worktree_post_create from project preferences without loading auto-worktree", () => {
62
+ const src = makeTmpDir();
63
+ const wt = makeTmpDir();
64
+ const previousGsdHome = process.env.GSD_HOME;
65
+ try {
66
+ process.env.GSD_HOME = join(src, "empty-gsd-home");
67
+ const hooksDir = join(src, ".gsd", "hooks");
68
+ mkdirSync(hooksDir, { recursive: true });
69
+ writeFileSync(
70
+ join(src, ".gsd", "PREFERENCES.md"),
71
+ [
72
+ "---",
73
+ "git:",
74
+ ` worktree_post_create: ${hookPath(".gsd/hooks/post-create")}`,
75
+ "---",
76
+ "",
77
+ ].join("\n"),
78
+ );
79
+ const hookFile = hookPath(join(hooksDir, "post-create"));
80
+ writeNodeHookScript(
81
+ hookFile,
82
+ `require("fs").writeFileSync(require("path").join(process.env.WORKTREE_DIR, "configured-hook-ran"), "ok");`,
83
+ );
84
+
85
+ const result = runWorktreePostCreateHook(src, wt);
86
+
87
+ assert.equal(result, null);
88
+ assert.ok(existsSync(join(wt, "configured-hook-ran")), "configured hook should run");
89
+ } finally {
90
+ if (previousGsdHome === undefined) delete process.env.GSD_HOME;
91
+ else process.env.GSD_HOME = previousGsdHome;
92
+ rmSync(src, { recursive: true, force: true });
93
+ rmSync(wt, { recursive: true, force: true });
94
+ }
95
+ });
96
+
97
+ test("prefers canonical project PREFERENCES.md over legacy lowercase preferences.md", () => {
98
+ const src = makeTmpDir();
99
+ const wt = makeTmpDir();
100
+ const previousGsdHome = process.env.GSD_HOME;
101
+ try {
102
+ process.env.GSD_HOME = join(src, "empty-gsd-home");
103
+ const hooksDir = join(src, ".gsd", "hooks");
104
+ mkdirSync(hooksDir, { recursive: true });
105
+ writeFileSync(
106
+ join(src, ".gsd", "preferences.md"),
107
+ [
108
+ "---",
109
+ "git:",
110
+ ` worktree_post_create: ${hookPath(".gsd/hooks/legacy")}`,
111
+ "---",
112
+ "",
113
+ ].join("\n"),
114
+ );
115
+ writeFileSync(
116
+ join(src, ".gsd", "PREFERENCES.md"),
117
+ [
118
+ "---",
119
+ "git:",
120
+ ` worktree_post_create: ${hookPath(".gsd/hooks/canonical")}`,
121
+ "---",
122
+ "",
123
+ ].join("\n"),
124
+ );
125
+ writeNodeHookScript(
126
+ hookPath(join(hooksDir, "canonical")),
127
+ `require("fs").writeFileSync(require("path").join(process.env.WORKTREE_DIR, "configured-hook-ran"), "canonical");`,
128
+ );
129
+ writeNodeHookScript(
130
+ hookPath(join(hooksDir, "legacy")),
131
+ `require("fs").writeFileSync(require("path").join(process.env.WORKTREE_DIR, "configured-hook-ran"), "legacy");`,
132
+ );
133
+
134
+ const result = runWorktreePostCreateHook(src, wt);
135
+
136
+ assert.equal(result, null);
137
+ assert.equal(readFileSync(join(wt, "configured-hook-ran"), "utf-8"), "canonical");
138
+ } finally {
139
+ if (previousGsdHome === undefined) delete process.env.GSD_HOME;
140
+ else process.env.GSD_HOME = previousGsdHome;
141
+ rmSync(src, { recursive: true, force: true });
142
+ rmSync(wt, { recursive: true, force: true });
143
+ }
144
+ });
145
+
146
+ test("prefers canonical global PREFERENCES.md over legacy lowercase preferences.md", () => {
147
+ const src = makeTmpDir();
148
+ const wt = makeTmpDir();
149
+ const previousGsdHome = process.env.GSD_HOME;
150
+ try {
151
+ const gsdHomeDir = join(src, "global-gsd-home");
152
+ process.env.GSD_HOME = gsdHomeDir;
153
+ const hooksDir = join(src, ".gsd", "hooks");
154
+ mkdirSync(hooksDir, { recursive: true });
155
+ mkdirSync(gsdHomeDir, { recursive: true });
156
+ writeFileSync(
157
+ join(gsdHomeDir, "preferences.md"),
158
+ [
159
+ "---",
160
+ "git:",
161
+ ` worktree_post_create: ${hookPath(".gsd/hooks/legacy")}`,
162
+ "---",
163
+ "",
164
+ ].join("\n"),
165
+ );
166
+ writeFileSync(
167
+ join(gsdHomeDir, "PREFERENCES.md"),
168
+ [
169
+ "---",
170
+ "git:",
171
+ ` worktree_post_create: ${hookPath(".gsd/hooks/canonical")}`,
172
+ "---",
173
+ "",
174
+ ].join("\n"),
175
+ );
176
+ writeNodeHookScript(
177
+ hookPath(join(hooksDir, "canonical")),
178
+ `require("fs").writeFileSync(require("path").join(process.env.WORKTREE_DIR, "configured-hook-ran"), "canonical");`,
179
+ );
180
+ writeNodeHookScript(
181
+ hookPath(join(hooksDir, "legacy")),
182
+ `require("fs").writeFileSync(require("path").join(process.env.WORKTREE_DIR, "configured-hook-ran"), "legacy");`,
183
+ );
184
+
185
+ const result = runWorktreePostCreateHook(src, wt);
186
+
187
+ assert.equal(result, null);
188
+ assert.equal(readFileSync(join(wt, "configured-hook-ran"), "utf-8"), "canonical");
189
+ } finally {
190
+ if (previousGsdHome === undefined) delete process.env.GSD_HOME;
191
+ else process.env.GSD_HOME = previousGsdHome;
52
192
  rmSync(src, { recursive: true, force: true });
53
193
  rmSync(wt, { recursive: true, force: true });
54
194
  }
@@ -0,0 +1,127 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Lightweight worktree post-create hook runner.
3
+
4
+ import { execFileSync } from "node:child_process";
5
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { isAbsolute, join } from "node:path";
8
+
9
+ import { parse as parseYaml } from "yaml";
10
+
11
+ import { gsdHome } from "./gsd-home.js";
12
+ import { gsdRoot } from "./paths.js";
13
+
14
+ function readPreferencesObject(path: string): Record<string, unknown> | null {
15
+ if (!existsSync(path)) return null;
16
+
17
+ const content = readFileSync(path, "utf-8");
18
+ try {
19
+ const startMarker = content.startsWith("---\r\n") ? "---\r\n" : "---\n";
20
+ if (content.startsWith(startMarker)) {
21
+ const searchStart = startMarker.length;
22
+ const endIdx = content.indexOf("\n---", searchStart);
23
+ if (endIdx === -1) return null;
24
+
25
+ const parsed = parseYaml(content.slice(searchStart, endIdx).replace(/\r/g, ""));
26
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
27
+ ? parsed as Record<string, unknown>
28
+ : null;
29
+ }
30
+
31
+ const gitLines: string[] = [];
32
+ let inGitSection = false;
33
+ for (const rawLine of content.split("\n")) {
34
+ const line = rawLine.replace(/\r$/, "");
35
+ const heading = line.match(/^##\s+(.+)$/);
36
+ if (heading) {
37
+ inGitSection = heading[1].trim().toLowerCase().replace(/\s+/g, "_") === "git";
38
+ continue;
39
+ }
40
+ if (inGitSection && line.trim() && !line.trimStart().startsWith("#")) {
41
+ gitLines.push(line.replace(/^\s*-\s*/, ""));
42
+ }
43
+ }
44
+ if (gitLines.length === 0) return null;
45
+
46
+ const parsed = parseYaml(gitLines.join("\n"));
47
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
48
+ ? { git: parsed as Record<string, unknown> }
49
+ : null;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function extractHookPath(preferences: Record<string, unknown> | null): string | null {
56
+ const git = preferences?.git;
57
+ if (!git || typeof git !== "object" || Array.isArray(git)) return null;
58
+ const hookPath = (git as Record<string, unknown>).worktree_post_create;
59
+ return typeof hookPath === "string" && hookPath.trim() ? hookPath : null;
60
+ }
61
+
62
+ function resolveConfiguredHookPath(sourceDir: string): string | null {
63
+ const paths = [
64
+ join(homedir(), ".pi", "agent", "gsd-preferences.md"),
65
+ join(gsdHome(), "preferences.md"),
66
+ join(gsdHome(), "PREFERENCES.md"),
67
+ join(gsdRoot(sourceDir), "preferences.md"),
68
+ join(gsdRoot(sourceDir), "PREFERENCES.md"),
69
+ ];
70
+
71
+ let hookPath: string | null = null;
72
+ for (const path of paths) {
73
+ hookPath = extractHookPath(readPreferencesObject(path)) ?? hookPath;
74
+ }
75
+ return hookPath;
76
+ }
77
+
78
+ /**
79
+ * Run the user-configured post-create hook script after worktree creation.
80
+ * The script receives SOURCE_DIR and WORKTREE_DIR as environment variables.
81
+ * Failure is non-fatal -- returns the error message or null on success.
82
+ *
83
+ * Reads git.worktree_post_create from effective global/project preferences
84
+ * unless hookPath is provided directly.
85
+ */
86
+ export function runWorktreePostCreateHook(
87
+ sourceDir: string,
88
+ worktreeDir: string,
89
+ hookPath?: string,
90
+ ): string | null {
91
+ if (hookPath === undefined) {
92
+ hookPath = resolveConfiguredHookPath(sourceDir) ?? undefined;
93
+ }
94
+ if (!hookPath) return null;
95
+
96
+ let resolved = isAbsolute(hookPath) ? hookPath : join(sourceDir, hookPath);
97
+ if (!existsSync(resolved)) {
98
+ return `Worktree post-create hook not found: ${resolved}`;
99
+ }
100
+ if (process.platform === "win32") {
101
+ try {
102
+ resolved = realpathSync.native(resolved);
103
+ } catch {
104
+ // Keep the original path; the exec error below will include the failure.
105
+ }
106
+ }
107
+
108
+ try {
109
+ const needsShell = process.platform === "win32" && /\.(bat|cmd)$/i.test(resolved);
110
+ execFileSync(resolved, [], {
111
+ cwd: worktreeDir,
112
+ env: {
113
+ ...process.env,
114
+ SOURCE_DIR: sourceDir,
115
+ WORKTREE_DIR: worktreeDir,
116
+ },
117
+ stdio: ["ignore", "pipe", "pipe"],
118
+ encoding: "utf-8",
119
+ timeout: 30_000,
120
+ shell: needsShell,
121
+ });
122
+ return null;
123
+ } catch (err) {
124
+ const msg = err instanceof Error ? err.message : String(err);
125
+ return `Worktree post-create hook failed: ${msg}`;
126
+ }
127
+ }