gsd-pi 2.78.1-dev.84a383f51 → 2.78.1-dev.8a893322c

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 (147) hide show
  1. package/README.md +1 -0
  2. package/dist/bundled-resource-path.d.ts +7 -0
  3. package/dist/bundled-resource-path.js +34 -2
  4. package/dist/claude-cli-check.js +18 -6
  5. package/dist/headless-query.js +21 -6
  6. package/dist/loader.js +2 -3
  7. package/dist/resource-loader.js +2 -8
  8. package/dist/resources/.managed-resources-content-hash +1 -1
  9. package/dist/resources/extensions/claude-code-cli/readiness.js +19 -7
  10. package/dist/resources/extensions/google-search/index.js +2 -6
  11. package/dist/resources/extensions/gsd/auto/phases.js +3 -11
  12. package/dist/resources/extensions/gsd/auto/session.js +2 -6
  13. package/dist/resources/extensions/gsd/auto-dashboard.js +3 -2
  14. package/dist/resources/extensions/gsd/auto-dispatch.js +18 -6
  15. package/dist/resources/extensions/gsd/auto-prompts.js +63 -2
  16. package/dist/resources/extensions/gsd/auto-worktree.js +30 -13
  17. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +19 -1
  18. package/dist/resources/extensions/gsd/bootstrap/subagent-input.js +22 -0
  19. package/dist/resources/extensions/gsd/bootstrap/system-context.js +11 -0
  20. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +84 -2
  21. package/dist/resources/extensions/gsd/commands/catalog.js +8 -1
  22. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
  23. package/dist/resources/extensions/gsd/commands/handlers/ops.js +8 -0
  24. package/dist/resources/extensions/gsd/commands-config.js +3 -2
  25. package/dist/resources/extensions/gsd/commands-extensions.js +46 -3
  26. package/dist/resources/extensions/gsd/commands-handlers.js +3 -2
  27. package/dist/resources/extensions/gsd/commands-worktree.js +309 -0
  28. package/dist/resources/extensions/gsd/docs/preferences-reference.md +6 -0
  29. package/dist/resources/extensions/gsd/doctor-providers.js +2 -1
  30. package/dist/resources/extensions/gsd/forensics.js +8 -6
  31. package/dist/resources/extensions/gsd/guided-flow.js +2 -1
  32. package/dist/resources/extensions/gsd/home-dir.js +16 -0
  33. package/dist/resources/extensions/gsd/key-manager.js +2 -1
  34. package/dist/resources/extensions/gsd/migrate/command.js +3 -2
  35. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +10 -0
  36. package/dist/resources/extensions/gsd/prompts/complete-slice.md +10 -0
  37. package/dist/resources/extensions/gsd/prompts/plan-slice.md +10 -0
  38. package/dist/resources/extensions/gsd/prompts/refine-slice.md +10 -0
  39. package/dist/resources/extensions/gsd/unit-context-manifest.js +29 -4
  40. package/dist/resources/extensions/gsd/worktree-manager.js +20 -1
  41. package/dist/resources/extensions/gsd/worktree-resolver.js +4 -13
  42. package/dist/resources/extensions/gsd/worktree-root.js +124 -0
  43. package/dist/resources/extensions/gsd/worktree.js +4 -115
  44. package/dist/resources/extensions/mcp-client/index.js +0 -6
  45. package/dist/resources/extensions/ollama/index.js +15 -2
  46. package/dist/resources/extensions/ollama/model-capabilities.js +31 -0
  47. package/dist/resources/extensions/ollama/ollama-client.js +40 -4
  48. package/dist/resources/extensions/subagent/index.js +324 -178
  49. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  50. package/dist/web/standalone/.next/BUILD_ID +1 -1
  51. package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
  52. package/dist/web/standalone/.next/build-manifest.json +2 -2
  53. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  54. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.html +1 -1
  71. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
  78. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  80. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  81. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  82. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  83. package/dist/welcome-screen.js +27 -1
  84. package/dist/worktree-cli.d.ts +1 -0
  85. package/dist/worktree-cli.js +9 -3
  86. package/package.json +1 -3
  87. package/packages/mcp-server/src/workflow-tools.test.ts +52 -0
  88. package/packages/native/tsconfig.tsbuildinfo +1 -1
  89. package/src/resources/extensions/claude-code-cli/readiness.ts +20 -7
  90. package/src/resources/extensions/google-search/index.ts +2 -9
  91. package/src/resources/extensions/gsd/auto/phases.ts +3 -11
  92. package/src/resources/extensions/gsd/auto/session.ts +2 -6
  93. package/src/resources/extensions/gsd/auto-dashboard.ts +3 -2
  94. package/src/resources/extensions/gsd/auto-dispatch.ts +18 -6
  95. package/src/resources/extensions/gsd/auto-prompts.ts +60 -2
  96. package/src/resources/extensions/gsd/auto-worktree.ts +44 -12
  97. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +19 -0
  98. package/src/resources/extensions/gsd/bootstrap/subagent-input.ts +20 -0
  99. package/src/resources/extensions/gsd/bootstrap/system-context.ts +11 -0
  100. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +103 -1
  101. package/src/resources/extensions/gsd/commands/catalog.ts +8 -1
  102. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
  103. package/src/resources/extensions/gsd/commands/handlers/ops.ts +10 -0
  104. package/src/resources/extensions/gsd/commands-config.ts +3 -2
  105. package/src/resources/extensions/gsd/commands-extensions.ts +43 -3
  106. package/src/resources/extensions/gsd/commands-handlers.ts +3 -2
  107. package/src/resources/extensions/gsd/commands-worktree.ts +383 -0
  108. package/src/resources/extensions/gsd/docs/preferences-reference.md +6 -0
  109. package/src/resources/extensions/gsd/doctor-providers.ts +2 -1
  110. package/src/resources/extensions/gsd/forensics.ts +10 -5
  111. package/src/resources/extensions/gsd/guided-flow.ts +2 -1
  112. package/src/resources/extensions/gsd/home-dir.ts +19 -0
  113. package/src/resources/extensions/gsd/journal.ts +4 -1
  114. package/src/resources/extensions/gsd/key-manager.ts +2 -1
  115. package/src/resources/extensions/gsd/migrate/command.ts +3 -2
  116. package/src/resources/extensions/gsd/prompts/complete-milestone.md +10 -0
  117. package/src/resources/extensions/gsd/prompts/complete-slice.md +10 -0
  118. package/src/resources/extensions/gsd/prompts/plan-slice.md +10 -0
  119. package/src/resources/extensions/gsd/prompts/refine-slice.md +10 -0
  120. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +15 -0
  121. package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +50 -27
  122. package/src/resources/extensions/gsd/tests/commands-extensions-version-compare.test.ts +58 -0
  123. package/src/resources/extensions/gsd/tests/commands-worktree-clean.test.ts +48 -0
  124. package/src/resources/extensions/gsd/tests/google-search-stub.test.ts +25 -65
  125. package/src/resources/extensions/gsd/tests/home-dir.test.ts +52 -0
  126. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +50 -1
  127. package/src/resources/extensions/gsd/tests/milestone-report-path.test.ts +18 -1
  128. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +34 -0
  129. package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +17 -1
  130. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +38 -3
  131. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +34 -33
  132. package/src/resources/extensions/gsd/tests/worktree.test.ts +8 -0
  133. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +116 -1
  134. package/src/resources/extensions/gsd/unit-context-manifest.ts +36 -4
  135. package/src/resources/extensions/gsd/worktree-manager.ts +40 -1
  136. package/src/resources/extensions/gsd/worktree-resolver.ts +4 -14
  137. package/src/resources/extensions/gsd/worktree-root.ts +144 -0
  138. package/src/resources/extensions/gsd/worktree.ts +8 -119
  139. package/src/resources/extensions/mcp-client/index.ts +0 -7
  140. package/src/resources/extensions/ollama/index.ts +16 -2
  141. package/src/resources/extensions/ollama/model-capabilities.ts +34 -0
  142. package/src/resources/extensions/ollama/ollama-client.ts +41 -4
  143. package/src/resources/extensions/ollama/tests/model-capabilities.test.ts +96 -0
  144. package/src/resources/extensions/ollama/tests/ollama-client-timeout-env.test.ts +147 -0
  145. package/src/resources/extensions/subagent/index.ts +165 -7
  146. /package/dist/web/standalone/.next/static/{UF5VF4F1tB0miEtJS7LyX → QK8fABiGPmonfTgboN0Y9}/_buildManifest.js +0 -0
  147. /package/dist/web/standalone/.next/static/{UF5VF4F1tB0miEtJS7LyX → QK8fABiGPmonfTgboN0Y9}/_ssgManifest.js +0 -0
