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
@@ -1,6 +1,7 @@
1
1
  import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
3
3
  import { minimatch } from "minimatch";
4
+ import { logWarning } from "../workflow-logger.js";
4
5
  /**
5
6
  * Regex matching milestone CONTEXT.md file names in both legacy M001
6
7
  * and unique M001-abc123 formats. Exported so regex-hardening tests
@@ -458,6 +459,42 @@ export function shouldBlockQueueExecutionInSnapshot(snapshot, toolName, input, q
458
459
  // guards.
459
460
  const PLANNING_WRITE_TOOLS = new Set(["write", "edit", "multi_edit", "notebook_edit"]);
460
461
  const PLANNING_SUBAGENT_TOOLS = new Set(["subagent", "task"]);
462
+ /**
463
+ * Canonical registry for agents that planning-dispatch may consider. Unit
464
+ * manifests still declare per-unit subsets via ToolsPolicy.allowedSubagents.
465
+ */
466
+ const PLANNING_DISPATCH_AGENT_REGISTRY = {
467
+ scout: { readOnlySpecialist: true },
468
+ planner: { readOnlySpecialist: true },
469
+ reviewer: { readOnlySpecialist: true },
470
+ security: { readOnlySpecialist: true },
471
+ tester: { readOnlySpecialist: true },
472
+ };
473
+ export const ALLOWED_PLANNING_DISPATCH_AGENTS = new Set(Object.entries(PLANNING_DISPATCH_AGENT_REGISTRY)
474
+ .filter(([, metadata]) => metadata.readOnlySpecialist)
475
+ .map(([agentId]) => agentId));
476
+ let warnedMissingPlanningDispatchAgentClasses = false;
477
+ function isReadOnlySpecialist(agentId) {
478
+ const metadata = PLANNING_DISPATCH_AGENT_REGISTRY[agentId];
479
+ return metadata?.readOnlySpecialist === true;
480
+ }
481
+ function allowedPlanningDispatchAgentsList() {
482
+ return [...ALLOWED_PLANNING_DISPATCH_AGENTS].join(", ");
483
+ }
484
+ function warnMissingPlanningDispatchAgentClasses(unitType, mode, toolName) {
485
+ if (warnedMissingPlanningDispatchAgentClasses)
486
+ return;
487
+ warnedMissingPlanningDispatchAgentClasses = true;
488
+ // TODO(#5060): Remove this migration shim once all subagent/task callers are verified to forward agent identities.
489
+ const message = `[write-gate] planning-dispatch: shouldBlockPlanningUnit called for tool "${toolName}" ` +
490
+ `on unit "${unitType}" without agentClasses - stale caller; blocking dispatch.`;
491
+ console.warn(message);
492
+ logWarning("intercept", message, {
493
+ unitType,
494
+ mode,
495
+ toolName,
496
+ });
497
+ }
461
498
  /**
462
499
  * Read-only / planning-safe tools that any non-"all" mode allows. Mirrors
463
500
  * QUEUE_SAFE_TOOLS / GATE_SAFE_TOOLS but is the inclusive default for
@@ -501,6 +538,10 @@ function blockReason(unitType, mode, what) {
501
538
  * - "read-only" → blocks all writes, bash, and subagent dispatch.
502
539
  * - "planning" → blocks writes to paths outside <basePath>/.gsd/,
503
540
  * bash that isn't read-only, and subagent dispatch.
541
+ * - "planning-dispatch"
542
+ * → like "planning", but permits subagent dispatch only
543
+ * when every forwarded agent class is globally allowed
544
+ * and listed in the policy's allowedSubagents.
504
545
  * - "docs" → like "planning" but also allows writes to paths
505
546
  * matching `allowedPathGlobs` relative to basePath.
506
547
  *
@@ -510,8 +551,13 @@ function blockReason(unitType, mode, what) {
510
551
  * `policy` of null means "no manifest resolved" — pass-through. Callers
511
552
  * that have no active unit (interactive sessions) pass null and this
512
553
  * predicate is a no-op.
554
+ *
555
+ * `agentClasses` is supplied by the tool hook for subagent-shaped calls. If
556
+ * absent, planning-dispatch fails closed so stale callers cannot silently
557
+ * bypass the agent allowlists. An explicitly supplied-but-empty list is
558
+ * allowed through so the downstream tool call can reject the malformed input.
513
559
  */
