@opengsd/gsd-pi 1.0.2-dev.d456457 → 1.0.2-dev.dbfb371

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 (110) hide show
  1. package/README.md +63 -12
  2. package/dist/resource-loader.d.ts +5 -0
  3. package/dist/resource-loader.js +24 -8
  4. package/dist/resources/.managed-resources-content-hash +1 -1
  5. package/dist/resources/extensions/gsd/auto/loop.js +19 -0
  6. package/dist/resources/extensions/gsd/auto/phases.js +1 -1
  7. package/dist/resources/extensions/gsd/auto-worktree.js +2 -54
  8. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -15
  9. package/dist/resources/extensions/gsd/commands-handlers.js +3 -0
  10. package/dist/resources/extensions/gsd/worktree-post-create-hook.js +117 -0
  11. package/dist/resources/shared/package-manager-detection.js +36 -0
  12. package/dist/update-check.d.ts +6 -2
  13. package/dist/update-check.js +7 -3
  14. package/dist/web/standalone/.next/BUILD_ID +1 -1
  15. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  16. package/dist/web/standalone/.next/build-manifest.json +2 -2
  17. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  18. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  35. package/dist/web/standalone/.next/server/app/api/session/events/route.js +1 -1
  36. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  37. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  38. package/dist/web/standalone/.next/server/app/index.html +1 -1
  39. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  46. package/dist/web/standalone/.next/server/chunks/1834.js +1 -1
  47. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  48. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  49. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  50. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  51. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  52. package/dist/worktree-cli.d.ts +0 -2
  53. package/dist/worktree-cli.js +21 -9
  54. package/package.json +5 -2
  55. package/packages/cloud-mcp-gateway/bin/gsd-cloud-mcp-gateway.js +14 -0
  56. package/packages/cloud-mcp-gateway/package.json +4 -3
  57. package/packages/contracts/package.json +1 -1
  58. package/packages/daemon/package.json +4 -4
  59. package/packages/gsd-agent-core/package.json +5 -5
  60. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  61. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +3 -1
  62. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  63. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
  64. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +0 -1
  65. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  66. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts +1 -0
  67. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
  68. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -0
  69. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
  70. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  71. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +2 -1
  72. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
  73. package/packages/gsd-agent-modes/package.json +7 -7
  74. package/packages/mcp-server/bin/gsd-mcp-server.js +14 -0
  75. package/packages/mcp-server/package.json +5 -4
  76. package/packages/native/package.json +1 -1
  77. package/packages/pi-agent-core/dist/agent-loop.js +13 -13
  78. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  79. package/packages/pi-agent-core/package.json +1 -1
  80. package/packages/pi-ai/bin/pi-ai.js +14 -0
  81. package/packages/pi-ai/dist/models.generated.d.ts +40 -17
  82. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  83. package/packages/pi-ai/dist/models.generated.js +42 -23
  84. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  85. package/packages/pi-ai/package.json +3 -2
  86. package/packages/pi-coding-agent/dist/core/tools/read.d.ts +2 -2
  87. package/packages/pi-coding-agent/dist/core/tools/read.d.ts.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/tools/read.js +5 -3
  89. package/packages/pi-coding-agent/dist/core/tools/read.js.map +1 -1
  90. package/packages/pi-coding-agent/package.json +8 -8
  91. package/packages/pi-tui/package.json +1 -1
  92. package/packages/rpc-client/package.json +2 -2
  93. package/pkg/package.json +1 -1
  94. package/scripts/install/deps.js +10 -0
  95. package/scripts/install/detect-existing.js +17 -3
  96. package/scripts/install/npm-global.js +103 -33
  97. package/scripts/install.js +1 -0
  98. package/src/resources/extensions/gsd/auto/loop.ts +22 -0
  99. package/src/resources/extensions/gsd/auto/phases.ts +1 -1
  100. package/src/resources/extensions/gsd/auto-worktree.ts +2 -56
  101. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -15
  102. package/src/resources/extensions/gsd/commands-handlers.ts +2 -0
  103. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +64 -0
  104. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +60 -0
  105. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +141 -1
  106. package/src/resources/extensions/gsd/worktree-post-create-hook.ts +127 -0
  107. package/src/resources/shared/package-manager-detection.ts +39 -0
  108. package/dist/tsconfig.extensions.tsbuildinfo +0 -1
  109. /package/dist/web/standalone/.next/static/{4NVKiVx4C-8FUT9A7DZdq → BVrLsL82ynrLee5zxeihC}/_buildManifest.js +0 -0
  110. /package/dist/web/standalone/.next/static/{4NVKiVx4C-8FUT9A7DZdq → BVrLsL82ynrLee5zxeihC}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -13,6 +13,28 @@ GSD Pi is a local-first coding agent for planning, implementing, verifying, and
13
13
 
14
14
  It combines a terminal agent, project workflow tools, worktree-aware Git automation, and optional UI integrations so a project can move from idea to reviewed implementation with less manual coordination.
15
15
 