@@ -21,6 +21,7 @@ import { GSDError, GSD_PARSE_ERROR, GSD_STALE_STATE, GSD_LOCK_HELD, GSD_GIT_ERRO
21
21
  import { logWarning } from "./workflow-logger.js";
22
22
  import { nativeBranchDelete, nativeBranchExists, nativeBranchForceReset, nativeCommit, nativeDetectMainBranch, nativeDiffContent, nativeDiffNameStatus, nativeDiffNumstat, nativeGetCurrentBranch, nativeIsAncestor, nativeLogOneline, nativeMergeSquash, nativeWorktreeAdd, nativeWorktreeList, nativeWorktreePrune, nativeWorktreeRemove, } from "./native-git-bridge.js";
23
23
  import { emitCanonicalRootRedirect } from "./worktree-telemetry.js";
24
+ import { isGsdWorktreePath, normalizeWorktreePathForCompare, resolveWorktreeProjectRoot, } from "./worktree-root.js";
24
25
  // ─── Path Helpers ──────────────────────────────────────────────────────────
25
26
  function normalizePathForComparison(path) {
26
27
  const normalized = path
@@ -29,6 +30,14 @@ function normalizePathForComparison(path) {
29
30
  .replace(/\/+$/, "");
30
31
  return process.platform === "win32" ? normalized.toLowerCase() : normalized;
31
32
  }
33
+ function normalizeBasePathForWorktreeOps(basePath) {
34
+ const resolved = resolveWorktreeProjectRoot(basePath);
35
+ if (isGsdWorktreePath(basePath) &&
36
+ normalizeWorktreePathForCompare(resolved) === normalizeWorktreePathForCompare(basePath)) {
37
+ throw new GSDError(GSD_GIT_ERROR, `Cannot resolve project root from worktree path: ${basePath}. Run the command from the project root or set GSD_PROJECT_ROOT.`);
38
+ }
39
+ return resolved;
40
+ }
32
41
  // ─── resolveGitDir ─────────────────────────────────────────────────────────
33
42
  /**
34
43
  * Resolve the actual git directory for a given repository path.
@@ -61,7 +70,7 @@ export function resolveGitDir(basePath) {
61
70
  return gitPath;
62
71
  }
63
72
  export function worktreesDir(basePath) {
64
- return join(basePath, ".gsd", "worktrees");
73
+ return join(resolveWorktreeProjectRoot(basePath), ".gsd", "worktrees");
65
74
  }
66
75
  export function worktreePath(basePath, name) {
67
76
  return join(worktreesDir(basePath), name);
@@ -143,6 +152,7 @@ export function resolveCanonicalMilestoneRoot(basePath, milestoneId) {
143
152
  * @param opts.branch — override the default `worktree/<name>` branch name
144
153
  */
145
154
  export function createWorktree(basePath, name, opts = {}) {
155
+ basePath = normalizeBasePathForWorktreeOps(basePath);
146
156
  // Validate name: alphanumeric, hyphens, underscores only
147
157
  if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
148
158
  throw new GSDError(GSD_PARSE_ERROR, `Invalid worktree name "${name}". Use only letters, numbers, hyphens, and underscores.`);
@@ -227,6 +237,7 @@ export function createWorktree(basePath, name, opts = {}) {
227
237
  * Uses native worktree list and filters to those under .gsd/worktrees/.
228
238
  */
229
239
  export function listWorktrees(basePath) {
240
+ basePath = normalizeBasePathForWorktreeOps(basePath);
230
241
  const baseVariants = [resolve(basePath)];
231
242
  if (existsSync(basePath)) {
232
243
  baseVariants.push(realpathSync(basePath));
@@ -366,6 +377,7 @@ export function findNestedGitDirs(rootPath) {
366
377
  * If the process is currently inside the worktree, chdir out first.
367
378
  */
368
379
  export function removeWorktree(basePath, name, opts = {}) {
380
+ basePath = normalizeBasePathForWorktreeOps(basePath);
369
381
  let wtPath = worktreePath(basePath, name);
370
382
  const branch = opts.branch ?? worktreeBranchName(name);
371
383
  const { deleteBranch = true, force = true } = opts;
@@ -614,6 +626,7 @@ function parseDiffNameStatus(entries) {
614
626
  * Returns a summary of added, modified, and removed GSD artifacts.
615
627
  */
616
628
  export function diffWorktreeGSD(basePath, name) {
629
+ basePath = normalizeBasePathForWorktreeOps(basePath);
617
630
  const branch = worktreeBranchName(name);
618
631
  const mainBranch = nativeDetectMainBranch(basePath);
619
632
  const entries = nativeDiffNameStatus(basePath, mainBranch, branch, ".gsd/", true);
@@ -626,6 +639,7 @@ export function diffWorktreeGSD(basePath, name) {
626
639
  * content, this correctly returns an empty diff.
627
640
  */
628
641
  export function diffWorktreeAll(basePath, name) {
642
+ basePath = normalizeBasePathForWorktreeOps(basePath);
629
643
  const branch = worktreeBranchName(name);
630
644
  const mainBranch = nativeDetectMainBranch(basePath);
631
645
  const entries = nativeDiffNameStatus(basePath, mainBranch, branch);
@@ -636,6 +650,7 @@ export function diffWorktreeAll(basePath, name) {
636
650
  * Uses direct diff (not merge-base) so the preview matches the actual merge outcome.
637
651
  */
638
652
  export function diffWorktreeNumstat(basePath, name) {
653
+ basePath = normalizeBasePathForWorktreeOps(basePath);
639
654
  const branch = worktreeBranchName(name);
640
655
  const mainBranch = nativeDetectMainBranch(basePath);
641
656
  const rawStats = nativeDiffNumstat(basePath, mainBranch, branch);
@@ -652,6 +667,7 @@ export function diffWorktreeNumstat(basePath, name) {
652
667
  * Returns the raw unified diff for LLM consumption.
653
668
  */
654
669
  export function getWorktreeGSDDiff(basePath, name) {
670
+ basePath = normalizeBasePathForWorktreeOps(basePath);
655
671
  const branch = worktreeBranchName(name);
656
672
  const mainBranch = nativeDetectMainBranch(basePath);
657
673
  return nativeDiffContent(basePath, mainBranch, branch, ".gsd/", undefined, true);
@@ -661,6 +677,7 @@ export function getWorktreeGSDDiff(basePath, name) {
661
677
  * Returns the raw unified diff for LLM consumption.
662
678
  */
663
679
  export function getWorktreeCodeDiff(basePath, name) {
680
+ basePath = normalizeBasePathForWorktreeOps(basePath);
664
681
  const branch = worktreeBranchName(name);
665
682
  const mainBranch = nativeDetectMainBranch(basePath);
666
683
  return nativeDiffContent(basePath, mainBranch, branch, undefined, ".gsd/", true);
@@ -669,6 +686,7 @@ export function getWorktreeCodeDiff(basePath, name) {
669
686
  * Get commit log for the worktree branch since it diverged from main.
670
687
  */
671
688
  export function getWorktreeLog(basePath, name) {
689
+ basePath = normalizeBasePathForWorktreeOps(basePath);
672
690
  const branch = worktreeBranchName(name);
673
691
  const mainBranch = nativeDetectMainBranch(basePath);
674
692
  const entries = nativeLogOneline(basePath, mainBranch, branch);
@@ -680,6 +698,7 @@ export function getWorktreeLog(basePath, name) {
680
698
  * Returns the merge commit message.
681
699
  */
682
700
  export function mergeWorktreeToMain(basePath, name, commitMessage) {
701
+ basePath = normalizeBasePathForWorktreeOps(basePath);
683
702
  const branch = worktreeBranchName(name);
684
703
  const mainBranch = nativeDetectMainBranch(basePath);
685
704
  const current = nativeGetCurrentBranch(basePath);
@@ -20,28 +20,19 @@ import { emitJournalEvent } from "./journal.js";
20
20
  import { emitWorktreeCreated, emitWorktreeMerged } from "./worktree-telemetry.js";
21
21
  import { getCollapseCadence, getMilestoneResquash, resquashMilestoneOnMain } from "./slice-cadence.js";
22
22
  import { loadEffectiveGSDPreferences } from "./preferences.js";
23
+ import { resolveWorktreeProjectRoot } from "./worktree-root.js";
23
24
  // ─── Path Helpers ──────────────────────────────────────────────────────────
24
- /**
25
- * Worktree marker segment — present in any path produced by worktreePath().
26
- * Used to strip the worktree suffix and recover the project root (#3729).
27
- */
28
- const WORKTREE_MARKER = "/.gsd/worktrees/";
29
25
  /**
30
26
  * Resolve the project root from session path state.
31
27
  *
32
28
  * Prefers `originalBasePath` (always the project root when set), but falls
33
29
  * back to `basePath` when `originalBasePath` is falsy (e.g. fresh AutoSession
34
30
  * with default empty string). If `basePath` itself is inside a worktree
35
- * directory (contains `/.gsd/worktrees/`), strip that suffix to recover the
36
- * actual project root preventing double-nested worktree paths (#3729).
31
+ * directory (including symlink-resolved ~/.gsd/projects/<hash>/worktrees
32
+ * paths), recover the actual project root to prevent double nesting (#3729).
37
33
  */
38
34
  export function resolveProjectRoot(originalBasePath, basePath) {
39
- let resolved = originalBasePath || basePath;
40
- const markerIdx = resolved.indexOf(WORKTREE_MARKER);
41
- if (markerIdx !== -1) {
42
- resolved = resolved.slice(0, markerIdx);
43
- }
44
- return resolved;
35
+ return resolveWorktreeProjectRoot(basePath, originalBasePath);
45
36
  }
46
37
  // ─── WorktreeResolver ──────────────────────────────────────────────────────
47
38
  export class WorktreeResolver {
@@ -0,0 +1,124 @@
1
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ export function normalizeWorktreePathForCompare(path) {
5
+ let normalized;
6
+ try {
7
+ normalized = realpathSync(path);
8
+ }
9
+ catch {
10
+ normalized = resolve(path);
11
+ }
12
+ const slashed = normalized.replaceAll("\\", "/");
13
+ const trimmed = slashed.replace(/\/+$/, "");
14
+ return process.platform === "win32" ? (trimmed || "/").toLowerCase() : (trimmed || "/");
15
+ }
16
+ /**
17
+ * Find the GSD worktree segment in both direct project layout and the
18
+ * symlink-resolved external-state layout used by ~/.gsd/projects/<hash>.
19
+ */
20
+ export function findWorktreeSegment(normalizedPath) {
21
+ const directMarker = "/.gsd/worktrees/";
22
+ const directIdx = normalizedPath.indexOf(directMarker);
23
+ if (directIdx !== -1) {
24
+ return { gsdIdx: directIdx, afterWorktrees: directIdx + directMarker.length };
25
+ }
26
+ const externalRe = /\/\.gsd\/projects\/[^/]+\/worktrees\//;
27
+ const externalMatch = normalizedPath.match(externalRe);
28
+ if (externalMatch && externalMatch.index !== undefined) {
29
+ return {
30
+ gsdIdx: externalMatch.index,
31
+ afterWorktrees: externalMatch.index + externalMatch[0].length,
32
+ };
33
+ }
34
+ return null;
35
+ }
36
+ export function isGsdWorktreePath(path) {
37
+ return findWorktreeSegment(path.replaceAll("\\", "/")) !== null;
38
+ }
39
+ /**
40
+ * Resolve the canonical project root for worktree operations.
41
+ *
42
+ * `originalBasePath` wins when available because session state already knows the
43
+ * root. `GSD_PROJECT_ROOT` is the next strongest signal for worker processes.
44
+ * Otherwise, derive the root from direct `.gsd/worktrees` paths, or recover it
45
+ * from the worktree `.git` file for symlink-resolved ~/.gsd/project paths.
46
+ */
47
+ export function resolveWorktreeProjectRoot(basePath, originalBasePath) {
48
+ const preferred = originalBasePath?.trim() ||
49
+ process.env.GSD_PROJECT_ROOT?.trim() ||
50
+ basePath;
51
+ return resolveProjectRootFromPath(preferred);
52
+ }
53
+ function resolveProjectRootFromPath(path) {
54
+ const normalizedPath = path.replaceAll("\\", "/");
55
+ const segment = findWorktreeSegment(normalizedPath);
56
+ if (!segment)
57
+ return resolveGitWorkingTreeRoot(path) ?? path;
58
+ const sepChar = path.includes("\\") ? "\\" : "/";
59
+ const gsdMarker = `${sepChar}.gsd${sepChar}`;
60
+ const markerIdx = path.indexOf(gsdMarker);
61
+ const candidate = markerIdx !== -1
62
+ ? path.slice(0, markerIdx)
63
+ : path.slice(0, segment.gsdIdx);
64
+ const gsdHome = normalizeWorktreePathForCompare(process.env.GSD_HOME || join(homedir(), ".gsd"));
65
+ const candidateGsdPath = normalizeWorktreePathForCompare(join(candidate, ".gsd"));
66
+ if (candidateGsdPath === gsdHome || candidateGsdPath.startsWith(`${gsdHome}/`)) {
67
+ const realRoot = resolveProjectRootFromGitFile(path);
68
+ return realRoot ?? path;
69
+ }
70
+ return candidate;
71
+ }
72
+ function resolveGitWorkingTreeRoot(path) {
73
+ try {
74
+ let dir = existsSync(path) && !statSync(path).isDirectory()
75
+ ? resolve(path, "..")
76
+ : path;
77
+ for (let i = 0; i < 30; i++) {
78
+ const gitPath = join(dir, ".git");
79
+ if (existsSync(gitPath))
80
+ return dir;
81
+ const parent = resolve(dir, "..");
82
+ if (parent === dir)
83
+ break;
84
+ dir = parent;
85
+ }
86
+ }
87
+ catch {
88
+ // Non-fatal: callers either keep the original path or fail closed.
89
+ }
90
+ return null;
91
+ }
92
+ function resolveProjectRootFromGitFile(worktreePath) {
93
+ try {
94
+ let dir = worktreePath;
95
+ for (let i = 0; i < 30; i++) {
96
+ const gitPath = join(dir, ".git");
97
+ if (existsSync(gitPath)) {
98
+ const content = readFileSync(gitPath, "utf8").trim();
99
+ if (content.startsWith("gitdir: ")) {
100
+ const gitDir = resolve(dir, content.slice(8));
101
+ const dotGitDir = resolve(gitDir, "..", "..");
102
+ if (dotGitDir.endsWith(".git") || dotGitDir.endsWith(".git/") || dotGitDir.endsWith(".git\\")) {
103
+ return resolve(dotGitDir, "..");
104
+ }
105
+ const commonDirPath = join(gitDir, "commondir");
106
+ if (existsSync(commonDirPath)) {
107
+ const commonDir = readFileSync(commonDirPath, "utf8").trim();
108
+ const resolvedCommonDir = resolve(gitDir, commonDir);
109
+ return resolve(resolvedCommonDir, "..");
110
+ }
111
+ }
112
+ break;
113
+ }
114
+ const parent = resolve(dir, "..");
115
+ if (parent === dir)
116
+ break;
117
+ dir = parent;
118
+ }
119
+ }
120
+ catch {
121
+ // Non-fatal: callers either keep the original path or fail closed.
122
+ }
123
+ return null;
124
+ }
@@ -11,11 +11,12 @@
11
11
  * Pure utility functions (detectWorktreeName, getSliceBranchName, parseSliceBranch,
12
12
  * SLICE_BRANCH_RE) remain for backwards compatibility with legacy branches.
13
13
  */
14
- import { existsSync, readFileSync, realpathSync, utimesSync } from "node:fs";
14
+ import { existsSync, readFileSync, utimesSync } from "node:fs";
15
15
  import { join, resolve } from "node:path";
16
- import { homedir } from "node:os";
17
16
  import { GitServiceImpl, writeIntegrationBranch } from "./git-service.js";
18
17
  import { loadEffectiveGSDPreferences } from "./preferences.js";
18
+ import { findWorktreeSegment, resolveWorktreeProjectRoot, } from "./worktree-root.js";
19
+ export { resolveWorktreeProjectRoot } from "./worktree-root.js";
19
20
  export { MergeConflictError } from "./git-service.js";
20
21
  // ─── Lazy GitServiceImpl Cache ─────────────────────────────────────────────
21
22
  let cachedService = null;
@@ -67,28 +68,6 @@ export function captureIntegrationBranch(basePath, milestoneId) {
67
68
  writeIntegrationBranch(basePath, milestoneId, current);
68
69
  }
69
70
  // ─── Pure Utility Functions (unchanged) ────────────────────────────────────
70
- /**
71
- * Find the worktrees segment in a path, supporting both direct
72
- * (`/.gsd/worktrees/`) and symlink-resolved (`/.gsd/projects/<hash>/worktrees/`)
73
- * layouts. When `.gsd` is a symlink to `~/.gsd/projects/<hash>`, resolved
74
- * paths contain the intermediate `projects/<hash>/` segment that the old
75
- * single-marker check missed.
76
- */
77
- function findWorktreeSegment(normalizedPath) {
78
- // Direct layout: /.gsd/worktrees/<name>
79
- const directMarker = "/.gsd/worktrees/";
80
- const idx = normalizedPath.indexOf(directMarker);
81
- if (idx !== -1) {
82
- return { gsdIdx: idx, afterWorktrees: idx + directMarker.length };
83
- }
84
- // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/<name>
85
- const symlinkRe = /\/\.gsd\/projects\/[a-f0-9]+\/worktrees\//;
86
- const match = normalizedPath.match(symlinkRe);
87
- if (match && match.index !== undefined) {
88
- return { gsdIdx: match.index, afterWorktrees: match.index + match[0].length };
89
- }
90
- return null;
91
- }
92
71
  /**
93
72
  * Detect the active worktree name from the current working directory.
94
73
  * Returns null if not inside a GSD worktree (.gsd/worktrees/<name>/).
@@ -121,97 +100,7 @@ export function detectWorktreeName(basePath) {
121
100
  * operate against the real project root, not a worktree subdirectory.
122
101
  */
123
102
  export function resolveProjectRoot(basePath) {
124
- // Layer 1: If the coordinator passed the real project root, use it.
125
- if (process.env.GSD_PROJECT_ROOT) {
126
- return process.env.GSD_PROJECT_ROOT;
127
- }
128
- const normalizedPath = basePath.replaceAll("\\", "/");
129
- const seg = findWorktreeSegment(normalizedPath);
130
- if (!seg)
131
- return basePath;
132
- // Candidate root via the string-slice heuristic
133
- const sepChar = basePath.includes("\\") ? "\\" : "/";
134
- const gsdMarker = `${sepChar}.gsd${sepChar}`;
135
- const gsdIdx = basePath.indexOf(gsdMarker);
136
- const candidate = gsdIdx !== -1
137
- ? basePath.slice(0, gsdIdx)
138
- : basePath.slice(0, seg.gsdIdx);
139
- // Layer 2: Guard against resolving to the user's home directory.
140
- // When .gsd is a symlink into ~/.gsd/projects/<hash>, the resolved path
141
- // contains /.gsd/ at the user-level boundary. Slicing there yields ~ — wrong.
142
- const gsdHome = normalizePathForCompare(process.env.GSD_HOME || join(homedir(), ".gsd"));
143
- const candidateGsdPath = normalizePathForCompare(join(candidate, ".gsd"));
144
- if (candidateGsdPath === gsdHome || candidateGsdPath.startsWith(gsdHome + "/")) {
145
- // The candidate is the home directory (or within it in a way that .gsd
146
- // maps to the user-level GSD dir). Try to recover the real project root
147
- // from the worktree's .git file.
148
- const realRoot = resolveProjectRootFromGitFile(basePath);
149
- if (realRoot)
150
- return realRoot;
151
- // If git file resolution failed, return basePath unchanged rather than ~
152
- return basePath;
153
- }
154
- return candidate;
155
- }
156
- /**
157
- * Recover the real project root from a worktree's .git file.
158
- *
159
- * Each git worktree has a `.git` file (not directory) containing:
160
- * gitdir: /real/project/.git/worktrees/<name>
161
- *
162
- * Walking up from that gitdir gives us `/real/project/.git`, and its
163
- * parent is the real project root.
164
- */
165
- function resolveProjectRootFromGitFile(worktreePath) {
166
- try {
167
- // Walk up from the worktree path to find the .git file
168
- let dir = worktreePath;
169
- for (let i = 0; i < 30; i++) {
170
- const gitPath = join(dir, ".git");
171
- if (existsSync(gitPath)) {
172
- const content = readFileSync(gitPath, "utf8").trim();
173
- if (content.startsWith("gitdir: ")) {
174
- // gitdir points to: <real-project>/.git/worktrees/<name>
175
- const gitDir = resolve(dir, content.slice(8));
176
- // Walk up: .git/worktrees/<name> → .git/worktrees → .git → project root
177
- const dotGitDir = resolve(gitDir, "..", "..");
178
- // Verify this looks like a .git directory
179
- if (dotGitDir.endsWith(".git") || dotGitDir.endsWith(".git/") || dotGitDir.endsWith(".git\\")) {
180
- return resolve(dotGitDir, "..");
181
- }
182
- // Alternative: the commondir file inside the worktree gitdir
183
- // points to the main .git directory
184
- const commonDirPath = join(gitDir, "commondir");
185
- if (existsSync(commonDirPath)) {
186
- const commonDir = readFileSync(commonDirPath, "utf8").trim();
187
- const resolvedCommonDir = resolve(gitDir, commonDir);
188
- return resolve(resolvedCommonDir, "..");
189
- }
190
- }
191
- break;
192
- }
193
- const parent = resolve(dir, "..");
194
- if (parent === dir)
195
- break;
196
- dir = parent;
197
- }
198
- }
199
- catch {
200
- // Non-fatal — caller will use fallback
201
- }
202
- return null;
203
- }
204
- function normalizePathForCompare(path) {
205
- let normalized;
206
- try {
207
- normalized = realpathSync(path);
208
- }
209
- catch {
210
- normalized = resolve(path);
211
- }
212
- const slashed = normalized.replaceAll("\\", "/");
213
- const trimmed = slashed.replace(/\/+$/, "");
214
- return trimmed || "/";
103
+ return resolveWorktreeProjectRoot(basePath);
215
104
  }
216
105
  /**
217
106
  * Get the slice branch name, namespaced by worktree when inside one.
@@ -503,12 +503,6 @@ export default function (pi) {
503
503
  },
504
504
  });
505
505
  // ── Lifecycle ─────────────────────────────────────────────────────────────
506
- pi.on("session_start", async (_event, ctx) => {
507
- const servers = readConfigs();
508
- if (servers.length > 0) {
509
- ctx.ui.notify(`MCP client ready — ${servers.length} server(s) configured`, "info");
510
- }
511
- });
512
506
  pi.on("session_shutdown", async () => {
513
507
  await closeAll();
514
508
  });
@@ -103,19 +103,32 @@ export default function ollama(pi) {
103
103
  // In headless/auto mode, await the probe so the fallback resolver can
104
104
  // see Ollama before the first LLM call (#3531 race condition).
105
105
  // In interactive mode, keep it async for fast startup.
106
+ // Surface probe failures under GSD_DEBUG so users can diagnose silent
107
+ // "Ollama is missing from /model" reports without patching dist/. The
108
+ // probe still soft-fails (registration is best-effort) — we just stop
109
+ // dropping the error on the floor. See #4982.
110
+ const debugOllama = (where, error) => {
111
+ if (process.env.GSD_DEBUG) {
112
+ const msg = error instanceof Error ? error.message : String(error);
113
+ process.stderr.write(`[ollama] ${where} probe failed: ${msg}\n`);
114
+ }
115
+ };
106
116
  if (!ctx.hasUI) {
107
117
  try {
108
118
  await probeAndRegister(pi);
109
119
  }
110
- catch { /* non-fatal */ }
120
+ catch (error) {
121
+ debugOllama("headless", error);
122
+ }
111
123
  }
112
124
  else {
113
125
  probeAndRegister(pi)
114
126
  .then((found) => {
115
127
  ctx.ui.setStatus("ollama", found ? "Ollama" : undefined);
116
128
  })
117
- .catch(() => {
129
+ .catch((error) => {
118
130
  ctx.ui.setStatus("ollama", undefined);
131
+ debugOllama("interactive", error);
119
132
  });
120
133
  }
121
134
  });
@@ -32,9 +32,40 @@ const KNOWN_MODELS = [
32
32
  ["llama3", { contextWindow: 8192, maxTokens: 8192, ollamaOptions: { num_ctx: 8192 } }],
33
33
  ["llama2", { contextWindow: 4096, maxTokens: 4096, ollamaOptions: { num_ctx: 4096 } }],
34
34
  // ─── Qwen family ────────────────────────────────────────────────────
35
+ // Long-variant entries MUST appear before the bare `qwen3` base —
36
+ // `baseName.startsWith(pattern)` returns true for `qwen3.5`/`qwen3-coder`/
37
+ // `qwen3-next` against `qwen3`, and the first match wins (#4991).
38
+ // ref: qwen3-next 1M ctx — https://qwen.ai/blog?id=qwen3-next
39
+ ["qwen3-next", { contextWindow: 1048576, maxTokens: 32768, ollamaOptions: { num_ctx: 1048576 } }],
40
+ // ref: qwen3-coder 256K ctx — https://qwenlm.github.io/blog/qwen3-coder/
41
+ ["qwen3-coder", { contextWindow: 262144, maxTokens: 32768, ollamaOptions: { num_ctx: 262144 } }],
42
+ // ref: qwen3.5 / qwen3.6 1M ctx — Ollama Cloud release notes
43
+ ["qwen3.6", { contextWindow: 1048576, maxTokens: 32768, ollamaOptions: { num_ctx: 1048576 } }],
44
+ ["qwen3.5", { contextWindow: 1048576, maxTokens: 32768, ollamaOptions: { num_ctx: 1048576 } }],
35
45
  ["qwen3", { contextWindow: 131072, maxTokens: 32768, ollamaOptions: { num_ctx: 131072 } }],
36
46
  ["qwen2.5", { contextWindow: 131072, maxTokens: 32768, ollamaOptions: { num_ctx: 131072 } }],
37
47
  ["qwen2", { contextWindow: 131072, maxTokens: 32768, ollamaOptions: { num_ctx: 131072 } }],
48
+ // ─── GLM family (Z.ai, Ollama Cloud) ────────────────────────────────
49
+ // ref: glm 4.6 / 5.x 200K ctx — https://docs.z.ai/devpack/using5.1
50
+ // Long-variant entries before bare `glm-5` / `glm-4` would-be bases to
51
+ // avoid prefix shadowing (#4991).
52
+ ["glm-5.1", { contextWindow: 204800, maxTokens: 16384, ollamaOptions: { num_ctx: 204800 } }],
53
+ ["glm-5", { contextWindow: 204800, maxTokens: 16384, ollamaOptions: { num_ctx: 204800 } }],
54
+ ["glm-4.6", { contextWindow: 204800, maxTokens: 16384, ollamaOptions: { num_ctx: 204800 } }],
55
+ ["glm-4", { contextWindow: 131072, maxTokens: 16384, ollamaOptions: { num_ctx: 131072 } }],
56
+ // ─── Kimi K2 (Moonshot, Ollama Cloud) ──────────────────────────────
57
+ // ref: kimi-k2 256K ctx — https://platform.moonshot.ai/docs
58
+ // Same shadowing concern: kimi-k2-thinking and kimi-k2.{5,6} must
59
+ // match before any future bare `kimi-k2` entry (#4991).
60
+ ["kimi-k2-thinking", { contextWindow: 262144, maxTokens: 16384, ollamaOptions: { num_ctx: 262144 } }],
61
+ ["kimi-k2.6", { contextWindow: 262144, maxTokens: 16384, ollamaOptions: { num_ctx: 262144 } }],
62
+ ["kimi-k2.5", { contextWindow: 262144, maxTokens: 16384, ollamaOptions: { num_ctx: 262144 } }],
63
+ ["kimi-k2", { contextWindow: 262144, maxTokens: 16384, ollamaOptions: { num_ctx: 262144 } }],
64
+ // ─── MiniMax M2 (Ollama Cloud) ─────────────────────────────────────
65
+ // ref: minimax-m2 1M ctx — https://www.minimax.io/news/minimax-m2
66
+ ["minimax-m2.7", { contextWindow: 1048576, maxTokens: 16384, ollamaOptions: { num_ctx: 1048576 } }],
67
+ ["minimax-m2.5", { contextWindow: 1048576, maxTokens: 16384, ollamaOptions: { num_ctx: 1048576 } }],
68
+ ["minimax-m2", { contextWindow: 1048576, maxTokens: 16384, ollamaOptions: { num_ctx: 1048576 } }],
38
69
  // ─── Gemma family ───────────────────────────────────────────────────
39
70
  ["gemma3", { contextWindow: 131072, maxTokens: 16384, ollamaOptions: { num_ctx: 131072 } }],
40
71
  ["gemma2", { contextWindow: 8192, maxTokens: 8192, ollamaOptions: { num_ctx: 8192 } }],
@@ -1,8 +1,44 @@
1
1
  // GSD2 — HTTP client for Ollama REST API
2
2
  import { parseNDJsonStream } from "./ndjson-stream.js";
3
3
  const DEFAULT_HOST = "http://localhost:11434";
4
- const PROBE_TIMEOUT_MS = 1500;
5
- const REQUEST_TIMEOUT_MS = 10000;
4
+ const DEFAULT_PROBE_TIMEOUT_MS = 1500;
5
+ const DEFAULT_REQUEST_TIMEOUT_MS = 10000;
6
+ export const MAX_TIMER_DELAY_MS = 2_147_483_647;
7
+ /**
8
+ * Parse a positive integer from an environment variable, falling back to
9
+ * `fallback` when the var is unset, empty, non-numeric, zero, or negative.
10
+ *
11
+ * Defensive parsing: a typo like `OLLAMA_PROBE_TIMEOUT_MS=abc` or
12
+ * `OLLAMA_PROBE_TIMEOUT_MS=0` should not silently disable the timeout —
13
+ * fall back to the documented default instead.
14
+ */
15
+ export function envPositiveInt(name, fallback) {
16
+ const raw = process.env[name];
17
+ if (!raw)
18
+ return fallback;
19
+ const parsed = Number.parseInt(raw, 10);
20
+ if (!Number.isFinite(parsed) || parsed <= 0)
21
+ return fallback;
22
+ return Math.min(parsed, MAX_TIMER_DELAY_MS);
23
+ }
24
+ /**
25
+ * Effective probe timeout for the startup `isRunning()` health check.
26
+ * Override with `OLLAMA_PROBE_TIMEOUT_MS=<ms>` for slower networks (LAN
27
+ * Ollama hosts, cloud endpoints, contended cold starts).
28
+ *
29
+ * Resolved at call time — tests and downstream callers can mutate
30
+ * `process.env` between invocations and pick up the new value.
31
+ */
32
+ export function getProbeTimeoutMs() {
33
+ return envPositiveInt("OLLAMA_PROBE_TIMEOUT_MS", DEFAULT_PROBE_TIMEOUT_MS);
34
+ }
35
+ /**
36
+ * Effective per-request timeout for REST calls. Override with
37
+ * `OLLAMA_REQUEST_TIMEOUT_MS=<ms>`.
38
+ */
39
+ export function getRequestTimeoutMs() {
40
+ return envPositiveInt("OLLAMA_REQUEST_TIMEOUT_MS", DEFAULT_REQUEST_TIMEOUT_MS);
41
+ }
6
42
  /**
7
43
  * Get the Ollama host URL from OLLAMA_HOST or default.
8
44
  */
@@ -38,7 +74,7 @@ function withAuth(options = {}) {
38
74
  headers: { ...authHeaders, ...(options.headers || {}) },
39
75
  };
40
76
  }
41
- async function fetchWithTimeout(url, options = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
77
+ async function fetchWithTimeout(url, options = {}, timeoutMs = getRequestTimeoutMs()) {
42
78
  const controller = new AbortController();
43
79
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
44
80
  try {
@@ -58,7 +94,7 @@ export async function isRunning() {
58
94
  const host = getOllamaHost();
59
95
  const isCloud = host.includes("ollama.com") || host.includes("cloud");
60
96
  const probeUrl = isCloud ? `${host}/api/tags` : `${host}/`;
61
- const timeout = isCloud ? REQUEST_TIMEOUT_MS : PROBE_TIMEOUT_MS;
97
+ const timeout = isCloud ? getRequestTimeoutMs() : getProbeTimeoutMs();
62
98
  const response = await fetchWithTimeout(probeUrl, isCloud ? { method: "GET" } : {}, timeout);
63
99
  return response.ok;
64
100
  }