gsd-pi 2.78.1-dev.84a383f51 → 2.78.1-dev.9d08d820b

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 (127) hide show
  1. package/dist/bundled-resource-path.d.ts +7 -0
  2. package/dist/bundled-resource-path.js +34 -2
  3. package/dist/claude-cli-check.js +18 -6
  4. package/dist/headless-query.js +21 -6
  5. package/dist/loader.js +2 -3
  6. package/dist/resource-loader.js +2 -8
  7. package/dist/resources/.managed-resources-content-hash +1 -1
  8. package/dist/resources/extensions/claude-code-cli/readiness.js +19 -7
  9. package/dist/resources/extensions/gsd/auto/phases.js +3 -11
  10. package/dist/resources/extensions/gsd/auto/session.js +2 -6
  11. package/dist/resources/extensions/gsd/auto-dashboard.js +3 -2
  12. package/dist/resources/extensions/gsd/auto-dispatch.js +18 -6
  13. package/dist/resources/extensions/gsd/auto-prompts.js +63 -2
  14. package/dist/resources/extensions/gsd/auto-worktree.js +30 -13
  15. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +7 -1
  16. package/dist/resources/extensions/gsd/bootstrap/subagent-input.js +22 -0
  17. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +84 -2
  18. package/dist/resources/extensions/gsd/commands-config.js +3 -2
  19. package/dist/resources/extensions/gsd/commands-extensions.js +46 -3
  20. package/dist/resources/extensions/gsd/commands-handlers.js +3 -2
  21. package/dist/resources/extensions/gsd/docs/preferences-reference.md +6 -0
  22. package/dist/resources/extensions/gsd/doctor-providers.js +2 -1
  23. package/dist/resources/extensions/gsd/forensics.js +8 -6
  24. package/dist/resources/extensions/gsd/guided-flow.js +2 -1
  25. package/dist/resources/extensions/gsd/home-dir.js +16 -0
  26. package/dist/resources/extensions/gsd/key-manager.js +2 -1
  27. package/dist/resources/extensions/gsd/migrate/command.js +3 -2
  28. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +10 -0
  29. package/dist/resources/extensions/gsd/prompts/complete-slice.md +10 -0
  30. package/dist/resources/extensions/gsd/prompts/plan-slice.md +10 -0
  31. package/dist/resources/extensions/gsd/prompts/refine-slice.md +10 -0
  32. package/dist/resources/extensions/gsd/unit-context-manifest.js +29 -4
  33. package/dist/resources/extensions/gsd/worktree-manager.js +20 -1
  34. package/dist/resources/extensions/gsd/worktree-resolver.js +4 -13
  35. package/dist/resources/extensions/gsd/worktree-root.js +124 -0
  36. package/dist/resources/extensions/gsd/worktree.js +4 -115
  37. package/dist/resources/extensions/ollama/index.js +15 -2
  38. package/dist/resources/extensions/ollama/model-capabilities.js +31 -0
  39. package/dist/resources/extensions/ollama/ollama-client.js +40 -4
  40. package/dist/resources/extensions/subagent/index.js +324 -178
  41. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  42. package/dist/web/standalone/.next/BUILD_ID +1 -1
  43. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  44. package/dist/web/standalone/.next/build-manifest.json +2 -2
  45. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  46. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.html +1 -1
  63. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  70. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  72. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  73. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  74. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  75. package/dist/worktree-cli.d.ts +1 -0
  76. package/dist/worktree-cli.js +9 -3
  77. package/package.json +1 -3
  78. package/packages/mcp-server/src/workflow-tools.test.ts +52 -0
  79. package/packages/native/tsconfig.tsbuildinfo +1 -1
  80. package/src/resources/extensions/claude-code-cli/readiness.ts +20 -7
  81. package/src/resources/extensions/gsd/auto/phases.ts +3 -11
  82. package/src/resources/extensions/gsd/auto/session.ts +2 -6
  83. package/src/resources/extensions/gsd/auto-dashboard.ts +3 -2
  84. package/src/resources/extensions/gsd/auto-dispatch.ts +18 -6
  85. package/src/resources/extensions/gsd/auto-prompts.ts +60 -2
  86. package/src/resources/extensions/gsd/auto-worktree.ts +44 -12
  87. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +6 -0
  88. package/src/resources/extensions/gsd/bootstrap/subagent-input.ts +20 -0
  89. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +103 -1
  90. package/src/resources/extensions/gsd/commands-config.ts +3 -2
  91. package/src/resources/extensions/gsd/commands-extensions.ts +43 -3
  92. package/src/resources/extensions/gsd/commands-handlers.ts +3 -2
  93. package/src/resources/extensions/gsd/docs/preferences-reference.md +6 -0
  94. package/src/resources/extensions/gsd/doctor-providers.ts +2 -1
  95. package/src/resources/extensions/gsd/forensics.ts +10 -5
  96. package/src/resources/extensions/gsd/guided-flow.ts +2 -1
  97. package/src/resources/extensions/gsd/home-dir.ts +19 -0
  98. package/src/resources/extensions/gsd/journal.ts +4 -1
  99. package/src/resources/extensions/gsd/key-manager.ts +2 -1
  100. package/src/resources/extensions/gsd/migrate/command.ts +3 -2
  101. package/src/resources/extensions/gsd/prompts/complete-milestone.md +10 -0
  102. package/src/resources/extensions/gsd/prompts/complete-slice.md +10 -0
  103. package/src/resources/extensions/gsd/prompts/plan-slice.md +10 -0
  104. package/src/resources/extensions/gsd/prompts/refine-slice.md +10 -0
  105. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +15 -0
  106. package/src/resources/extensions/gsd/tests/commands-extensions-version-compare.test.ts +58 -0
  107. package/src/resources/extensions/gsd/tests/home-dir.test.ts +52 -0
  108. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +50 -1
  109. package/src/resources/extensions/gsd/tests/milestone-report-path.test.ts +18 -1
  110. package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +17 -1
  111. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +38 -3
  112. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +34 -33
  113. package/src/resources/extensions/gsd/tests/worktree.test.ts +8 -0
  114. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +116 -1
  115. package/src/resources/extensions/gsd/unit-context-manifest.ts +36 -4
  116. package/src/resources/extensions/gsd/worktree-manager.ts +40 -1
  117. package/src/resources/extensions/gsd/worktree-resolver.ts +4 -14
  118. package/src/resources/extensions/gsd/worktree-root.ts +144 -0
  119. package/src/resources/extensions/gsd/worktree.ts +8 -119
  120. package/src/resources/extensions/ollama/index.ts +16 -2
  121. package/src/resources/extensions/ollama/model-capabilities.ts +34 -0
  122. package/src/resources/extensions/ollama/ollama-client.ts +41 -4
  123. package/src/resources/extensions/ollama/tests/model-capabilities.test.ts +96 -0
  124. package/src/resources/extensions/ollama/tests/ollama-client-timeout-env.test.ts +147 -0
  125. package/src/resources/extensions/subagent/index.ts +165 -7
  126. /package/dist/web/standalone/.next/static/{UF5VF4F1tB0miEtJS7LyX → -Ukk6_YxRd4GY4iUOnRUE}/_buildManifest.js +0 -0
  127. /package/dist/web/standalone/.next/static/{UF5VF4F1tB0miEtJS7LyX → -Ukk6_YxRd4GY4iUOnRUE}/_ssgManifest.js +0 -0