16
+ ## Screenshots
17
+
18
+ GSD runs as a terminal-first TUI with optional browser dashboard controls.
19
+
20
+ ![GSD TUI running an agent workflow](./docs/assets/screenshots/gsd-tui-agent-run.png)
21
+
22
+ ![GSD TUI progress dashboard](./docs/assets/screenshots/gsd-tui-progress-dashboard.png)
23
+
24
+ ![GSD TUI metrics dashboard](./docs/assets/screenshots/gsd-tui-metrics-dashboard.png)
25
+
26
+ ## Feature Roll-Up
27
+
28
+ - **Guided terminal agent** — Start with `gsd`, configure providers, and run planned or quick coding sessions from your shell.
29
+ - **Autonomous project workflow** — Break work into milestones, slices, and tasks, then let auto mode plan, implement, verify, and advance.
30
+ - **Worktree-aware Git automation** — Keep implementation work isolated while preserving a reviewable main checkout.
31
+ - **Local project memory** — Store project requirements, decisions, runtime notes, generated plans, summaries, and validation evidence under `.gsd/`.
32
+ - **Multi-provider model routing** — Use the provider your team already has, with configurable defaults and per-phase model preferences.
33
+ - **Extension surface** — Add project-specific commands, tools, skills, and UI integrations through bundled or community extensions.
34
+ - **Terminal and web surfaces** — Use the TUI by default, or launch `gsd --web` when a visual control plane fits the work better than a terminal.
35
+
36
+ See [CHANGELOG.md](./CHANGELOG.md) for release-by-release fixes and [Legacy Release History](./docs/archive/legacy-release-history.md) for archived history before the `open-gsd/gsd-pi` baseline.
37
+
16
38
  ## Status
17
39
 
18
40
  This repository is starting a new development baseline at version `1.0.0` under the `open-gsd/gsd-pi` project.
@@ -27,41 +49,63 @@ Recommended — guided installer:
27
49
  npx @opengsd/gsd-pi@latest
28
50
  ```
29
51
 
30
- Alternative direct global install:
52
+ For CI or scripted installs:
53
+
54
+ ```bash
55
+ npx @opengsd/gsd-pi@latest --yes
56
+ ```
57
+
58
+ Alternative — direct npm global install:
31
59
 
32
60
  ```bash
33
61
  npm install -g @opengsd/gsd-pi@latest
34
62
  ```
35
63
 
36
- For CI or scripted installs:
64
+ If you want pnpm to own the global install, use pnpm's runner:
37
65
 
38
66
  ```bash
39
- npx @opengsd/gsd-pi@latest --yes
67
+ pnpm setup
68
+ exec $SHELL -l
69
+ pnpm dlx @opengsd/gsd-pi@latest
40
70
  ```
41
71
 
42
72
  Source: [`open-gsd/gsd-pi`](https://github.com/open-gsd/gsd-pi).
43
73
 
44
74
  ## Migrate From Older Installs
45
75
 
46
- GSD Pi now installs from the scoped npm package `@opengsd/gsd-pi`. If you previously installed the older unscoped `gsd-pi` package, remove it first so the old global binary does not shadow the new package.
76
+ GSD Pi now installs from the scoped package `@opengsd/gsd-pi`. If you previously installed the older unscoped `gsd-pi` package, remove it first so the old global binary does not shadow the new package.
47
77
 
48
- macOS / Linux:
78
+ Recommended migration with the guided `npx` installer:
49
79
 
50
80
  ```bash
51
- npm uninstall -g gsd-pi
81
+ npm uninstall -g gsd-pi @opengsd/gsd-pi
52
82
  rm -f ~/.gsd/.update-check ~/.gsd/agent/managed-resources.json
53
- npm install -g @opengsd/gsd-pi@latest
54
- which gsd
83
+ npx @opengsd/gsd-pi@latest
84
+ command -v gsd
55
85
  gsd --version
56
86
  ```
57
87
 
58
- Windows PowerShell:
88
+ If the old package was installed with `sudo npm install -g`, use `sudo npm uninstall -g gsd-pi` for the old package removal.
89
+
90
+ To migrate from old npm globals to a pnpm-owned global install:
91
+
92
+ ```bash
93
+ npm uninstall -g gsd-pi @opengsd/gsd-pi
94
+ rm -f ~/.gsd/.update-check ~/.gsd/agent/managed-resources.json
95
+ pnpm setup
96
+ exec $SHELL -l
97
+ pnpm dlx @opengsd/gsd-pi@latest
98
+ command -v gsd
99
+ gsd --version
100
+ ```
101
+
102
+ Windows PowerShell with the guided `npx` installer:
59
103
 
60
104
  ```powershell
61
- npm uninstall -g gsd-pi
105
+ npm uninstall -g gsd-pi @opengsd/gsd-pi
62
106
  Remove-Item "$env:USERPROFILE\.gsd\.update-check" -Force -ErrorAction SilentlyContinue
63
107
  Remove-Item "$env:USERPROFILE\.gsd\agent\managed-resources.json" -Force -ErrorAction SilentlyContinue
64
- npm install -g @opengsd/gsd-pi@latest
108
+ npx @opengsd/gsd-pi@latest
65
109
  where.exe gsd