514
- export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitType, policy) {
560
+ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitType, policy, agentClasses) {
515
561
  if (!policy)
516
562
  return { block: false };
517
563
  if (policy.mode === "all")
@@ -529,12 +575,48 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
529
575
  // Unknown tool in read-only mode — block by default.
530
576
  return { block: true, reason: blockReason(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`) };
531
577
  }
532
- // planning / docs modes share the same surface for safe tools, bash, and subagent.
578
+ // planning / planning-dispatch / docs modes share the same surface for safe tools, bash, and subagent.
533
579
  if (PLANNING_SAFE_TOOLS.has(tool))
534
580
  return { block: false };
535
581
  if (tool.startsWith("gsd_"))
536
582
  return { block: false };
537
583
  if (PLANNING_SUBAGENT_TOOLS.has(tool)) {
584
+ if (policy.mode === "planning-dispatch") {
585
+ const requested = (agentClasses ?? []).map(a => a.trim()).filter(Boolean);
586
+ const allowedSubagents = Array.isArray(policy.allowedSubagents) ? policy.allowedSubagents : [];
587
+ const allowed = new Set(allowedSubagents);
588
+ // When agentClasses is undefined, the caller has not been updated to extract
589
+ // agent identities yet. Block and warn so stale callers surface in telemetry
590
+ // instead of silently bypassing the gate.
591
+ if (agentClasses === undefined) {
592
+ warnMissingPlanningDispatchAgentClasses(unitType, policy.mode, tool);
593
+ return {
594
+ block: true,
595
+ reason: blockReason(unitType, policy.mode, `subagent dispatch blocked: stale caller did not supply agent identities for "${tool}"; update extractSubagentAgentClasses to handle this input shape`),
596
+ };
597
+ }
598
+ // agentClasses was explicitly provided but resolved to an empty list (for
599
+ // example, a bare tool call with no agent field). Pass through; no agents
600
+ // to validate means the downstream tool call itself will fail.
601
+ if (requested.length === 0) {
602
+ return { block: false };
603
+ }
604
+ const globallyDisallowed = requested.find(a => !isReadOnlySpecialist(a));
605
+ if (globallyDisallowed) {
606
+ return {
607
+ block: true,
608
+ reason: blockReason(unitType, policy.mode, `subagent dispatch of "${globallyDisallowed}" not permitted; only read-only specialists (${allowedPlanningDispatchAgentsList()}) may be dispatched from planning-dispatch units`),
609
+ };
610
+ }
611
+ const disallowedByPolicy = requested.find(a => !allowed.has(a));
612
+ if (disallowedByPolicy) {
613
+ return {
614
+ block: true,
615
+ reason: blockReason(unitType, policy.mode, `subagent dispatch of "${disallowedByPolicy}" not permitted by ToolsPolicy.allowedSubagents; permitted agents for this unit: ${allowedSubagents.join(", ")}`),
616
+ };
617
+ }
618
+ return { block: false };
619
+ }
538
620
  return { block: true, reason: blockReason(unitType, policy.mode, `subagent dispatch is not permitted in planning units`) };
539
621
  }
540
622
  if (tool === "bash") {
@@ -6,6 +6,7 @@
6
6
  import { AuthStorage } from "@gsd/pi-coding-agent";
7
7
  import { existsSync, mkdirSync } from "node:fs";
8
8
  import { join, dirname } from "node:path";
9
+ import { getHomeDir } from "./home-dir.js";
9
10
  /**
10
11
  * Tool API key configurations.
11
12
  * This is the source of truth for tool credentials - used by both the config wizard
@@ -29,7 +30,7 @@ function getStoredToolKey(auth, providerId) {
29
30
  */
30
31
  export function loadToolApiKeys() {
31
32
  try {
32
- const authPath = join(process.env.HOME ?? "", ".gsd", "agent", "auth.json");
33
+ const authPath = join(getHomeDir(), ".gsd", "agent", "auth.json");
33
34
  if (!existsSync(authPath))
34
35
  return;
35
36
  const auth = AuthStorage.create(authPath);
@@ -45,7 +46,7 @@ export function loadToolApiKeys() {
45
46
  }
46
47
  }
47
48
  export function getConfigAuthStorage() {
48
- const authPath = join(process.env.HOME ?? "", ".gsd", "agent", "auth.json");
49
+ const authPath = join(getHomeDir(), ".gsd", "agent", "auth.json");
49
50
  mkdirSync(dirname(authPath), { recursive: true });
50
51
  return AuthStorage.create(authPath);
51
52
  }
@@ -10,7 +10,50 @@ import { dirname, join, resolve } from "node:path";
10
10
  import { homedir, tmpdir } from "node:os";
11
11
  import { execFileSync } from "node:child_process";
12
12
  import { lockSync, unlockSync } from "proper-lockfile";
13
- import semver from "semver";
13
+ /**
14
+ * Strict numeric comparison of two npm-style version strings.
15
+ *
16
+ * Returns true when `a` is strictly greater than `b`. Compares the dotted
17
+ * release components numerically (so `1.10.0` > `1.9.0`) and treats any
18
+ * prerelease suffix (`-beta.1`, `-rc.2`) as less than the equivalent
19
+ * release version (`1.0.0` > `1.0.0-beta.1`). Sufficient for npm package
20
+ * version comparison in the extension installer; we don't need the full
21
+ * semver range/intersect machinery here.
22
+ *
23
+ * Replaces the earlier `import semver from "semver"` — that import broke
24
+ * `tsc -p tsconfig.json` whenever `@types/semver` failed to install
25
+ * (Issue #4946) because the file is pulled in transitively despite being
26
+ * under the `src/resources` exclude.
27
+ */
28
+ export function isVersionGreater(a, b) {
29
+ const split = (v) => {
30
+ const dash = v.indexOf("-");
31
+ const release = (dash === -1 ? v : v.slice(0, dash))
32
+ .split(".")
33
+ .map(part => Number.parseInt(part, 10) || 0);
34
+ const pre = dash === -1 ? null : v.slice(dash + 1);
35
+ return { release, pre };
36
+ };
37
+ const sa = split(a);
38
+ const sb = split(b);
39
+ const len = Math.max(sa.release.length, sb.release.length);
40
+ for (let i = 0; i < len; i++) {
41
+ const ai = sa.release[i] ?? 0;
42
+ const bi = sb.release[i] ?? 0;
43
+ if (ai !== bi)
44
+ return ai > bi;
45
+ }
46
+ // Release components equal — a release version beats any prerelease,
47
+ // and prerelease strings are compared lexicographically (good enough
48
+ // for `beta.1` vs `beta.2`, the only realistic case here).
49
+ if (sa.pre === null && sb.pre !== null)
50
+ return true;
51
+ if (sa.pre !== null && sb.pre === null)
52
+ return false;
53
+ if (sa.pre !== null && sb.pre !== null)
54
+ return sa.pre > sb.pre;
55
+ return false;
56
+ }
14
57
  const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
15
58
  // ─── Registry I/O ───────────────────────────────────────────────────────────
16
59
  function getRegistryPath() {
@@ -413,7 +456,7 @@ async function updateSingleExtension(id, registry, ctx) {
413
456
  ctx.ui.notify(`Could not fetch latest version for "${id}".`, "warning");
414
457
  return;
415
458
  }
416
- if (semver.gt(latest, current)) {
459
+ if (isVersionGreater(latest, current)) {
417
460
  ctx.ui.notify(`Updating "${id}": v${current} → v${latest}...`, "info");
418
461
  await handleInstall(packageName, ctx);
419
462
  }
@@ -464,7 +507,7 @@ async function updateAllExtensions(registry, ctx) {
464
507
  skipped++;
465
508
  continue;
466
509
  }
467
- if (semver.gt(latest, current)) {
510
+ if (isVersionGreater(latest, current)) {
468
511
  ctx.ui.notify(` ${entry.id}: v${current} → v${latest} (updating)`, "info");
469
512
  await handleInstall(packageName, ctx);
470
513
  updated++;
@@ -9,6 +9,7 @@ import { join, resolve as resolvePath, sep } from "node:path";
9
9
  import { homedir } from "node:os";
10
10
  import { deriveState } from "./state.js";
11
11
  import { gsdRoot } from "./paths.js";
12
+ import { getHomeDir } from "./home-dir.js";
12
13
  import { appendCapture, hasPendingCaptures, loadPendingCaptures } from "./captures.js";
13
14
  import { appendOverride, appendKnowledge } from "./files.js";
14
15
  import { formatDoctorIssuesForPrompt, formatDoctorReport, formatDoctorReportJson, runGSDDoctor, selectDoctorScope, filterDoctorIssues, } from "./doctor.js";
@@ -60,7 +61,7 @@ async function fetchLatestVersionForCommand() {
60
61
  }
61
62
  }
62
63
  export function dispatchDoctorHeal(pi, scope, reportText, structuredIssues) {
63
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
64
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(getHomeDir(), ".gsd", "agent", "GSD-WORKFLOW.md");
64
65
  const workflow = readFileSync(workflowPath, "utf-8");
65
66
  const prompt = loadPrompt("doctor-heal", {
66
67
  doctorSummary: reportText,
@@ -210,7 +211,7 @@ export async function handleTriage(ctx, pi, basePath) {
210
211
  currentPlan: currentPlan || "(no active slice plan)",
211
212
  roadmapContext: roadmapContext || "(no active roadmap)",
212
213
  });
213
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
214
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(getHomeDir(), ".gsd", "agent", "GSD-WORKFLOW.md");
214
215
  const workflow = readFileSync(workflowPath, "utf-8");
215
216
  pi.sendMessage({
216
217
  customType: "gsd-triage",
@@ -165,6 +165,12 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
165
165
  - `skip_reassess`: boolean — force-disable roadmap reassessment even if `reassess_after_slice` is enabled. Default: `false`.
166
166
  - `skip_slice_research`: boolean — skip per-slice research. Default: `false`.
167
167
 
168
+ - `reactive_execution`: controls automatic parallel task dispatch inside a slice. Reactive execution is enabled by default when omitted; set `enabled: false` to opt out. With default-on behavior, GSD only attempts a reactive batch when at least three ready tasks are available and the task-plan IO graph is non-ambiguous. If you set `enabled: true` explicitly, GSD uses the earlier opt-in threshold of two ready tasks. Keys:
169
+ - `enabled`: boolean — set `false` to force sequential task execution. Default: `true`.
170
+ - `max_parallel`: number — maximum tasks to dispatch in one batch, range `1`-`8`. Default: `2`.
171
+ - `isolation_mode`: `"same-tree"` — currently the only supported value.
172
+ - `subagent_model`: string — optional model override for reactive task subagents. Falls back to the `models.subagent` routing when omitted.
173
+
168
174
  - `remote_questions`: route interactive questions to Slack/Discord for headless auto-mode. Keys:
169
175
  - `channel`: `"slack"` or `"discord"` — channel type.
170
176
  - `channel_id`: string or number — channel ID.
@@ -16,6 +16,7 @@ import { AuthStorage } from "@gsd/pi-coding-agent";
16
16
  import { getEnvApiKey } from "@gsd/pi-ai";
17
17
  import { loadEffectiveGSDPreferences } from "./preferences.js";
18
18
  import { getAuthPath, PROVIDER_REGISTRY } from "./key-manager.js";
19
+ import { getHomeDir } from "./home-dir.js";
19
20
  // ── Provider routing constants ────────────────────────────────────────────────
20
21
  /**
21
22
  * Providers that use external CLI authentication (not API keys).
@@ -158,7 +159,7 @@ function isCliBinaryInPath(providerId) {
158
159
  return pathDirs.some(dir => executableNames.some(name => existsSync(join(dir, name))));
159
160
  }
160
161
  function modelsJsonPaths() {
161
- const home = process.env.HOME ?? "~";
162
+ const home = getHomeDir();
162
163
  return [
163
164
  join(home, ".gsd", "agent", "models.json"),
164
165
  // Keep parity with custom-provider discovery during auto bootstrap.
@@ -10,7 +10,7 @@
10
10
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
11
11
  import { join, dirname, relative } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
- import { homedir } from "node:os";
13
+ import { getHomeDir } from "./home-dir.js";
14
14
  import { extractTrace } from "./session-forensics.js";
15
15
  import { nativeParseJsonlTail } from "./native-parser-bridge.js";
16
16
  import { MAX_JSONL_BYTES, parseJSONL } from "./jsonl-utils.js";
@@ -135,7 +135,7 @@ export async function handleForensics(args, ctx, pi) {
135
135
  // when import.meta.url resolves to the npm-global install path (Windows).
136
136
  let gsdSourceDir = dirname(fileURLToPath(import.meta.url));
137
137
  if (!existsSync(join(gsdSourceDir, "prompts"))) {
138
- const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
138
+ const gsdHome = process.env.GSD_HOME || join(getHomeDir(), ".gsd");
139
139
  const fallback = join(gsdHome, "agent", "extensions", "gsd");
140
140
  if (existsSync(join(fallback, "prompts")))
141
141
  gsdSourceDir = fallback;
@@ -1109,11 +1109,13 @@ function formatReportForPrompt(report) {
1109
1109
  // ─── Redaction ────────────────────────────────────────────────────────────────
1110
1110
  function redactForGitHub(text, basePath) {
1111
1111
  let result = text;
1112
+ // Build regex that matches both / and \ separator variants (Windows)
1113
+ // Normalize to / first, escape for regex, then replace each / with [/\\]
1114
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1115
+ const pathRe = (p) => new RegExp(esc(p.replace(/\\/g, "/")).replace(/\//g, "[/\\\\]"), "gi");
1112
1116
  // Replace absolute paths
1113
- result = result.replaceAll(basePath, ".");
1114
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
1115
- if (home)
1116
- result = result.replaceAll(home, "~");
1117
+ result = result.replace(pathRe(basePath), ".");
1118
+ result = result.replace(pathRe(getHomeDir()), "~");
1117
1119
  // Strip API key patterns
1118
1120
  result = result.replace(/sk-[a-zA-Z0-9]{20,}/g, "sk-***");
1119
1121
  result = result.replace(/Bearer\s+\S+/g, "Bearer ***");
@@ -18,6 +18,7 @@ import { clearLock } from "./crash-recovery.js";
18
18
  import { assessInterruptedSession, formatInterruptedSessionRunningMessage, formatInterruptedSessionSummary, } from "./interrupted-session.js";
19
19
  import { listUnitRuntimeRecords, clearUnitRuntimeRecord } from "./unit-runtime.js";
20
20
  import { resolveExpectedArtifactPath } from "./auto.js";
21
+ import { getHomeDir } from "./home-dir.js";
21
22
  import { gsdRoot, milestonesDir, resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile, relMilestoneFile, relSliceFile, } from "./paths.js";
22
23
  import { join } from "node:path";
23
24
  import { readFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from "node:fs";
@@ -495,7 +496,7 @@ async function dispatchWorkflow(pi, note, customType = "gsd-run", ctx, unitType)
495
496
  removed: currentTools.length - scopedTools.length,
496
497
  });
497
498
  }
498
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
499
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(getHomeDir(), ".gsd", "agent", "GSD-WORKFLOW.md");
499
500
  const workflow = readFileSync(workflowPath, "utf-8");
500
501
  pi.sendMessage({
501
502
  customType,
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Cross-platform home directory resolution.
3
+ *
4
+ * `process.env.HOME` is not set on Windows (CMD/PowerShell).
5
+ * Falls back to USERPROFILE, then os.homedir(), then throws.
6
+ *
7
+ * @see https://github.com/gsd-build/gsd-2/issues/5015
8
+ */
9
+ import { homedir } from "node:os";
10
+ export function getHomeDir() {
11
+ const home = process.env.HOME || process.env.USERPROFILE || homedir();
12
+ if (!home) {
13
+ throw new Error("Cannot resolve home directory. Set HOME or USERPROFILE environment variable.");
14
+ }
15
+ return home;
16
+ }
@@ -9,6 +9,7 @@ import { existsSync, statSync, chmodSync } from "node:fs";
9
9
  import { join, dirname } from "node:path";
10
10
  import { mkdirSync } from "node:fs";
11
11
  import { getErrorMessage } from "./error-utils.js";
12
+ import { getHomeDir } from "./home-dir.js";
12
13
  export const PROVIDER_REGISTRY = [
13
14
  // LLM Providers
14
15
  { id: "anthropic", label: "Anthropic (Claude)", category: "llm", envVar: "ANTHROPIC_API_KEY", prefixes: ["sk-ant-"], hasOAuth: true, dashboardUrl: "console.anthropic.com" },
@@ -98,7 +99,7 @@ export function describeCredential(cred) {
98
99
  * Get the auth.json path.
99
100
  */
100
101
  export function getAuthPath() {
101
- return join(process.env.HOME ?? "~", ".gsd", "agent", "auth.json");
102
+ return join(getHomeDir(), ".gsd", "agent", "auth.json");
102
103
  }
103
104
  /**
104
105
  * Create an AuthStorage instance for key management.
@@ -13,6 +13,7 @@ import { resolve, join, dirname } from "node:path";
13
13
  import { gsdRoot } from "../paths.js";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { showNextAction } from "../../shared/tui.js";
16
+ import { getHomeDir } from "../home-dir.js";
16
17
  import { validatePlanningDirectory, parsePlanningDirectory, transformToGSD, generatePreview, writeGSDDirectory, } from "./index.js";
17
18
  /** Format preview stats for embedding in the review prompt. */
18
19
  function formatPreviewStats(preview) {
@@ -50,10 +51,10 @@ export async function handleMigrate(args, ctx, pi) {
50
51
  // Default to cwd when no args given; expand ~ to HOME
51
52
  let rawPath = args.trim() || ".";
52
53
  if (rawPath.startsWith("~/")) {
53
- rawPath = join(process.env.HOME ?? "~", rawPath.slice(2));
54
+ rawPath = join(getHomeDir(), rawPath.slice(2));
54
55
  }
55
56
  else if (rawPath === "~") {
56
- rawPath = process.env.HOME ?? "~";
57
+ rawPath = getHomeDir();
57
58
  }
58
59
  let sourcePath = resolve(process.cwd(), rawPath);
59
60
  if (!sourcePath.endsWith(".planning")) {
@@ -16,6 +16,16 @@ Start with what the excerpts give you. Read full files when the section heads si
16
16
 
17
17
  **On-demand Read ordering:** Complete all slice SUMMARY Reads you need for cross-slice synthesis, the Decision Re-evaluation table, and LEARNINGS **before** calling `gsd_complete_milestone` (step 10). Once that tool runs, the milestone is marked complete in the DB — running out of tool budget between step 10 and the LEARNINGS write (step 12) leaves the milestone committed without its LEARNINGS artifact.
18
18
 
19
+ ### Delegate Review Work
20
+
21
+ This unit runs under the `planning-dispatch` tools-policy: you may use the `subagent` tool to delegate review work that benefits from a fresh context window. For non-trivial milestones, delegate before drafting LEARNINGS:
22
+
23
+ - **Cross-slice integrations or new public APIs** → dispatch the **reviewer** agent with the milestone diff and roadmap; treat its findings as input to your Decision Re-evaluation and LEARNINGS sections.
24
+ - **Touched auth, network, parsing, file IO, shell exec, or crypto** → dispatch the **security** agent for an OWASP-style audit across the merged slices.
25
+ - **Significant test surface added or changed** → dispatch the **tester** agent to assess coverage gaps relative to the milestone success criteria.
26
+
27
+ Subagents read the diff and report findings — they do **not** write user source. Apply their feedback into the milestone summary and any captured decisions before calling `gsd_complete_milestone`.
28
+
19
29
  {{inlinedContext}}
20
30
 
21
31
  Then:
@@ -20,6 +20,16 @@ All relevant context has been preloaded below — the slice plan, all task summa
20
20
 
21
21
  **Match effort to complexity.** A simple slice with 1-2 tasks needs a brief summary and lightweight verification. A complex slice with 5 tasks across multiple subsystems needs thorough verification and a detailed summary. Scale the work below accordingly.
22
22
 
23
+ ### Delegate Review Work
24
+
25
+ This unit runs under the `planning-dispatch` tools-policy: you may use the `subagent` tool to delegate review work that benefits from a fresh context window. Strongly consider delegating when the slice is non-trivial:
26
+
27
+ - **Cross-cutting code or new abstractions** → dispatch the **reviewer** agent with the slice diff and plan; apply High/Critical findings before completing.
28
+ - **Touched auth, network, parsing, file IO, shell exec, or crypto** → dispatch the **security** agent for an OWASP-style audit.
29
+ - **Added or modified tests** → dispatch the **tester** agent to assess coverage gaps relative to the slice plan.
30
+
31
+ Subagents read the diff and report findings — they do **not** write user source. You remain responsible for acting on their feedback before calling `gsd_complete_slice` with `milestoneId` and `sliceId`.
32
+
23
33
  Then:
24
34
  1. Use the **Slice Summary** and **UAT** output templates from the inlined context above
25
35
  2. {{skillActivation}}
@@ -20,6 +20,16 @@ Pay particular attention to **Forward Intelligence** sections — they contain h
20
20
 
21
21
  You have full tool access. Before decomposing, explore the relevant code to ground your plan in reality.
22
22
 
23
+ ### Delegate Recon and Sub-Decomposition When Useful
24
+
25
+ This unit runs under the `planning-dispatch` tools-policy: you may use the `subagent` tool to delegate work that benefits from an isolated context window. Prefer delegation over inline work when:
26
+
27
+ - You'd otherwise read more than ~3 files to understand a subsystem → dispatch the **scout** agent for codebase recon and work from its compressed report.
28
+ - The slice spans multiple subsystems and the decomposition isn't obvious → dispatch the **planner** agent or use the **decompose-into-slices** skill on a focused sub-area, then integrate.
29
+ - You need current external information (library docs, API behavior, recent changes) → dispatch the **researcher** agent.
30
+
31
+ **Do not** dispatch implementation-tier agents (`worker`, `refactorer`, `tester`) from this unit — they would write user source and bypass this unit's write isolation. Implementation belongs in `execute-task`.
32
+
23
33
  ### Verify Roadmap Assumptions (JIT Reassessment — ADR-003 §4)
24
34
 
25
35
  Before planning this slice, verify that the roadmap's assumptions still hold given prior slice summaries. Check inlined dependency summaries (below) for discovered constraints, changed approaches, or flagged fragility.
@@ -20,6 +20,16 @@ Pay particular attention to **Forward Intelligence** sections — they contain h
20
20
 
21
21
  ## Your Role in the Pipeline
22
22
 
23
+ ### Delegate Recon When Useful
24
+
25
+ This unit runs under the `planning-dispatch` tools-policy: you may use the `subagent` tool to delegate recon and sub-decomposition. Prefer delegation over inline work when:
26
+
27
+ - You'd otherwise read more than ~3 files to understand a subsystem touched by the sketch → dispatch the **scout** agent and work from its compressed report.
28
+ - A specific area of the refinement needs deeper architectural analysis → dispatch the **planner** agent for a focused sub-plan, then integrate.
29
+ - You need current external information (library docs, API behavior) → dispatch the **researcher** agent.
30
+
31
+ **Do not** dispatch implementation-tier agents (`worker`, `refactorer`, `tester`) — they would write user source and bypass write isolation. Implementation belongs in `execute-task`.
32
+
23
33
  ### Respect the Sketch Scope
24
34
 
25
35
  The sketch scope inlined above is a **hard constraint**. Plan within it. If, after exploring the codebase, the scope is too narrow to deliver the goal, surface this as a deviation in the plan's narrative and still produce the plan — do not silently expand the scope.
@@ -73,6 +73,18 @@ const COMMON_BUDGET_SMALL = 250_000; // ~65K tokens
73
73
  // allowed-path set for the docs policy lives in one reviewable place.
74
74
  const TOOLS_ALL = { mode: "all" };
75
75
  const TOOLS_PLANNING = { mode: "planning" };
76
+ // Like TOOLS_PLANNING but permits dispatch to read-only recon/planning
77
+ // specialists. Runtime-enforced by write-gate.ts before the subagent tool runs.
78
+ const TOOLS_PLANNING_DISPATCH_RECON = {
79
+ mode: "planning-dispatch",
80
+ allowedSubagents: ["scout", "planner"],
81
+ };
82
+ // Like TOOLS_PLANNING_DISPATCH_RECON, but for closeout units that fan out
83
+ // verification work to review-tier specialists.
84
+ const TOOLS_PLANNING_DISPATCH_REVIEW = {
85
+ mode: "planning-dispatch",
86
+ allowedSubagents: ["reviewer", "security", "tester"],
87
+ };
76
88
  const TOOLS_DOCS = {
77
89
  mode: "docs",
78
90
  // Globs are resolved relative to project basePath. The set is intentionally
@@ -177,7 +189,11 @@ export const UNIT_MANIFESTS = {
177
189
  memory: "prompt-relevant",
178
190
  codebaseMap: false,
179
191
  preferences: "active-only",
180
- tools: TOOLS_PLANNING,
192
+ // planning-dispatch: completion is a high-leverage place to fan out to
193
+ // reviewer / security / tester subagents. They read the diff and report
194
+ // findings; they do not write user source. Write isolation to .gsd/ is
195
+ // preserved.
196
+ tools: TOOLS_PLANNING_DISPATCH_REVIEW,
181
197
  artifacts: {
182
198
  // #4780 landed slice-summary as excerpt for this unit; phase 2 of
183
199
  // the architecture will read this manifest as the source of truth
@@ -209,7 +225,10 @@ export const UNIT_MANIFESTS = {
209
225
  memory: "prompt-relevant",
210
226
  codebaseMap: true,
211
227
  preferences: "active-only",
212
- tools: TOOLS_PLANNING,
228
+ // planning-dispatch: allows subagent dispatch so the planner can fan out
229
+ // to scout for codebase recon and to planner/decompose-style specialists
230
+ // for sub-decomposition. Write-isolation to .gsd/ is preserved.
231
+ tools: TOOLS_PLANNING_DISPATCH_RECON,
213
232
  artifacts: {
214
233
  inline: ["roadmap", "slice-research", "dependency-summaries", "requirements", "decisions", "templates"],
215
234
  excerpt: [],
@@ -223,7 +242,10 @@ export const UNIT_MANIFESTS = {
223
242
  memory: "prompt-relevant",
224
243
  codebaseMap: true,
225
244
  preferences: "active-only",
226
- tools: TOOLS_PLANNING,
245
+ // See plan-slice — same rationale: dispatch to scout/planner-style
246
+ // specialists during refinement is materially better than re-doing recon
247
+ // inline.
248
+ tools: TOOLS_PLANNING_DISPATCH_RECON,
227
249
  artifacts: {
228
250
  inline: ["slice-plan", "slice-research", "dependency-summaries", "templates"],
229
251
  excerpt: [],
@@ -251,7 +273,10 @@ export const UNIT_MANIFESTS = {
251
273
  memory: "prompt-relevant",
252
274
  codebaseMap: false,
253
275
  preferences: "active-only",
254
- tools: TOOLS_PLANNING,
276
+ // See complete-milestone — same rationale: dispatch to reviewer / security /
277
+ // tester subagents to fan out review work without bloating this unit's
278
+ // context.
279
+ tools: TOOLS_PLANNING_DISPATCH_REVIEW,
255
280
  artifacts: {
256
281
  // Phase 3 migration (#4782): matches today's actual
257
282
  // buildCompleteSlicePrompt inlining order. Overrides prepend +
@@ -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);