@@ -111,6 +111,12 @@ export type PreferencesPolicy = "none" | "active-only" | "full";
111
111
  * Task subagent dispatch denied. Catches the bug class
112
112
  * where a discuss-milestone turn modifies user source
113
113
  * files (forensics: ~/Github/test-apps/b23, #4934).
114
+ * - "planning-dispatch"
115
+ * — Same read + .gsd/** write + safe-Bash surface as
116
+ * "planning", but permits controlled subagent dispatch
117
+ * only to the agents listed in the ToolsPolicy
118
+ * `allowedSubagents` field. See write-gate.ts for the
119
+ * runtime agent-class enforcement details.
114
120
  * - "docs" — Read tools always; writes restricted to .gsd/** AND
115
121
  * the explicit `allowedPathGlobs` set; Bash safe-allowlist;
116
122
  * no subagents. Reserved for rewrite-docs, which legitimately
@@ -125,6 +131,7 @@ export type ToolsPolicy =
125
131
  | { readonly mode: "all" }
126
132
  | { readonly mode: "read-only" }
127
133
  | { readonly mode: "planning" }
134
+ | { readonly mode: "planning-dispatch"; readonly allowedSubagents: readonly string[] }
128
135
  | { readonly mode: "docs"; readonly allowedPathGlobs: readonly string[] };
129
136
 
130
137
  // ─── Computed-artifact registry (#4924 v2 contract) ───────────────────────
@@ -268,6 +275,18 @@ const COMMON_BUDGET_SMALL = 250_000; // ~65K tokens
268
275
 
269
276
  const TOOLS_ALL: ToolsPolicy = { mode: "all" };
270
277
  const TOOLS_PLANNING: ToolsPolicy = { mode: "planning" };
278
+ // Like TOOLS_PLANNING but permits dispatch to read-only recon/planning
279
+ // specialists. Runtime-enforced by write-gate.ts before the subagent tool runs.
280
+ const TOOLS_PLANNING_DISPATCH_RECON: ToolsPolicy = {
281
+ mode: "planning-dispatch",
282
+ allowedSubagents: ["scout", "planner"],
283
+ };
284
+ // Like TOOLS_PLANNING_DISPATCH_RECON, but for closeout units that fan out
285
+ // verification work to review-tier specialists.
286
+ const TOOLS_PLANNING_DISPATCH_REVIEW: ToolsPolicy = {
287
+ mode: "planning-dispatch",
288
+ allowedSubagents: ["reviewer", "security", "tester"],
289
+ };
271
290
  const TOOLS_DOCS: ToolsPolicy = {
272
291
  mode: "docs",
273
292
  // Globs are resolved relative to project basePath. The set is intentionally
@@ -376,7 +395,11 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
376
395
  memory: "prompt-relevant",
377
396
  codebaseMap: false,
378
397
  preferences: "active-only",
379
- tools: TOOLS_PLANNING,
398
+ // planning-dispatch: completion is a high-leverage place to fan out to
399
+ // reviewer / security / tester subagents. They read the diff and report
400
+ // findings; they do not write user source. Write isolation to .gsd/ is
401
+ // preserved.
402
+ tools: TOOLS_PLANNING_DISPATCH_REVIEW,
380
403
  artifacts: {
381
404
  // #4780 landed slice-summary as excerpt for this unit; phase 2 of
382
405
  // the architecture will read this manifest as the source of truth
@@ -409,7 +432,10 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
409
432
  memory: "prompt-relevant",
410
433
  codebaseMap: true,
411
434
  preferences: "active-only",
412
- tools: TOOLS_PLANNING,
435
+ // planning-dispatch: allows subagent dispatch so the planner can fan out
436
+ // to scout for codebase recon and to planner/decompose-style specialists
437
+ // for sub-decomposition. Write-isolation to .gsd/ is preserved.
438
+ tools: TOOLS_PLANNING_DISPATCH_RECON,
413
439
  artifacts: {
414
440
  inline: ["roadmap", "slice-research", "dependency-summaries", "requirements", "decisions", "templates"],
415
441
  excerpt: [],
@@ -423,7 +449,10 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
423
449
  memory: "prompt-relevant",
424
450
  codebaseMap: true,
425
451
  preferences: "active-only",
426
- tools: TOOLS_PLANNING,
452
+ // See plan-slice — same rationale: dispatch to scout/planner-style
453
+ // specialists during refinement is materially better than re-doing recon
454
+ // inline.
455
+ tools: TOOLS_PLANNING_DISPATCH_RECON,
427
456
  artifacts: {
428
457
  inline: ["slice-plan", "slice-research", "dependency-summaries", "templates"],
429
458
  excerpt: [],
@@ -451,7 +480,10 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
451
480
  memory: "prompt-relevant",
452
481
  codebaseMap: false,
453
482
  preferences: "active-only",
454
- tools: TOOLS_PLANNING,
483
+ // See complete-milestone — same rationale: dispatch to reviewer / security /
484
+ // tester subagents to fan out review work without bloating this unit's
485
+ // context.
486
+ tools: TOOLS_PLANNING_DISPATCH_REVIEW,
455
487
  artifacts: {
456
488
  // Phase 3 migration (#4782): matches today's actual
457
489
  // buildCompleteSlicePrompt inlining order. Overrides prepend +
@@ -39,6 +39,11 @@ import {
39
39
  nativeWorktreeRemove,
40
40
  } from "./native-git-bridge.js";
41
41
  import { emitCanonicalRootRedirect } from "./worktree-telemetry.js";
42
+ import {
43
+ isGsdWorktreePath,
44
+ normalizeWorktreePathForCompare,
45
+ resolveWorktreeProjectRoot,
46
+ } from "./worktree-root.js";
42
47
 
43
48
  // ─── Types ─────────────────────────────────────────────────────────────────
44
49
 
@@ -75,6 +80,20 @@ function normalizePathForComparison(path: string): string {
75
80
  return process.platform === "win32" ? normalized.toLowerCase() : normalized;
76
81
  }
77
82
 
83
+ function normalizeBasePathForWorktreeOps(basePath: string): string {
84
+ const resolved = resolveWorktreeProjectRoot(basePath);
85
+ if (
86
+ isGsdWorktreePath(basePath) &&
87
+ normalizeWorktreePathForCompare(resolved) === normalizeWorktreePathForCompare(basePath)
88
+ ) {
89
+ throw new GSDError(
90
+ GSD_GIT_ERROR,
91
+ `Cannot resolve project root from worktree path: ${basePath}. Run the command from the project root or set GSD_PROJECT_ROOT.`,
92
+ );
93
+ }
94
+ return resolved;
95
+ }
96
+
78
97
  // ─── resolveGitDir ─────────────────────────────────────────────────────────
79
98
 
80
99
  /**
@@ -106,7 +125,7 @@ export function resolveGitDir(basePath: string): string {
106
125
  }
107
126
 
108
127
  export function worktreesDir(basePath: string): string {
109
- return join(basePath, ".gsd", "worktrees");
128
+ return join(resolveWorktreeProjectRoot(basePath), ".gsd", "worktrees");
110
129
  }
111
130
 
112
131
  export function worktreePath(basePath: string, name: string): string {
@@ -195,6 +214,8 @@ export function resolveCanonicalMilestoneRoot(
195
214
  * @param opts.branch — override the default `worktree/<name>` branch name
196
215
  */
197
216
  export function createWorktree(basePath: string, name: string, opts: { branch?: string; startPoint?: string; reuseExistingBranch?: boolean } = {}): WorktreeInfo {
217
+ basePath = normalizeBasePathForWorktreeOps(basePath);
218
+
198
219
  // Validate name: alphanumeric, hyphens, underscores only
199
220
  if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
200
221
  throw new GSDError(GSD_PARSE_ERROR, `Invalid worktree name "${name}". Use only letters, numbers, hyphens, and underscores.`);
@@ -297,6 +318,8 @@ export function createWorktree(basePath: string, name: string, opts: { branch?:
297
318
  * Uses native worktree list and filters to those under .gsd/worktrees/.
298
319
  */
299
320
  export function listWorktrees(basePath: string): WorktreeInfo[] {
321
+ basePath = normalizeBasePathForWorktreeOps(basePath);
322
+
300
323
  const baseVariants = [resolve(basePath)];
301
324
  if (existsSync(basePath)) {
302
325
  baseVariants.push(realpathSync(basePath));
@@ -459,6 +482,8 @@ export function removeWorktree(
459
482
  name: string,
460
483
  opts: { deleteBranch?: boolean; force?: boolean; branch?: string } = {},
461
484
  ): void {
485
+ basePath = normalizeBasePathForWorktreeOps(basePath);
486
+
462
487
  let wtPath = worktreePath(basePath, name);
463
488
  const branch = opts.branch ?? worktreeBranchName(name);
464
489
  const { deleteBranch = true, force = true } = opts;
@@ -714,6 +739,8 @@ function parseDiffNameStatus(entries: { status: string; path: string }[]): Workt
714
739
  * Returns a summary of added, modified, and removed GSD artifacts.
715
740
  */
716
741
  export function diffWorktreeGSD(basePath: string, name: string): WorktreeDiffSummary {
742
+ basePath = normalizeBasePathForWorktreeOps(basePath);
743
+
717
744
  const branch = worktreeBranchName(name);
718
745
  const mainBranch = nativeDetectMainBranch(basePath);
719
746
 
@@ -729,6 +756,8 @@ export function diffWorktreeGSD(basePath: string, name: string): WorktreeDiffSum
729
756
  * content, this correctly returns an empty diff.
730
757
  */
731
758
  export function diffWorktreeAll(basePath: string, name: string): WorktreeDiffSummary {
759
+ basePath = normalizeBasePathForWorktreeOps(basePath);
760
+
732
761
  const branch = worktreeBranchName(name);
733
762
  const mainBranch = nativeDetectMainBranch(basePath);
734
763
 
@@ -742,6 +771,8 @@ export function diffWorktreeAll(basePath: string, name: string): WorktreeDiffSum
742
771
  * Uses direct diff (not merge-base) so the preview matches the actual merge outcome.
743
772
  */
744
773
  export function diffWorktreeNumstat(basePath: string, name: string): FileLineStat[] {
774
+ basePath = normalizeBasePathForWorktreeOps(basePath);
775
+
745
776
  const branch = worktreeBranchName(name);
746
777
  const mainBranch = nativeDetectMainBranch(basePath);
747
778
 
@@ -760,6 +791,8 @@ export function diffWorktreeNumstat(basePath: string, name: string): FileLineSta
760
791
  * Returns the raw unified diff for LLM consumption.
761
792
  */
762
793
  export function getWorktreeGSDDiff(basePath: string, name: string): string {
794
+ basePath = normalizeBasePathForWorktreeOps(basePath);
795
+
763
796
  const branch = worktreeBranchName(name);
764
797
  const mainBranch = nativeDetectMainBranch(basePath);
765
798
 
@@ -771,6 +804,8 @@ export function getWorktreeGSDDiff(basePath: string, name: string): string {
771
804
  * Returns the raw unified diff for LLM consumption.
772
805
  */
773
806
  export function getWorktreeCodeDiff(basePath: string, name: string): string {
807
+ basePath = normalizeBasePathForWorktreeOps(basePath);
808
+
774
809
  const branch = worktreeBranchName(name);
775
810
  const mainBranch = nativeDetectMainBranch(basePath);
776
811
 
@@ -781,6 +816,8 @@ export function getWorktreeCodeDiff(basePath: string, name: string): string {
781
816
  * Get commit log for the worktree branch since it diverged from main.
782
817
  */
783
818
  export function getWorktreeLog(basePath: string, name: string): string {
819
+ basePath = normalizeBasePathForWorktreeOps(basePath);
820
+
784
821
  const branch = worktreeBranchName(name);
785
822
  const mainBranch = nativeDetectMainBranch(basePath);
786
823
 
@@ -795,6 +832,8 @@ export function getWorktreeLog(basePath: string, name: string): string {
795
832
  * Returns the merge commit message.
796
833
  */
797
834
  export function mergeWorktreeToMain(basePath: string, name: string, commitMessage: string): string {
835
+ basePath = normalizeBasePathForWorktreeOps(basePath);
836
+
798
837
  const branch = worktreeBranchName(name);
799
838
  const mainBranch = nativeDetectMainBranch(basePath);
800
839
  const current = nativeGetCurrentBranch(basePath);
@@ -23,6 +23,7 @@ import { emitJournalEvent } from "./journal.js";
23
23
  import { emitWorktreeCreated, emitWorktreeMerged } from "./worktree-telemetry.js";
24
24
  import { getCollapseCadence, getMilestoneResquash, resquashMilestoneOnMain } from "./slice-cadence.js";
25
25
  import { loadEffectiveGSDPreferences } from "./preferences.js";
26
+ import { resolveWorktreeProjectRoot } from "./worktree-root.js";
26
27
 
27
28
  // ─── Dependency Interface ──────────────────────────────────────────────────
28
29
 
@@ -84,31 +85,20 @@ export interface NotifyCtx {
84
85
 
85
86
  // ─── Path Helpers ──────────────────────────────────────────────────────────
86
87
 
87
- /**
88
- * Worktree marker segment — present in any path produced by worktreePath().
89
- * Used to strip the worktree suffix and recover the project root (#3729).
90
- */
91
- const WORKTREE_MARKER = "/.gsd/worktrees/";
92
-
93
88
  /**
94
89
  * Resolve the project root from session path state.
95
90
  *
96
91
  * Prefers `originalBasePath` (always the project root when set), but falls
97
92
  * back to `basePath` when `originalBasePath` is falsy (e.g. fresh AutoSession
98
93
  * with default empty string). If `basePath` itself is inside a worktree
99
- * directory (contains `/.gsd/worktrees/`), strip that suffix to recover the
100
- * actual project root preventing double-nested worktree paths (#3729).
94
+ * directory (including symlink-resolved ~/.gsd/projects/<hash>/worktrees
95
+ * paths), recover the actual project root to prevent double nesting (#3729).
101
96
  */
102
97
  export function resolveProjectRoot(
103
98
  originalBasePath: string,
104
99
  basePath: string,
105
100
  ): string {
106
- let resolved = originalBasePath || basePath;
107
- const markerIdx = resolved.indexOf(WORKTREE_MARKER);
108
- if (markerIdx !== -1) {
109
- resolved = resolved.slice(0, markerIdx);
110
- }
111
- return resolved;
101
+ return resolveWorktreeProjectRoot(basePath, originalBasePath);
112
102
  }
113
103
 
114
104
  // ─── WorktreeResolver ──────────────────────────────────────────────────────
@@ -0,0 +1,144 @@
1
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+
5
+ export interface WorktreeSegment {
6
+ gsdIdx: number;
7
+ afterWorktrees: number;
8
+ }
9
+
10
+ export function normalizeWorktreePathForCompare(path: string): string {
11
+ let normalized: string;
12
+ try {
13
+ normalized = realpathSync(path);
14
+ } catch {
15
+ normalized = resolve(path);
16
+ }
17
+ const slashed = normalized.replaceAll("\\", "/");
18
+ const trimmed = slashed.replace(/\/+$/, "");
19
+ return process.platform === "win32" ? (trimmed || "/").toLowerCase() : (trimmed || "/");
20
+ }
21
+
22
+ /**
23
+ * Find the GSD worktree segment in both direct project layout and the
24
+ * symlink-resolved external-state layout used by ~/.gsd/projects/<hash>.
25
+ */
26
+ export function findWorktreeSegment(normalizedPath: string): WorktreeSegment | null {
27
+ const directMarker = "/.gsd/worktrees/";
28
+ const directIdx = normalizedPath.indexOf(directMarker);
29
+ if (directIdx !== -1) {
30
+ return { gsdIdx: directIdx, afterWorktrees: directIdx + directMarker.length };
31
+ }
32
+
33
+ const externalRe = /\/\.gsd\/projects\/[^/]+\/worktrees\//;
34
+ const externalMatch = normalizedPath.match(externalRe);
35
+ if (externalMatch && externalMatch.index !== undefined) {
36
+ return {
37
+ gsdIdx: externalMatch.index,
38
+ afterWorktrees: externalMatch.index + externalMatch[0].length,
39
+ };
40
+ }
41
+
42
+ return null;
43
+ }
44
+
45
+ export function isGsdWorktreePath(path: string): boolean {
46
+ return findWorktreeSegment(path.replaceAll("\\", "/")) !== null;
47
+ }
48
+
49
+ /**
50
+ * Resolve the canonical project root for worktree operations.
51
+ *
52
+ * `originalBasePath` wins when available because session state already knows the
53
+ * root. `GSD_PROJECT_ROOT` is the next strongest signal for worker processes.
54
+ * Otherwise, derive the root from direct `.gsd/worktrees` paths, or recover it
55
+ * from the worktree `.git` file for symlink-resolved ~/.gsd/project paths.
56
+ */
57
+ export function resolveWorktreeProjectRoot(
58
+ basePath: string,
59
+ originalBasePath?: string | null,
60
+ ): string {
61
+ const preferred =
62
+ originalBasePath?.trim() ||
63
+ process.env.GSD_PROJECT_ROOT?.trim() ||
64
+ basePath;
65
+
66
+ return resolveProjectRootFromPath(preferred);
67
+ }
68
+
69
+ function resolveProjectRootFromPath(path: string): string {
70
+ const normalizedPath = path.replaceAll("\\", "/");
71
+ const segment = findWorktreeSegment(normalizedPath);
72
+ if (!segment) return resolveGitWorkingTreeRoot(path) ?? path;
73
+
74
+ const sepChar = path.includes("\\") ? "\\" : "/";
75
+ const gsdMarker = `${sepChar}.gsd${sepChar}`;
76
+ const markerIdx = path.indexOf(gsdMarker);
77
+ const candidate = markerIdx !== -1
78
+ ? path.slice(0, markerIdx)
79
+ : path.slice(0, segment.gsdIdx);
80
+
81
+ const gsdHome = normalizeWorktreePathForCompare(process.env.GSD_HOME || join(homedir(), ".gsd"));
82
+ const candidateGsdPath = normalizeWorktreePathForCompare(join(candidate, ".gsd"));
83
+
84
+ if (candidateGsdPath === gsdHome || candidateGsdPath.startsWith(`${gsdHome}/`)) {
85
+ const realRoot = resolveProjectRootFromGitFile(path);
86
+ return realRoot ?? path;
87
+ }
88
+
89
+ return candidate;
90
+ }
91
+
92
+ function resolveGitWorkingTreeRoot(path: string): string | null {
93
+ try {
94
+ let dir = existsSync(path) && !statSync(path).isDirectory()
95
+ ? resolve(path, "..")
96
+ : path;
97
+
98
+ for (let i = 0; i < 30; i++) {
99
+ const gitPath = join(dir, ".git");
100
+ if (existsSync(gitPath)) return dir;
101
+
102
+ const parent = resolve(dir, "..");
103
+ if (parent === dir) break;
104
+ dir = parent;
105
+ }
106
+ } catch {
107
+ // Non-fatal: callers either keep the original path or fail closed.
108
+ }
109
+ return null;
110
+ }
111
+
112
+ function resolveProjectRootFromGitFile(worktreePath: string): string | null {
113
+ try {
114
+ let dir = worktreePath;
115
+ for (let i = 0; i < 30; i++) {
116
+ const gitPath = join(dir, ".git");
117
+ if (existsSync(gitPath)) {
118
+ const content = readFileSync(gitPath, "utf8").trim();
119
+ if (content.startsWith("gitdir: ")) {
120
+ const gitDir = resolve(dir, content.slice(8));
121
+ const dotGitDir = resolve(gitDir, "..", "..");
122
+ if (dotGitDir.endsWith(".git") || dotGitDir.endsWith(".git/") || dotGitDir.endsWith(".git\\")) {
123
+ return resolve(dotGitDir, "..");
124
+ }
125
+
126
+ const commonDirPath = join(gitDir, "commondir");
127
+ if (existsSync(commonDirPath)) {
128
+ const commonDir = readFileSync(commonDirPath, "utf8").trim();
129
+ const resolvedCommonDir = resolve(gitDir, commonDir);
130
+ return resolve(resolvedCommonDir, "..");
131
+ }
132
+ }
133
+ break;
134
+ }
135
+
136
+ const parent = resolve(dir, "..");
137
+ if (parent === dir) break;
138
+ dir = parent;
139
+ }
140
+ } catch {
141
+ // Non-fatal: callers either keep the original path or fail closed.
142
+ }
143
+ return null;
144
+ }
@@ -12,12 +12,16 @@
12
12
  * SLICE_BRANCH_RE) remain for backwards compatibility with legacy branches.
13
13
  */
14
14
 
15
- import { existsSync, readFileSync, realpathSync, utimesSync } from "node:fs";
16
- import { join, resolve, sep } from "node:path";
17
- import { homedir } from "node:os";
15
+ import { existsSync, readFileSync, utimesSync } from "node:fs";
16
+ import { join, resolve } from "node:path";
18
17
 
19
18
  import { GitServiceImpl, writeIntegrationBranch, type TaskCommitContext } from "./git-service.js";
20
19
  import { loadEffectiveGSDPreferences } from "./preferences.js";
20
+ import {
21
+ findWorktreeSegment,
22
+ resolveWorktreeProjectRoot,
23
+ } from "./worktree-root.js";
24
+ export { resolveWorktreeProjectRoot } from "./worktree-root.js";
21
25
 
22
26
  export { MergeConflictError } from "./git-service.js";
23
27
  export type { TaskCommitContext } from "./git-service.js";
@@ -78,29 +82,6 @@ export function captureIntegrationBranch(basePath: string, milestoneId: string):
78
82
 
79
83
  // ─── Pure Utility Functions (unchanged) ────────────────────────────────────
80
84
 
81
- /**
82
- * Find the worktrees segment in a path, supporting both direct
83
- * (`/.gsd/worktrees/`) and symlink-resolved (`/.gsd/projects/<hash>/worktrees/`)
84
- * layouts. When `.gsd` is a symlink to `~/.gsd/projects/<hash>`, resolved
85
- * paths contain the intermediate `projects/<hash>/` segment that the old
86
- * single-marker check missed.
87
- */
88
- function findWorktreeSegment(normalizedPath: string): { gsdIdx: number; afterWorktrees: number } | null {
89
- // Direct layout: /.gsd/worktrees/<name>
90
- const directMarker = "/.gsd/worktrees/";
91
- const idx = normalizedPath.indexOf(directMarker);
92
- if (idx !== -1) {
93
- return { gsdIdx: idx, afterWorktrees: idx + directMarker.length };
94
- }
95
- // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/<name>
96
- const symlinkRe = /\/\.gsd\/projects\/[a-f0-9]+\/worktrees\//;
97
- const match = normalizedPath.match(symlinkRe);
98
- if (match && match.index !== undefined) {
99
- return { gsdIdx: match.index, afterWorktrees: match.index + match[0].length };
100
- }
101
- return null;
102
- }
103
-
104
85
  /**
105
86
  * Detect the active worktree name from the current working directory.
106
87
  * Returns null if not inside a GSD worktree (.gsd/worktrees/<name>/).
@@ -133,99 +114,7 @@ export function detectWorktreeName(basePath: string): string | null {
133
114
  * operate against the real project root, not a worktree subdirectory.
134
115
  */
135
116
  export function resolveProjectRoot(basePath: string): string {
136
- // Layer 1: If the coordinator passed the real project root, use it.
137
- if (process.env.GSD_PROJECT_ROOT) {
138
- return process.env.GSD_PROJECT_ROOT;
139
- }
140
-
141
- const normalizedPath = basePath.replaceAll("\\", "/");
142
- const seg = findWorktreeSegment(normalizedPath);
143
- if (!seg) return basePath;
144
-
145
- // Candidate root via the string-slice heuristic
146
- const sepChar = basePath.includes("\\") ? "\\" : "/";
147
- const gsdMarker = `${sepChar}.gsd${sepChar}`;
148
- const gsdIdx = basePath.indexOf(gsdMarker);
149
- const candidate = gsdIdx !== -1
150
- ? basePath.slice(0, gsdIdx)
151
- : basePath.slice(0, seg.gsdIdx);
152
-
153
- // Layer 2: Guard against resolving to the user's home directory.
154
- // When .gsd is a symlink into ~/.gsd/projects/<hash>, the resolved path
155
- // contains /.gsd/ at the user-level boundary. Slicing there yields ~ — wrong.
156
- const gsdHome = normalizePathForCompare(process.env.GSD_HOME || join(homedir(), ".gsd"));
157
- const candidateGsdPath = normalizePathForCompare(join(candidate, ".gsd"));
158
-
159
- if (candidateGsdPath === gsdHome || candidateGsdPath.startsWith(gsdHome + "/")) {
160
- // The candidate is the home directory (or within it in a way that .gsd
161
- // maps to the user-level GSD dir). Try to recover the real project root
162
- // from the worktree's .git file.
163
- const realRoot = resolveProjectRootFromGitFile(basePath);
164
- if (realRoot) return realRoot;
165
- // If git file resolution failed, return basePath unchanged rather than ~
166
- return basePath;
167
- }
168
-
169
- return candidate;
170
- }
171
-
172
- /**
173
- * Recover the real project root from a worktree's .git file.
174
- *
175
- * Each git worktree has a `.git` file (not directory) containing:
176
- * gitdir: /real/project/.git/worktrees/<name>
177
- *
178
- * Walking up from that gitdir gives us `/real/project/.git`, and its
179
- * parent is the real project root.
180
- */
181
- function resolveProjectRootFromGitFile(worktreePath: string): string | null {
182
- try {
183
- // Walk up from the worktree path to find the .git file
184
- let dir = worktreePath;
185
- for (let i = 0; i < 30; i++) {
186
- const gitPath = join(dir, ".git");
187
- if (existsSync(gitPath)) {
188
- const content = readFileSync(gitPath, "utf8").trim();
189
- if (content.startsWith("gitdir: ")) {
190
- // gitdir points to: <real-project>/.git/worktrees/<name>
191
- const gitDir = resolve(dir, content.slice(8));
192
- // Walk up: .git/worktrees/<name> → .git/worktrees → .git → project root
193
- const dotGitDir = resolve(gitDir, "..", "..");
194
- // Verify this looks like a .git directory
195
- if (dotGitDir.endsWith(".git") || dotGitDir.endsWith(".git/") || dotGitDir.endsWith(".git\\")) {
196
- return resolve(dotGitDir, "..");
197
- }
198
- // Alternative: the commondir file inside the worktree gitdir
199
- // points to the main .git directory
200
- const commonDirPath = join(gitDir, "commondir");
201
- if (existsSync(commonDirPath)) {
202
- const commonDir = readFileSync(commonDirPath, "utf8").trim();
203
- const resolvedCommonDir = resolve(gitDir, commonDir);
204
- return resolve(resolvedCommonDir, "..");
205
- }
206
- }
207
- break;
208
- }
209
- const parent = resolve(dir, "..");
210
- if (parent === dir) break;
211
- dir = parent;
212
- }
213
- } catch {
214
- // Non-fatal — caller will use fallback
215
- }
216
- return null;
217
- }
218
-
219
- function normalizePathForCompare(path: string): string {
220
- let normalized: string;
221
- try {
222
- normalized = realpathSync(path);
223
- } catch {
224
- normalized = resolve(path);
225
- }
226
- const slashed = normalized.replaceAll("\\", "/");
227
- const trimmed = slashed.replace(/\/+$/, "");
228
- return trimmed || "/";
117
+ return resolveWorktreeProjectRoot(basePath);
229
118
  }
230
119
 
231
120
  /**
@@ -119,17 +119,31 @@ export default function ollama(pi: ExtensionAPI) {
119
119
  // In headless/auto mode, await the probe so the fallback resolver can
120
120
  // see Ollama before the first LLM call (#3531 race condition).
121
121
  // In interactive mode, keep it async for fast startup.
122
+ // Surface probe failures under GSD_DEBUG so users can diagnose silent
123
+ // "Ollama is missing from /model" reports without patching dist/. The
124
+ // probe still soft-fails (registration is best-effort) — we just stop
125
+ // dropping the error on the floor. See #4982.
126
+ const debugOllama = (where: string, error: unknown): void => {
127
+ if (process.env.GSD_DEBUG) {
128
+ const msg = error instanceof Error ? error.message : String(error);
129
+ process.stderr.write(`[ollama] ${where} probe failed: ${msg}\n`);
130
+ }
131
+ };
132
+
122
133
  if (!ctx.hasUI) {
123
134
  try {
124
135
  await probeAndRegister(pi);
125
- } catch { /* non-fatal */ }
136
+ } catch (error) {
137
+ debugOllama("headless", error);
138
+ }
126
139
  } else {
127
140
  probeAndRegister(pi)
128
141
  .then((found) => {
129
142
  ctx.ui.setStatus("ollama", found ? "Ollama" : undefined);
130
143
  })
131
- .catch(() => {
144
+ .catch((error) => {
132
145
  ctx.ui.setStatus("ollama", undefined);
146
+ debugOllama("interactive", error);
133
147
  });
134
148
  }
135
149
  });
@@ -56,10 +56,44 @@ const KNOWN_MODELS: Array<[pattern: string, caps: ModelCapability]> = [
56
56
  ["llama2", { contextWindow: 4096, maxTokens: 4096, ollamaOptions: { num_ctx: 4096 } }],
57
57
 
58
58
  // ─── Qwen family ────────────────────────────────────────────────────
59
+ // Long-variant entries MUST appear before the bare `qwen3` base —
60
+ // `baseName.startsWith(pattern)` returns true for `qwen3.5`/`qwen3-coder`/
61
+ // `qwen3-next` against `qwen3`, and the first match wins (#4991).
62
+ // ref: qwen3-next 1M ctx — https://qwen.ai/blog?id=qwen3-next
63
+ ["qwen3-next", { contextWindow: 1048576, maxTokens: 32768, ollamaOptions: { num_ctx: 1048576 } }],
64
+ // ref: qwen3-coder 256K ctx — https://qwenlm.github.io/blog/qwen3-coder/
65
+ ["qwen3-coder", { contextWindow: 262144, maxTokens: 32768, ollamaOptions: { num_ctx: 262144 } }],
66
+ // ref: qwen3.5 / qwen3.6 1M ctx — Ollama Cloud release notes
67
+ ["qwen3.6", { contextWindow: 1048576, maxTokens: 32768, ollamaOptions: { num_ctx: 1048576 } }],
68
+ ["qwen3.5", { contextWindow: 1048576, maxTokens: 32768, ollamaOptions: { num_ctx: 1048576 } }],
59
69
  ["qwen3", { contextWindow: 131072, maxTokens: 32768, ollamaOptions: { num_ctx: 131072 } }],
60
70
  ["qwen2.5", { contextWindow: 131072, maxTokens: 32768, ollamaOptions: { num_ctx: 131072 } }],
61
71
  ["qwen2", { contextWindow: 131072, maxTokens: 32768, ollamaOptions: { num_ctx: 131072 } }],
62
72
 
73
+ // ─── GLM family (Z.ai, Ollama Cloud) ────────────────────────────────
74
+ // ref: glm 4.6 / 5.x 200K ctx — https://docs.z.ai/devpack/using5.1
75
+ // Long-variant entries before bare `glm-5` / `glm-4` would-be bases to
76
+ // avoid prefix shadowing (#4991).
77
+ ["glm-5.1", { contextWindow: 204800, maxTokens: 16384, ollamaOptions: { num_ctx: 204800 } }],
78
+ ["glm-5", { contextWindow: 204800, maxTokens: 16384, ollamaOptions: { num_ctx: 204800 } }],
79
+ ["glm-4.6", { contextWindow: 204800, maxTokens: 16384, ollamaOptions: { num_ctx: 204800 } }],
80
+ ["glm-4", { contextWindow: 131072, maxTokens: 16384, ollamaOptions: { num_ctx: 131072 } }],
81
+
82
+ // ─── Kimi K2 (Moonshot, Ollama Cloud) ──────────────────────────────
83
+ // ref: kimi-k2 256K ctx — https://platform.moonshot.ai/docs
84
+ // Same shadowing concern: kimi-k2-thinking and kimi-k2.{5,6} must
85
+ // match before any future bare `kimi-k2` entry (#4991).
86
+ ["kimi-k2-thinking", { contextWindow: 262144, maxTokens: 16384, ollamaOptions: { num_ctx: 262144 } }],
87
+ ["kimi-k2.6", { contextWindow: 262144, maxTokens: 16384, ollamaOptions: { num_ctx: 262144 } }],
88
+ ["kimi-k2.5", { contextWindow: 262144, maxTokens: 16384, ollamaOptions: { num_ctx: 262144 } }],
89
+ ["kimi-k2", { contextWindow: 262144, maxTokens: 16384, ollamaOptions: { num_ctx: 262144 } }],
90
+
91
+ // ─── MiniMax M2 (Ollama Cloud) ─────────────────────────────────────
92
+ // ref: minimax-m2 1M ctx — https://www.minimax.io/news/minimax-m2
93
+ ["minimax-m2.7", { contextWindow: 1048576, maxTokens: 16384, ollamaOptions: { num_ctx: 1048576 } }],
94
+ ["minimax-m2.5", { contextWindow: 1048576, maxTokens: 16384, ollamaOptions: { num_ctx: 1048576 } }],
95
+ ["minimax-m2", { contextWindow: 1048576, maxTokens: 16384, ollamaOptions: { num_ctx: 1048576 } }],
96
+
63
97
  // ─── Gemma family ───────────────────────────────────────────────────
64
98
  ["gemma3", { contextWindow: 131072, maxTokens: 16384, ollamaOptions: { num_ctx: 131072 } }],
65
99
  ["gemma2", { contextWindow: 8192, maxTokens: 8192, ollamaOptions: { num_ctx: 8192 } }],