66
110
  gsd --version
67
111
  ```
@@ -85,6 +129,14 @@ npm uninstall -g @opengsd/gsd-pi gsd-pi
85
129
  rm -rf ~/.gsd
86
130
  ```
87
131
 
132
+ If you installed GSD with pnpm, use pnpm for the pnpm-owned package. If pnpm reports that its global bin directory is not on `PATH`, run `pnpm setup`, restart your shell, then retry.
133
+
134
+ ```bash
135
+ pnpm remove -g @opengsd/gsd-pi
136
+ npm uninstall -g gsd-pi
137
+ rm -rf ~/.gsd
138
+ ```
139
+
88
140
  Windows PowerShell:
89
141
 
90
142
  ```powershell
@@ -139,7 +191,6 @@ Then use slash commands inside the GSD session:
139
191
  | `native/` | Native engine packaging and platform binaries |
140
192
  | `studio/` | Desktop studio app |
141
193
  | `web/` | Web UI and API surface |
142
- | `vscode-extension/` | VS Code integration |
143
194
  | `docs/` | User and developer documentation |
144
195
  | `scripts/` | Build, release, migration, and maintenance scripts |
145
196
 
@@ -33,6 +33,11 @@ export declare function getNewerManagedResourceVersion(agentDir: string, current
33
33
  * 4. Makes the result writable for the next upgrade cycle.
34
34
  */
35
35
  export declare function syncResourceDir(srcDir: string, destDir: string): void;
36
+ export declare function resolvePackageNodeModulesLayout(root: string): {
37
+ internalNodeModules: string;
38
+ hoistedNodeModules: string | null;
39
+ };
40
+ export declare function findNearestNodeModulesAncestor(startPath: string): string | null;
36
41
  /** Check if any GSD workspace scopes exist in internal but not in hoisted node_modules */
37
42
  export declare function hasMissingWorkspaceScopes(hoisted: string, internal: string): boolean;
38
43
  /**
@@ -311,16 +311,15 @@ function copyDirRecursive(src, dest) {
311
311
  * them without requiring every call site to use jiti.
312
312
  *
313
313
  * Layout differences by install method:
314
- * - Source/monorepo: packageRoot/node_modules has everything simple symlink
315
- * - Global install (npm/bun/pnpm): merge hoisted dirname(packageRoot)/node_modules with
316
- * packageRoot/node_modules so package-local deps like @sinclair/typebox resolve (#3529, #3564)
314
+ * - Source/monorepo: packageRoot/node_modules has everything -> simple symlink
315
+ * - Global install (npm/bun/pnpm): merge the nearest ancestor node_modules
316
+ * with packageRoot/node_modules so both hoisted deps like yaml and
317
+ * package-local deps like @sinclair/typebox resolve (#3529, #3564).
317
318
  */
318
319
  function ensureNodeModulesSymlink(agentDir) {
319
320
  const agentNodeModules = join(agentDir, 'node_modules');
320
- const internalNodeModules = join(packageRoot, 'node_modules');
321
- const hoistedNodeModules = dirname(packageRoot);
322
- const isGlobalInstall = basename(hoistedNodeModules) === 'node_modules';
323
- if (!isGlobalInstall) {
321
+ const { internalNodeModules, hoistedNodeModules } = resolvePackageNodeModulesLayout(packageRoot);
322
+ if (!hoistedNodeModules) {
324
323
  // Source/monorepo: internal node_modules has everything
325
324
  reconcileSymlink(agentNodeModules, internalNodeModules);
326
325
  return;
@@ -330,6 +329,23 @@ function ensureNodeModulesSymlink(agentDir) {
330
329
  // @gsd/* scopes are hoisted — a hoisted-only symlink breaks extension imports.
331
330
  reconcileMergedNodeModules(agentNodeModules, hoistedNodeModules, internalNodeModules);
332
331
  }
332
+ export function resolvePackageNodeModulesLayout(root) {
333
+ return {
334
+ internalNodeModules: join(root, 'node_modules'),
335
+ hoistedNodeModules: findNearestNodeModulesAncestor(root),
336
+ };
337
+ }
338
+ export function findNearestNodeModulesAncestor(startPath) {
339
+ let current = resolve(startPath);
340
+ while (true) {
341
+ if (basename(current) === 'node_modules')
342
+ return current;
343
+ const parent = dirname(current);
344
+ if (parent === current)
345
+ return null;
346
+ current = parent;
347
+ }
348
+ }
333
349
  /** Check if any GSD workspace scopes exist in internal but not in hoisted node_modules */
334
350
  export function hasMissingWorkspaceScopes(hoisted, internal) {
335
351
  if (!existsSync(internal))
@@ -627,7 +643,7 @@ function cleanupBundledSkillsFromEcosystemDir() {
627
643
  makeTreeWritable(targetPath);
628
644
  rmSync(targetPath, { recursive: true, force: true });
629
645
  }
630
- else {
646
+ else if (process.env.GSD_RESOURCE_LOADER_DEBUG === '1') {
631
647
  console.warn(`[GSD] Leaving ambiguous skill collision in ${targetPath}; ` +
632
648
  `the bundled copy will be used from ~/.gsd/agent/skills/${entry.name}.`);
633
649
  }
@@ -1 +1 @@
1
- f796c99ee5d78c73
1
+ 9d6ff59a3cc348a6
@@ -50,6 +50,7 @@ import { handleCustomEngineVerifyPause, handleCustomEngineVerifyRetryOutcome, }
50
50
  import { handleCustomEngineReconcile } from "./workflow-custom-engine-reconcile.js";
51
51
  import { handleCustomEngineReconcileOutcome } from "./workflow-custom-engine-reconcile-outcome.js";
52
52
  import { formatLeaseConflictNotice } from "./lease-conflict-notice.js";
53
+ import { setAutoOutcomeWidget, unitVerb } from "../auto-dashboard.js";
53
54
  /**
54
55
  * Returns true if workerId is an active worker in this project whose OS
55
56
  * process no longer exists. Used to detect dead lease holders before
@@ -690,6 +691,24 @@ export async function autoLoop(ctx, pi, s, deps, options) {
690
691
  });
691
692
  if (reconcileFlow.action === "break")
692
693
  break;
694
+ if (s.stepMode) {
695
+ if (ctx.hasUI) {
696
+ ctx.ui.setWidget?.("gsd-progress", undefined);
697
+ setAutoOutcomeWidget(ctx, {
698
+ status: "step",
699
+ title: "Step complete",
700
+ detail: `Completed ${unitVerb(iterData.unitType)} ${iterData.unitId}.`,
701
+ unitLabel: `${unitVerb(iterData.unitType)} ${iterData.unitId}`,
702
+ nextAction: "Advance one step, or resume automatic mode.",
703
+ commands: ["/gsd next", "/gsd auto", "/gsd status for overview"],
704
+ startedAt: s.autoStartTime,
705
+ });
706
+ }
707
+ ctx.ui.setStatus("gsd-auto", "next");
708
+ ctx.ui.notify(`Step complete: ${unitVerb(iterData.unitType)} ${iterData.unitId}. Run /gsd next for the next step, or /gsd auto to continue automatically.`, "info");
709
+ s.preserveStepSurfaceAfterLoopExit = true;
710
+ break;
711
+ }
693
712
  continue;
694
713
  }
695
714
  if (!sidecarItem) {
@@ -1568,7 +1568,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
1568
1568
  const dispatchKey = `${unitType}/${unitId}`;
1569
1569
  const nextDispatchCount = (s.unitDispatchCount.get(dispatchKey) ?? 0) + 1;
1570
1570
  // Status bar (widget + preconditions deferred until after model selection — see #2899)
1571
- ctx.ui.setStatus("gsd-auto", "auto");
1571
+ ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
1572
1572
  if (mid)
1573
1573
  deps.updateSliceProgressCache(s.basePath, mid, state.activeSlice?.id);
1574
1574
  // ── Safety harness: reset evidence + create checkpoint ──
@@ -23,6 +23,7 @@ import { debugLog } from "./debug-logger.js";
23
23
  import { logWarning, logError } from "./workflow-logger.js";
24
24
  import { loadEffectiveGSDPreferences } from "./preferences.js";
25
25
  import { MILESTONE_ID_RE } from "./milestone-ids.js";
26
+ import { runWorktreePostCreateHook } from "./worktree-post-create-hook.js";
26
27
  import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeWorkingTreeStatus, nativeAddAllWithExclusions, nativeCommit, nativeCheckoutBranch, nativeMergeSquash, nativeConflictFiles, nativeAddPaths, nativeRmForce, nativeBranchDelete, nativeBranchForceReset, nativeBranchExists, nativeDiffNumstat, nativeUpdateRef, nativeIsAncestor, nativeMergeAbort, nativeWorktreeList, nativeLsFiles, } from "./native-git-bridge.js";
27
28
  import { gsdHome } from "./gsd-home.js";
28
29
  import { createWorkspace } from "./workspace.js";
@@ -796,60 +797,7 @@ export function syncGsdStateToWorktree(mainBasePath, worktreePath_) {
796
797
  export function syncWorktreeStateBack(mainBasePath, worktreePath, milestoneId) {
797
798
  return _finalizeProjectionForMergeImpl(mainBasePath, worktreePath, milestoneId);
798
799
  }
799
- // ─── Worktree Post-Create Hook (#597) ────────────────────────────────────────
800
- /**
801
- * Run the user-configured post-create hook script after worktree creation.
802
- * The script receives SOURCE_DIR and WORKTREE_DIR as environment variables.
803
- * Failure is non-fatal — returns the error message or null on success.
804
- *
805
- * Reads the hook path from git.worktree_post_create in preferences.
806
- * Pass hookPath directly to bypass preference loading (useful for testing).
807
- */
808
- export function runWorktreePostCreateHook(sourceDir, worktreeDir, hookPath) {
809
- if (hookPath === undefined) {
810
- const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
811
- hookPath = prefs?.worktree_post_create;
812
- }
813
- if (!hookPath)
814
- return null;
815
- // Resolve relative paths against the source project root.
816
- // On Windows, convert 8.3 short paths (e.g. RUNNER~1) to long paths
817
- // so execFileSync can locate the file correctly.
818
- let resolved = isAbsolute(hookPath) ? hookPath : join(sourceDir, hookPath);
819
- if (!existsSync(resolved)) {
820
- return `Worktree post-create hook not found: ${resolved}`;
821
- }
822
- if (process.platform === "win32") {
823
- try {
824
- resolved = realpathSync.native(resolved);
825
- }
826
- catch (err) { /* keep original */
827
- logWarning("worktree", `realpath failed: ${err instanceof Error ? err.message : String(err)}`);
828
- }
829
- }
830
- try {
831
- // .bat/.cmd files on Windows require shell mode — execFileSync cannot
832
- // spawn them directly (EINVAL).
833
- const needsShell = process.platform === "win32" && /\.(bat|cmd)$/i.test(resolved);
834
- execFileSync(resolved, [], {
835
- cwd: worktreeDir,
836
- env: {
837
- ...process.env,
838
- SOURCE_DIR: sourceDir,
839
- WORKTREE_DIR: worktreeDir,
840
- },
841
- stdio: ["ignore", "pipe", "pipe"],
842
- encoding: "utf-8",
843
- timeout: 30_000, // 30 second timeout
844
- shell: needsShell,
845
- });
846
- return null;
847
- }
848
- catch (err) {
849
- const msg = err instanceof Error ? err.message : String(err);
850
- return `Worktree post-create hook failed: ${msg}`;
851
- }
852
- }
800
+ export { runWorktreePostCreateHook } from "./worktree-post-create-hook.js";
853
801
  // ─── Auto-Worktree Branch Naming ───────────────────────────────────────────
854
802
  /** Returns the git branch name for a milestone worktree (`milestone/<MID>`). */
855
803
  export function autoWorktreeBranch(milestoneId) {
@@ -418,6 +418,17 @@ function initSessionNotifications(ctx) {
418
418
  installNotifyInterceptor(ctx);
419
419
  initNotificationWidget(ctx);
420
420
  }
421
+ async function prepareWorkflowMcpForHookContext(ctx, basePath) {
422
+ // Skip MCP auto-prep when running inside an auto-worktree. The worktree
423
+ // already has .mcp.json from createAutoWorktree, and re-running the writer
424
+ // post-chdir rewrites the file mid-run (non-idempotent due to cwd-relative
425
+ // CLI path resolution), dirtying the tree and breaking the milestone merge.
426
+ const { isInAutoWorktree } = await import("../auto-worktree.js");
427
+ if (isInAutoWorktree(basePath))
428
+ return;
429
+ const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js");
430
+ prepareWorkflowMcpForProject(ctx, basePath);
431
+ }
421
432
  export function registerHooks(pi, ecosystemHandlers) {
422
433
  // ADR-005 Phase 3b: surface pi-ai ProviderSwitchReport via audit, notification, and counter.
423
434
  // Idempotent — only the first registerHooks call installs.
@@ -438,12 +449,7 @@ export function registerHooks(pi, ecosystemHandlers) {
438
449
  await syncServiceTierStatus(ctx);
439
450
  await applyDisabledModelProviderPolicy(ctx);
440
451
  await applyCompactionThresholdOverride(ctx);
441
- // Skip MCP auto-prep when running inside an auto-worktree (see session_switch below).
442
- const { isInAutoWorktree } = await import("../auto-worktree.js");
443
- if (!isInAutoWorktree(basePath)) {
444
- const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js");
445
- prepareWorkflowMcpForProject(ctx, basePath);
446
- }
452
+ await prepareWorkflowMcpForHookContext(ctx, basePath);
447
453
  // Apply show_token_cost preference (#1515)
448
454
  try {
449
455
  const { loadEffectiveGSDPreferences } = await import("../preferences.js");
@@ -468,15 +474,7 @@ export function registerHooks(pi, ecosystemHandlers) {
468
474
  await syncServiceTierStatus(ctx);
469
475
  await applyDisabledModelProviderPolicy(ctx);
470
476
  await applyCompactionThresholdOverride(ctx);
471
- // Skip MCP auto-prep when running inside an auto-worktree. The worktree
472
- // already has .mcp.json from createAutoWorktree, and re-running the writer
473
- // post-chdir rewrites the file mid-run (non-idempotent due to cwd-relative
474
- // CLI path resolution), dirtying the tree and breaking the milestone merge.
475
- const { isInAutoWorktree } = await import("../auto-worktree.js");
476
- if (!isInAutoWorktree(basePath)) {
477
- const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js");
478
- prepareWorkflowMcpForProject(ctx, basePath);
479
- }
477
+ await prepareWorkflowMcpForHookContext(ctx, basePath);
480
478
  await loadToolApiKeysForSession();
481
479
  if (!isAutoActive()) {
482
480
  ctx.ui.setWidget("gsd-progress", undefined);
@@ -511,6 +509,10 @@ export function registerHooks(pi, ecosystemHandlers) {
511
509
  }
512
510
  }
513
511
  clearDeferredApprovalGate(beforeAgentBasePath);
512
+ // session_start can fire before the active provider has settled. By
513
+ // before_agent_start, Claude Code CLI sessions should get the same
514
+ // project MCP config that /gsd mcp init would write.
515
+ await prepareWorkflowMcpForHookContext(ctx, beforeAgentBasePath);
514
516
  // GSD's own context injection (existing behavior — unchanged).
515
517
  const { buildBeforeAgentStartResult } = await import("./system-context.js");
516
518
  const gsdResult = await buildBeforeAgentStartResult(event, ctx);
@@ -17,6 +17,7 @@ import { isAutoActive, checkRemoteAutoSession } from "./auto.js";
17
17
  import { getAutoWorktreePath } from "./auto-worktree.js";
18
18
  import { currentDirectoryRoot, projectRoot } from "./commands/context.js";
19
19
  import { loadPrompt } from "./prompt-loader.js";
20
+ import { isPnpmInstall } from "../../shared/package-manager-detection.js";
20
21
  import { buildDoctorHealIssuePayload, buildDoctorHealSummary, buildWorkflowDispatchContent, } from "./workflow-protocol.js";
21
22
  import { restoreGsdWorkflowTools, scopeGsdWorkflowToolsForDispatch, } from "./bootstrap/register-hooks.js";
22
23
  const UPDATE_REGISTRY_URL = "https://registry.npmjs.org/@opengsd%2fgsd-pi/latest";
@@ -42,6 +43,8 @@ function isBunInstall(argv1 = process.argv[1]) {
42
43
  function resolveInstallCommand(pkg) {
43
44
  if (isBunInstall())
44
45
  return `bun add -g ${pkg}`;
46
+ if (isPnpmInstall())
47
+ return `pnpm add -g ${pkg}`;
45
48
  return `npm install -g ${pkg}`;
46
49
  }
47
50
  async function fetchLatestVersionForCommand() {
@@ -0,0 +1,117 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Lightweight worktree post-create hook runner.
3
+ import { execFileSync } from "node:child_process";
4
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { isAbsolute, join } from "node:path";
7
+ import { parse as parseYaml } from "yaml";
8
+ import { gsdHome } from "./gsd-home.js";
9
+ import { gsdRoot } from "./paths.js";
10
+ function readPreferencesObject(path) {
11
+ if (!existsSync(path))
12
+ return null;
13
+ const content = readFileSync(path, "utf-8");
14
+ try {
15
+ const startMarker = content.startsWith("---\r\n") ? "---\r\n" : "---\n";
16
+ if (content.startsWith(startMarker)) {
17
+ const searchStart = startMarker.length;
18
+ const endIdx = content.indexOf("\n---", searchStart);
19
+ if (endIdx === -1)
20
+ return null;
21
+ const parsed = parseYaml(content.slice(searchStart, endIdx).replace(/\r/g, ""));
22
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
23
+ ? parsed
24
+ : null;
25
+ }
26
+ const gitLines = [];
27
+ let inGitSection = false;
28
+ for (const rawLine of content.split("\n")) {
29
+ const line = rawLine.replace(/\r$/, "");
30
+ const heading = line.match(/^##\s+(.+)$/);
31
+ if (heading) {
32
+ inGitSection = heading[1].trim().toLowerCase().replace(/\s+/g, "_") === "git";
33
+ continue;
34
+ }
35
+ if (inGitSection && line.trim() && !line.trimStart().startsWith("#")) {
36
+ gitLines.push(line.replace(/^\s*-\s*/, ""));
37
+ }
38
+ }
39
+ if (gitLines.length === 0)
40
+ return null;
41
+ const parsed = parseYaml(gitLines.join("\n"));
42
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
43
+ ? { git: parsed }
44
+ : null;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ function extractHookPath(preferences) {
51
+ const git = preferences?.git;
52
+ if (!git || typeof git !== "object" || Array.isArray(git))
53
+ return null;
54
+ const hookPath = git.worktree_post_create;
55
+ return typeof hookPath === "string" && hookPath.trim() ? hookPath : null;
56
+ }
57
+ function resolveConfiguredHookPath(sourceDir) {
58
+ const paths = [
59
+ join(homedir(), ".pi", "agent", "gsd-preferences.md"),
60
+ join(gsdHome(), "preferences.md"),
61
+ join(gsdHome(), "PREFERENCES.md"),
62
+ join(gsdRoot(sourceDir), "preferences.md"),
63
+ join(gsdRoot(sourceDir), "PREFERENCES.md"),
64
+ ];
65
+ let hookPath = null;
66
+ for (const path of paths) {
67
+ hookPath = extractHookPath(readPreferencesObject(path)) ?? hookPath;
68
+ }
69
+ return hookPath;
70
+ }
71
+ /**
72
+ * Run the user-configured post-create hook script after worktree creation.
73
+ * The script receives SOURCE_DIR and WORKTREE_DIR as environment variables.
74
+ * Failure is non-fatal -- returns the error message or null on success.
75
+ *
76
+ * Reads git.worktree_post_create from effective global/project preferences
77
+ * unless hookPath is provided directly.
78
+ */
79
+ export function runWorktreePostCreateHook(sourceDir, worktreeDir, hookPath) {
80
+ if (hookPath === undefined) {
81
+ hookPath = resolveConfiguredHookPath(sourceDir) ?? undefined;
82
+ }
83
+ if (!hookPath)
84
+ return null;
85
+ let resolved = isAbsolute(hookPath) ? hookPath : join(sourceDir, hookPath);
86
+ if (!existsSync(resolved)) {
87
+ return `Worktree post-create hook not found: ${resolved}`;
88
+ }
89
+ if (process.platform === "win32") {
90
+ try {
91
+ resolved = realpathSync.native(resolved);
92
+ }
93
+ catch {
94
+ // Keep the original path; the exec error below will include the failure.
95
+ }
96
+ }
97
+ try {
98
+ const needsShell = process.platform === "win32" && /\.(bat|cmd)$/i.test(resolved);
99
+ execFileSync(resolved, [], {
100
+ cwd: worktreeDir,
101
+ env: {
102
+ ...process.env,
103
+ SOURCE_DIR: sourceDir,
104
+ WORKTREE_DIR: worktreeDir,
105
+ },
106
+ stdio: ["ignore", "pipe", "pipe"],
107
+ encoding: "utf-8",
108
+ timeout: 30_000,
109
+ shell: needsShell,
110
+ });
111
+ return null;
112
+ }
113
+ catch (err) {
114
+ const msg = err instanceof Error ? err.message : String(err);
115
+ return `Worktree post-create hook failed: ${msg}`;
116
+ }
117
+ }
@@ -0,0 +1,36 @@
1
+ import { homedir } from 'node:os';
2
+ import { join, resolve as resolvePath, sep } from 'node:path';
3
+ function hasPnpmPath(value) {
4
+ if (!value)
5
+ return false;
6
+ const normalized = value.replace(/\\/g, '/').toLowerCase();
7
+ return (normalized.includes('/.pnpm/') ||
8
+ normalized.endsWith('/pnpm') ||
9
+ normalized.endsWith('/pnpm.cjs') ||
10
+ normalized.endsWith('/pnpm.js'));
11
+ }
12
+ function pathStartsWith(pathValue, dir) {
13
+ if (!pathValue)
14
+ return false;
15
+ const resolvedPath = resolvePath(pathValue);
16
+ const resolvedDir = resolvePath(dir);
17
+ return resolvedPath === resolvedDir || resolvedPath.startsWith(resolvedDir + sep);
18
+ }
19
+ // Shared by update-check.ts and gsd command handlers. The JS installer keeps a
20
+ // parallel copy because it runs before TypeScript output exists.
21
+ export function isPnpmInstall(argv1 = process.argv[1], env = process.env) {
22
+ if (env.npm_config_user_agent?.startsWith('pnpm/'))
23
+ return true;
24
+ if (hasPnpmPath(env.npm_execpath))
25
+ return true;
26
+ if (hasPnpmPath(argv1))
27
+ return true;
28
+ if (!argv1)
29
+ return false;
30
+ const pnpmBinDirs = [];
31
+ if (env.PNPM_HOME)
32
+ pnpmBinDirs.push(env.PNPM_HOME);
33
+ pnpmBinDirs.push(join(homedir(), 'Library', 'pnpm'));
34
+ pnpmBinDirs.push(join(homedir(), '.local', 'share', 'pnpm'));
35
+ return pnpmBinDirs.some((dir) => pathStartsWith(argv1, dir) || pathStartsWith(env.npm_execpath, dir));
36
+ }
@@ -1,3 +1,5 @@
1
+ import { isPnpmInstall } from './resources/shared/package-manager-detection.js';
2
+ export { isPnpmInstall };
1
3
  interface UpdateCheckCache {
2
4
  lastCheck: number;
3
5
  latestVersion: string;
@@ -22,7 +24,10 @@ export declare function fetchLatestVersionFromRegistry(registryUrl?: string, fet
22
24
  * (PR #4147) misses this path. Inspect the unresolved invocation path instead.
23
25
  */
24
26
  export declare function isBunInstall(argv1?: string | undefined): boolean;
25
- export declare function resolveInstallCommand(pkg: string): string;
27
+ export declare function resolveInstallCommand(pkg: string, options?: {
28
+ argv1?: string;
29
+ env?: NodeJS.ProcessEnv;
30
+ }): string;
26
31
  export interface UpdateCheckOptions {
27
32
  currentVersion?: string;
28
33
  cachePath?: string;
@@ -45,4 +50,3 @@ export declare function checkForUpdates(options?: UpdateCheckOptions): Promise<v
45
50
  * Returns true if an update was performed, false otherwise.
46
51
  */
47
52
  export declare function checkAndPromptForUpdates(options?: UpdateCheckOptions): Promise<boolean>;
48
- export {};
@@ -1,9 +1,11 @@
1
1
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { execSync } from 'node:child_process';
2
3
  import { dirname, join, resolve as resolvePath, sep } from 'node:path';
3
4
  import { homedir } from 'node:os';
4
5
  import chalk from 'chalk';
5
6
  import { appRoot } from './app-paths.js';
6
- import { execSync } from 'node:child_process';
7
+ import { isPnpmInstall } from './resources/shared/package-manager-detection.js';
8
+ export { isPnpmInstall };
7
9
  const CACHE_FILE = join(appRoot, '.update-check');
8
10
  const NPM_PACKAGE_NAME = '@opengsd/gsd-pi';
9
11
  const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
@@ -91,9 +93,11 @@ export function isBunInstall(argv1 = process.argv[1]) {
91
93
  const resolved = resolvePath(argv1);
92
94
  return bunBinDirs.some((dir) => resolved.startsWith(resolvePath(dir) + sep));
93
95
  }
94
- export function resolveInstallCommand(pkg) {
95
- if (isBunInstall())
96
+ export function resolveInstallCommand(pkg, options = {}) {
97
+ if (isBunInstall(options.argv1))
96
98
  return `bun add -g ${pkg}`;
99
+ if (isPnpmInstall(options.argv1, options.env))
100
+ return `pnpm add -g ${pkg}`;
97
101
  return `npm install -g ${pkg}`;
98
102
  }
99
103
  function printUpdateBanner(current, latest) {
@@ -1 +1 @@
1
- 4NVKiVx4C-8FUT9A7DZdq
1
+ BVrLsL82ynrLee5zxeihC
@@ -2,13 +2,13 @@
2
2
  "/_not-found/page": "/_not-found",
3
3
  "/_global-error/page": "/_global-error",
4
4
  "/api/boot/route": "/api/boot",
5
+ "/api/bridge-terminal/input/route": "/api/bridge-terminal/input",
5
6
  "/api/bridge-terminal/resize/route": "/api/bridge-terminal/resize",
6
7
  "/api/bridge-terminal/stream/route": "/api/bridge-terminal/stream",
8
+ "/api/browse-directories/route": "/api/browse-directories",
9
+ "/api/dev-mode/route": "/api/dev-mode",
7
10
  "/api/cleanup/route": "/api/cleanup",
8
11
  "/api/captures/route": "/api/captures",
9
- "/api/dev-mode/route": "/api/dev-mode",
10
- "/api/browse-directories/route": "/api/browse-directories",
11
- "/api/bridge-terminal/input/route": "/api/bridge-terminal/input",
12
12
  "/api/doctor/route": "/api/doctor",
13
13
  "/api/export-data/route": "/api/export-data",
14
14
  "/api/experimental/route": "/api/experimental",
@@ -18,23 +18,23 @@
18
18
  "/api/hooks/route": "/api/hooks",
19
19
  "/api/inspect/route": "/api/inspect",
20
20
  "/api/knowledge/route": "/api/knowledge",
21
+ "/api/files/route": "/api/files",
21
22
  "/api/live-state/route": "/api/live-state",
22
23
  "/api/mcp-connections/route": "/api/mcp-connections",
23
24
  "/api/notifications/route": "/api/notifications",
24
- "/api/files/route": "/api/files",
25
- "/api/preferences/route": "/api/preferences",
26
25
  "/api/onboarding/route": "/api/onboarding",
26
+ "/api/preferences/route": "/api/preferences",
27
27
  "/api/recovery/route": "/api/recovery",
28
28
  "/api/projects/route": "/api/projects",
29
29
  "/api/session/browser/route": "/api/session/browser",
30
30
  "/api/session/command/route": "/api/session/command",
31
31
  "/api/session/events/route": "/api/session/events",
32
- "/api/settings-data/route": "/api/settings-data",
33
32
  "/api/session/manage/route": "/api/session/manage",
33
+ "/api/settings-data/route": "/api/settings-data",
34
34
  "/api/shutdown/route": "/api/shutdown",
35
- "/api/remote-questions/route": "/api/remote-questions",
36
35
  "/api/skill-health/route": "/api/skill-health",
37
36
  "/api/steer/route": "/api/steer",
37
+ "/api/remote-questions/route": "/api/remote-questions",
38
38
  "/api/terminal/input/route": "/api/terminal/input",
39
39
  "/api/switch-root/route": "/api/switch-root",
40
40
  "/api/terminal/resize/route": "/api/terminal/resize",
@@ -4,8 +4,8 @@
4
4
  ],
5
5
  "devFiles": [],
6
6
  "lowPriorityFiles": [
7
- "static/4NVKiVx4C-8FUT9A7DZdq/_buildManifest.js",
8
- "static/4NVKiVx4C-8FUT9A7DZdq/_ssgManifest.js"
7
+ "static/BVrLsL82ynrLee5zxeihC/_buildManifest.js",
8
+ "static/BVrLsL82ynrLee5zxeihC/_ssgManifest.js"
9
9
  ],
10
10
  "rootMainFiles": [
11
11
  "static/chunks/webpack-41a6d3c17ba63b62.js",