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
package/README.md CHANGED
@@ -471,6 +471,7 @@ On first run, GSD launches a branded setup wizard that walks you through LLM pro
471
471
  | `/gsd logs` | Browse activity, debug, and metrics logs |
472
472
  | `/gsd export --html` | Generate HTML report for current or completed milestone |
473
473
  | `/worktree` (`/wt`) | Git worktree lifecycle — create, switch, merge, remove |
474
+ | `/gsd worktree` (`/gsd wt`) | TUI worktree management — list, merge, clean, remove with safety checks |
474
475
  | `/voice` | Toggle real-time speech-to-text (macOS, Linux) |
475
476
  | `/exit` | Graceful shutdown — saves session state before exiting |
476
477
  | `/kill` | Kill GSD process immediately |
@@ -1,3 +1,10 @@
1
+ export type FileExists = (path: string) => boolean;
2
+ export declare function resolvePackageRoot(importUrl: string): string;
3
+ export declare function hasCompleteBundledResources(resourcesDir: string, fileExists?: FileExists): boolean;
4
+ export declare function resolveBundledResourcesDirFromPackageRoot(packageRoot: string, fileExists?: FileExists): string;
5
+ export declare function resolveBundledResourcesDir(importUrl: string, fileExists?: FileExists): string;
6
+ export declare function resolveBundledResource(importUrl: string, ...segments: string[]): string;
7
+ export declare function resolveBundledGsdExtensionModule(importUrl: string, moduleFile: string, fileExists?: FileExists): string;
1
8
  /**
2
9
  * Resolve bundled raw resource files from the package root.
3
10
  *
@@ -1,5 +1,38 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { dirname, join, resolve } from "node:path";
2
3
  import { fileURLToPath } from "node:url";
4
+ export function resolvePackageRoot(importUrl) {
5
+ const moduleDir = dirname(fileURLToPath(importUrl));
6
+ return resolve(moduleDir, "..");
7
+ }
8
+ export function hasCompleteBundledResources(resourcesDir, fileExists = existsSync) {
9
+ return fileExists(join(resourcesDir, "agents")) &&
10
+ fileExists(join(resourcesDir, "extensions"));
11
+ }
12
+ export function resolveBundledResourcesDirFromPackageRoot(packageRoot, fileExists = existsSync) {
13
+ const distResources = join(packageRoot, "dist", "resources");
14
+ const srcResources = join(packageRoot, "src", "resources");
15
+ return hasCompleteBundledResources(distResources, fileExists)
16
+ ? distResources
17
+ : srcResources;
18
+ }
19
+ export function resolveBundledResourcesDir(importUrl, fileExists = existsSync) {
20
+ return resolveBundledResourcesDirFromPackageRoot(resolvePackageRoot(importUrl), fileExists);
21
+ }
22
+ export function resolveBundledResource(importUrl, ...segments) {
23
+ return join(resolveBundledResourcesDir(importUrl), ...segments);
24
+ }
25
+ export function resolveBundledGsdExtensionModule(importUrl, moduleFile, fileExists = existsSync) {
26
+ const packageRoot = resolvePackageRoot(importUrl);
27
+ const distResources = join(packageRoot, "dist", "resources");
28
+ const jsFile = moduleFile.replace(/\.ts$/, ".js");
29
+ const distModule = join(distResources, "extensions", "gsd", jsFile);
30
+ if (hasCompleteBundledResources(distResources, fileExists) && fileExists(distModule)) {
31
+ return distModule;
32
+ }
33
+ const tsFile = moduleFile.replace(/\.js$/, ".ts");
34
+ return join(packageRoot, "src", "resources", "extensions", "gsd", tsFile);
35
+ }
3
36
  /**
4
37
  * Resolve bundled raw resource files from the package root.
5
38
  *
@@ -8,7 +41,6 @@ import { fileURLToPath } from "node:url";
8
41
  * `src/resources/**`, not next to the compiled entry point.
9
42
  */
10
43
  export function resolveBundledSourceResource(importUrl, ...segments) {
11
- const moduleDir = dirname(fileURLToPath(importUrl));
12
- const packageRoot = resolve(moduleDir, "..");
44
+ const packageRoot = resolvePackageRoot(importUrl);
13
45
  return join(packageRoot, "src", "resources", ...segments);
14
46
  }
@@ -5,6 +5,21 @@
5
5
  // Set GSD_CLAUDE_DEBUG=1 to log probe output to stderr. Useful when
6
6
  // diagnosing platform-specific detection failures (Issue #4997).
7
7
  import { execFileSync } from 'node:child_process';
8
+ /**
9
+ * Spawn the Claude CLI without triggering Node's DEP0190.
10
+ *
11
+ * Passing `args` together with `shell: true` is deprecated in Node 22+
12
+ * because the args are concatenated into the command string without
13
+ * escaping. On Windows we still need a shell to resolve `.cmd` shims, so
14
+ * we invoke `cmd /c <command> <args...>` explicitly. On POSIX we don't
15
+ * need a shell at all.
16
+ */
17
+ function spawnClaude(command, args, opts) {
18
+ if (process.platform === 'win32') {
19
+ return execFileSync('cmd', ['/c', command, ...args], opts);
20
+ }
21
+ return execFileSync(command, args, opts);
22
+ }
8
23
  /**
9
24
  * Platform-correct binary name for the Claude Code CLI.
10
25
  *
@@ -44,10 +59,9 @@ function debugLog(...parts) {
44
59
  function findWorkingCommand() {
45
60
  for (const command of CLAUDE_COMMAND_CANDIDATES) {
46
61
  try {
47
- execFileSync(command, ['--version'], {
62
+ spawnClaude(command, ['--version'], {
48
63
  timeout: VERSION_TIMEOUT_MS,
49
64
  stdio: 'pipe',
50
- shell: process.platform === 'win32',
51
65
  });
52
66
  debugLog('version probe ok via', command);
53
67
  return command;
@@ -93,10 +107,9 @@ function parseAuthStatus(output) {
93
107
  function probeAuth(command) {
94
108
  // Try --json first (newer CLIs).
95
109
  try {
96
- const out = execFileSync(command, ['auth', 'status', '--json'], {
110
+ const out = spawnClaude(command, ['auth', 'status', '--json'], {
97
111
  timeout: AUTH_TIMEOUT_MS,
98
112
  stdio: 'pipe',
99
- shell: process.platform === 'win32',
100
113
  }).toString();
101
114
  debugLog('auth status --json output:', out.slice(0, 200));
102
115
  const parsed = parseAuthStatus(out);
@@ -108,10 +121,9 @@ function probeAuth(command) {
108
121
  }
109
122
  // Fallback: plain `auth status` (older CLIs that don't accept --json).
110
123
  try {
111
- const out = execFileSync(command, ['auth', 'status'], {
124
+ const out = spawnClaude(command, ['auth', 'status'], {
112
125
  timeout: AUTH_TIMEOUT_MS,
113
126
  stdio: 'pipe',
114
- shell: process.platform === 'win32',
115
127
  }).toString();
116
128
  debugLog('auth status output:', out.slice(0, 200));
117
129
  return parseAuthStatus(out);
@@ -17,7 +17,7 @@ import { createJiti } from '@mariozechner/jiti';
17
17
  import { fileURLToPath } from 'node:url';
18
18
  import { join } from 'node:path';
19
19
  import { homedir } from 'node:os';
20
- import { resolveBundledSourceResource } from './bundled-resource-path.js';
20
+ import { resolveBundledGsdExtensionModule } from './bundled-resource-path.js';
21
21
  const jiti = createJiti(fileURLToPath(import.meta.url), { interopDefault: true, debug: false });
22
22
  const { existsSync } = await import('node:fs');
23
23
  /**
@@ -30,7 +30,8 @@ const { existsSync } = await import('node:fs');
30
30
  * #3471 contract can be exercised in tests without spawning a subprocess.
31
31
  */
32
32
  export function resolveGsdAgentExtensionsDir(env = process.env) {
33
- return join(env.GSD_AGENT_DIR || join(homedir(), '.gsd', 'agent'), 'extensions', 'gsd');
33
+ const agentRoot = env.GSD_AGENT_DIR || join(env.GSD_HOME || join(homedir(), '.gsd'), 'agent');
34
+ return join(agentRoot, 'extensions', 'gsd');
34
35
  }
35
36
  /**
36
37
  * Decide whether headless-query should load extensions from the agent
@@ -41,13 +42,27 @@ export function shouldUseAgentExtensionsDir(opts) {
41
42
  const env = opts.env ?? process.env;
42
43
  const fileExists = opts.fileExists ?? existsSync;
43
44
  const agentDir = resolveGsdAgentExtensionsDir(env);
44
- return { agentDir, useAgentDir: fileExists(join(agentDir, 'state.ts')) };
45
+ return {
46
+ agentDir,
47
+ useAgentDir: fileExists(join(agentDir, 'state.ts')) || fileExists(join(agentDir, 'state.js')),
48
+ };
45
49
  }
46
50
  const agentExtensionsDir = resolveGsdAgentExtensionsDir();
47
- const useAgentDir = existsSync(join(agentExtensionsDir, 'state.ts'));
51
+ const { useAgentDir } = shouldUseAgentExtensionsDir({ env: process.env });
48
52
  const gsdExtensionPath = (...segments) => useAgentDir
49
- ? join(agentExtensionsDir, ...segments)
50
- : resolveBundledSourceResource(import.meta.url, 'extensions', 'gsd', ...segments);
53
+ ? resolveAgentExtensionModule(agentExtensionsDir, segments)
54
+ : resolveBundledGsdExtensionModule(import.meta.url, segments.join('/'));
55
+ function resolveAgentExtensionModule(agentDir, segments) {
56
+ const requested = join(agentDir, ...segments);
57
+ if (existsSync(requested))
58
+ return requested;
59
+ if (segments.length === 1 && segments[0].endsWith('.ts')) {
60
+ const jsPath = join(agentDir, segments[0].replace(/\.ts$/, '.js'));
61
+ if (existsSync(jsPath))
62
+ return jsPath;
63
+ }
64
+ return requested;
65
+ }
51
66
  async function loadExtensionModules() {
52
67
  const stateModule = await jiti.import(gsdExtensionPath('state.ts'), {});
53
68
  const dispatchModule = await jiti.import(gsdExtensionPath('auto-dispatch.ts'), {});
package/dist/loader.js CHANGED
@@ -60,6 +60,7 @@ if (firstArg === '--help' || firstArg === '-h') {
60
60
  import { agentDir, appRoot } from './app-paths.js';
61
61
  import { applyRtkProcessEnv } from './rtk-shared.js';
62
62
  import { serializeBundledExtensionPaths } from './bundled-extension-paths.js';
63
+ import { resolveBundledResourcesDirFromPackageRoot } from './bundled-resource-path.js';
63
64
  import { discoverExtensionEntryPaths } from './extension-discovery.js';
64
65
  import { loadRegistry, readManifestFromEntryPath, isExtensionEnabled } from './extension-registry.js';
65
66
  import { renderLogo } from './logo.js';
@@ -118,9 +119,7 @@ process.env.GSD_BIN_PATH = process.argv[1];
118
119
  // GSD_WORKFLOW_PATH — absolute path to bundled GSD-WORKFLOW.md, used by patched gsd extension
119
120
  // when dispatching workflow prompts. Prefers dist/resources/ (stable, set at build time)
120
121
  // over src/resources/ (live working tree) — see resource-loader.ts for rationale.
121
- const distRes = join(gsdRoot, 'dist', 'resources');
122
- const srcRes = join(gsdRoot, 'src', 'resources');
123
- const resourcesDir = existsSync(distRes) ? distRes : srcRes;
122
+ const resourcesDir = resolveBundledResourcesDirFromPackageRoot(gsdRoot);
124
123
  process.env.GSD_WORKFLOW_PATH = join(resourcesDir, 'GSD-WORKFLOW.md');
125
124
  // GSD_BUNDLED_EXTENSION_PATHS — dynamically discovered bundled extension entry points.
126
125
  // Uses the shared discoverExtensionEntryPaths() to scan the bundled resources
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import { compareSemver } from './update-check.js';
7
7
  import { discoverExtensionEntryPaths } from './extension-discovery.js';
8
8
  import { loadRegistry, readManifestFromEntryPath, isExtensionEnabled, ensureRegistryEntries } from './extension-registry.js';
9
+ import { resolveBundledResourcesDirFromPackageRoot } from './bundled-resource-path.js';
9
10
  let piCodingAgentModulePromise;
10
11
  function loadPiCodingAgentModule() {
11
12
  return (piCodingAgentModulePromise ??= import('@gsd/pi-coding-agent'));
@@ -19,14 +20,7 @@ function loadPiCodingAgentModule() {
19
20
  // dist/resources/ is populated by the build step (`npm run copy-resources`) and
20
21
  // reflects the built state, not the currently checked-out branch.
21
22
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
22
- const distResources = join(packageRoot, 'dist', 'resources');
23
- const srcResources = join(packageRoot, 'src', 'resources');
24
- // Use dist/resources only if it has the full expected structure.
25
- // A partial build (tsc without copy-resources) creates dist/resources/extensions/
26
- // but not agents/ or skills/, causing initResources to sync from an incomplete source.
27
- const resourcesDir = (existsSync(distResources) && existsSync(join(distResources, 'agents')))
28
- ? distResources
29
- : srcResources;
23
+ const resourcesDir = resolveBundledResourcesDirFromPackageRoot(packageRoot);
30
24
  const bundledExtensionsDir = join(resourcesDir, 'extensions');
31
25
  const resourceVersionManifestName = 'managed-resources.json';
32
26
  const resourceFingerprintFileName = '.managed-resources-content-hash';
@@ -1 +1 @@
1
- afb1073f61989a78
1
+ 868d22f3f04d038e
@@ -14,6 +14,21 @@
14
14
  * failures (Issue #4997).
15
15
  */
16
16
  import { execFileSync } from "node:child_process";
17
+ /**
18
+ * Spawn the Claude CLI without triggering Node's DEP0190.
19
+ *
20
+ * Passing `args` together with `shell: true` is deprecated in Node 22+
21
+ * because the args are concatenated into the command string without
22
+ * escaping. On Windows we still need a shell to resolve `.cmd` shims, so
23
+ * we invoke `cmd /c <command> <args...>` explicitly. On POSIX we don't
24
+ * need a shell at all.
25
+ */
26
+ function spawnClaude(command, args, opts) {
27
+ if (process.platform === "win32") {
28
+ return execFileSync("cmd", ["/c", command, ...args], opts);
29
+ }
30
+ return execFileSync(command, args, opts);
31
+ }
17
32
  /**
18
33
  * Candidate executable names for the Claude Code CLI.
19
34
  *
@@ -43,7 +58,7 @@ function debugLog(...parts) {
43
58
  * Find the first candidate that responds to `--version`. Returns the
44
59
  * candidate name on success, null if none worked.
45
60
  *
46
- * On Windows with `shell: true`, a missing candidate surfaces as a
61
+ * On Windows with `cmd /c`, a missing candidate surfaces as a
47
62
  * non-zero exit from cmd.exe rather than ENOENT — so we cannot rely on
48
63
  * the error code to decide "try next". Treat any failure as "try next"
49
64
  * for the version probe; the only thing that matters for binary
@@ -53,10 +68,9 @@ function debugLog(...parts) {
53
68
  function findWorkingCommand() {
54
69
  for (const command of CLAUDE_COMMAND_CANDIDATES) {
55
70
  try {
56
- execFileSync(command, ["--version"], {
71
+ spawnClaude(command, ["--version"], {
57
72
  timeout: VERSION_TIMEOUT_MS,
58
73
  stdio: "pipe",
59
- shell: process.platform === "win32",
60
74
  });
61
75
  debugLog("version probe ok via", command);
62
76
  return command;
@@ -103,10 +117,9 @@ function parseAuthStatus(output) {
103
117
  function probeAuth(command) {
104
118
  // Try --json first (newer CLIs).
105
119
  try {
106
- const out = execFileSync(command, ["auth", "status", "--json"], {
120
+ const out = spawnClaude(command, ["auth", "status", "--json"], {
107
121
  timeout: AUTH_TIMEOUT_MS,
108
122
  stdio: "pipe",
109
- shell: process.platform === "win32",
110
123
  }).toString();
111
124
  debugLog("auth status --json output:", out.slice(0, 200));
112
125
  const parsed = parseAuthStatus(out);
@@ -118,10 +131,9 @@ function probeAuth(command) {
118
131
  }
119
132
  // Fallback: plain `auth status` (older CLIs that don't accept --json).
120
133
  try {
121
- const out = execFileSync(command, ["auth", "status"], {
134
+ const out = spawnClaude(command, ["auth", "status"], {
122
135
  timeout: AUTH_TIMEOUT_MS,
123
136
  stdio: "pipe",
124
- shell: process.platform === "win32",
125
137
  }).toString();
126
138
  debugLog("auth status output:", out.slice(0, 200));
127
139
  return parseAuthStatus(out);
@@ -1,7 +1,3 @@
1
- export default function (pi) {
2
- pi.on("session_start", async (_event, ctx) => {
3
- ctx.ui.notify("google_search is being extracted to @gsd-extensions/google-search " +
4
- "(not yet published to npm). This stub will be replaced once the " +
5
- "package is available. No action needed for now.", "warning");
6
- });
1
+ export default function (_pi) {
2
+ // Deprecation notice intentionally suppressed until @gsd-extensions/google-search ships.
7
3
  }
@@ -11,6 +11,7 @@ import { MAX_RECOVERY_CHARS, BUDGET_THRESHOLDS, MAX_FINALIZE_TIMEOUTS, } from ".
11
11
  import { detectStuck } from "./detect-stuck.js";
12
12
  import { runUnit } from "./run-unit.js";
13
13
  import { debugLog } from "../debug-logger.js";
14
+ import { resolveWorktreeProjectRoot } from "../worktree-root.js";
14
15
  import { PROJECT_FILES, hasProjectFileInAncestor } from "../detection.js";
15
16
  import { MergeConflictError } from "../git-service.js";
16
17
  import { setCurrentPhase, clearCurrentPhase } from "../../shared/gsd-phase-state.js";
@@ -48,11 +49,7 @@ export function resetSessionTimeoutState() {
48
49
  * Exported for testing as _resolveReportBasePath.
49
50
  */
50
51
  export function _resolveReportBasePath(s) {
51
- // Strip /.gsd/worktrees/ suffix when basePath is itself a worktree path and
52
- // originalBasePath is falsy — prevents reports landing in the worktree (#3729).
53
- const resolved = s.originalBasePath || s.basePath;
54
- const markerIdx = resolved.indexOf("/.gsd/worktrees/");
55
- return markerIdx !== -1 ? resolved.slice(0, markerIdx) : resolved;
52
+ return resolveWorktreeProjectRoot(s.basePath, s.originalBasePath);
56
53
  }
57
54
  /**
58
55
  * Resolve the authoritative project base for dispatch guards.
@@ -60,12 +57,7 @@ export function _resolveReportBasePath(s) {
60
57
  * unit is running inside an auto worktree.
61
58
  */
62
59
  export function _resolveDispatchGuardBasePath(s) {
63
- // Strip /.gsd/worktrees/ suffix when basePath is itself a worktree path and
64
- // originalBasePath is falsy — prevents guard checks running against the
65
- // worktree instead of the project root (#3729).
66
- const resolved = s.originalBasePath || s.basePath;
67
- const markerIdx = resolved.indexOf("/.gsd/worktrees/");
68
- return markerIdx !== -1 ? resolved.slice(0, markerIdx) : resolved;
60
+ return resolveWorktreeProjectRoot(s.basePath, s.originalBasePath);
69
61
  }
70
62
  const PLAN_V2_GATE_PHASES = new Set([
71
63
  "executing",
@@ -15,6 +15,7 @@
15
15
  * auto-session-encapsulation.test.ts enforce that auto.ts has no module-level
16
16
  * `let` or `var` declarations.
17
17
  */
18
+ import { resolveWorktreeProjectRoot } from "../worktree-root.js";
18
19
  // ─── Constants ───────────────────────────────────────────────────────────────
19
20
  export const STUB_RECOVERY_THRESHOLD = 2;
20
21
  export const NEW_SESSION_TIMEOUT_MS = 120_000;
@@ -155,12 +156,7 @@ export class AutoSession {
155
156
  this.unitLifetimeDispatches.clear();
156
157
  }
157
158
  get lockBasePath() {
158
- // Prefer originalBasePath (project root); fall back to basePath.
159
- // Strip /.gsd/worktrees/ suffix if basePath is itself a worktree path
160
- // to avoid reading/writing the lock inside the worktree (#3729).
161
- const resolved = this.originalBasePath || this.basePath;
162
- const markerIdx = resolved.indexOf("/.gsd/worktrees/");
163
- return markerIdx !== -1 ? resolved.slice(0, markerIdx) : resolved;
159
+ return resolveWorktreeProjectRoot(this.basePath, this.originalBasePath);
164
160
  }
165
161
  reset() {
166
162
  this.clearTimers();
@@ -10,6 +10,7 @@ import { getActiveHook } from "./post-unit-hooks.js";
10
10
  import { getLedger, getProjectTotals } from "./metrics.js";
11
11
  import { getErrorMessage } from "./error-utils.js";
12
12
  import { nativeIsRepo } from "./native-git-bridge.js";
13
+ import { getHomeDir } from "./home-dir.js";
13
14
  import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
14
15
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
15
16
  import { execFileSync } from "node:child_process";
@@ -461,8 +462,8 @@ export function updateProgressWidget(ctx, unitType, unitId, state, accessors, ti
461
462
  let widgetPwd;
462
463
  {
463
464
  let fullPwd = process.cwd();
464
- const widgetHome = process.env.HOME || process.env.USERPROFILE;
465
- if (widgetHome && fullPwd.startsWith(widgetHome)) {
465
+ const widgetHome = getHomeDir();
466
+ if (widgetHome && (fullPwd === widgetHome || fullPwd.startsWith(widgetHome + "/") || fullPwd.startsWith(widgetHome + "\\"))) {
466
467
  fullPwd = `~${fullPwd.slice(widgetHome.length)}`;
467
468
  }
468
469
  const parts = fullPwd.split("/");
@@ -655,14 +655,25 @@ export const DISPATCH_RULES = [
655
655
  return null;
656
656
  if (!state.activeSlice)
657
657
  return null; // fall through
658
- // Only activate when reactive_execution is explicitly enabled
658
+ // Reactive dispatch is on by default when there are enough ready tasks to
659
+ // benefit from parallelism. Users opt out explicitly via
660
+ // `reactive_execution.enabled: false`. The downstream safety checks
661
+ // (graph ambiguity, ready-task count, conflict-free selection) still gate
662
+ // every actual dispatch, so the worst-case "default-on" outcome is the
663
+ // same fall-through to sequential execution as before.
659
664
  const reactiveConfig = prefs?.reactive_execution;
660
- if (!reactiveConfig?.enabled)
665
+ if (reactiveConfig?.enabled === false)
661
666
  return null;
662
667
  const sid = state.activeSlice.id;
663
668
  const sTitle = state.activeSlice.title;
664
- const maxParallel = reactiveConfig.max_parallel ?? 2;
665
- const subagentModel = reactiveConfig.subagent_model ?? resolveModelWithFallbacksForUnit("subagent")?.primary;
669
+ const maxParallel = reactiveConfig?.max_parallel ?? 2;
670
+ const subagentModel = reactiveConfig?.subagent_model ?? resolveModelWithFallbacksForUnit("subagent")?.primary;
671
+ // Default-on safety threshold: only activate reactive dispatch when at
672
+ // least N tasks are ready. Users who explicitly enabled reactive_execution
673
+ // keep the legacy threshold of 2 (matches the prior "any parallelism is
674
+ // better than none" intent). Default-on installs require >=3 to avoid
675
+ // surprising users with parallelism on small slices.
676
+ const minReadyTasksForReactive = reactiveConfig?.enabled === true ? 2 : 3;
666
677
  // Dry-run mode: max_parallel=1 means graph is derived and logged but
667
678
  // execution remains sequential
668
679
  if (maxParallel <= 1)
@@ -678,8 +689,9 @@ export const DISPATCH_RULES = [
678
689
  return null;
679
690
  const completed = new Set(graph.filter((n) => n.done).map((n) => n.id));
680
691
  const readyIds = getReadyTasks(graph, completed, new Set());
681
- // Only activate reactive dispatch when >1 task is ready
682
- if (readyIds.length <= 1)
692
+ // Only activate reactive dispatch when enough tasks are ready.
693
+ // Threshold is 2 when explicitly opted in, 3 when default-on.
694
+ if (readyIds.length < minReadyTasksForReactive)
683
695
  return null;
684
696
  const uokFlags = resolveUokFlags(prefs);
685
697
  const selected = uokFlags.executionGraph
@@ -23,7 +23,7 @@ import { composeInlinedContext } from "./unit-context-composer.js";
23
23
  import { logWarning } from "./workflow-logger.js";
24
24
  import { inlineGraphSubgraph } from "./graph-context.js";
25
25
  import { buildExtractionStepsBlock } from "./commands-extract-learnings.js";
26
- import { warnIfManifestHasMissingSkills } from "./skill-manifest.js";
26
+ import { resolveSkillManifest, warnIfManifestHasMissingSkills } from "./skill-manifest.js";
27
27
  // ─── Preamble Cap ─────────────────────────────────────────────────────────────
28
28
  /**
29
29
  * Historical static ceiling for the preamble cap. Kept as an upper bound even
@@ -675,6 +675,26 @@ function formatSkillActivationBlock(skillNames) {
675
675
  const calls = safe.map(name => `Call Skill({ skill: '${name}' })`).join('. ');
676
676
  return `<skill_activation>${calls}.</skill_activation>`;
677
677
  }
678
+ /**
679
+ * Manifest-driven recommendations block — informational only, does NOT
680
+ * auto-invoke. Lists per-unit-type skills that are installed but not already
681
+ * activated by explicit user intent (always_use_skills / prefer_skills /
682
+ * skill_rules / task-plan skills_used). Surfaces relevant skills to the
683
+ * model so they can be invoked when the model judges them useful.
684
+ *
685
+ * This is the additive complement to the existing activation directive:
686
+ * activation force-invokes (explicit intent), recommendations remind
687
+ * (manifest defaults). User intent is preserved as the stronger signal
688
+ * (RFC #4779 design principle); this block only adds visibility.
689
+ */
690
+ function formatSkillRecommendationsBlock(unitType, skillNames) {
691
+ if (!unitType)
692
+ return "";
693
+ const safe = skillNames.filter(name => SAFE_SKILL_NAME.test(name));
694
+ if (safe.length === 0)
695
+ return "";
696
+ return `<skill_recommendations unit="${unitType}">For this unit type, also consider invoking: ${safe.join(", ")}. Use Skill({ skill: 'name' }) when relevant — these are recommendations, not requirements.</skill_recommendations>`;
697
+ }
678
698
  export function buildSkillActivationBlock(params) {
679
699
  const prefs = params.preferences ?? loadEffectiveGSDPreferences(params.base)?.preferences;
680
700
  const contextTokens = tokenizeSkillContext(params.milestoneId, params.milestoneTitle, params.sliceId, params.sliceTitle, params.taskId, params.taskTitle);
@@ -717,10 +737,51 @@ export function buildSkillActivationBlock(params) {
717
737
  logWarning("prompt", `parseTaskPlanFile failed: ${err instanceof Error ? err.message : String(err)}`);
718
738
  }
719
739
  }
740
+ // Heuristic auto-match (gated on skill_discovery: "auto").
741
+ // For each installed skill, check if its name or description appears in the
742
+ // unit's context tokens (milestone/slice/task titles). Only consider skills
743
+ // already on the unit-type manifest allowlist — this keeps the heuristic
744
+ // narrow and avoids wildly off-topic activations.
745
+ // Users who set `skill_discovery: "off"` or "suggest" do not get
746
+ // auto-matched skills (the recommendations block still surfaces manifest
747
+ // skills passively); only "auto" actually adds them to the activation
748
+ // directive set. Default `skill_discovery` is "suggest", so this is opt-in.
749
+ if ((prefs?.skill_discovery ?? "suggest") === "auto") {
750
+ const manifestAllow = resolveSkillManifest(params.unitType);
751
+ const allowSet = manifestAllow ? new Set(manifestAllow) : null;
752
+ for (const skill of visibleSkills) {
753
+ const normalized = normalizeSkillReference(skill.name);
754
+ if (matched.has(normalized) || avoided.has(normalized))
755
+ continue;
756
+ // Respect the manifest allowlist when present; wildcard (null) lets all
757
+ // installed skills compete for keyword match.
758
+ if (allowSet && !allowSet.has(normalized))
759
+ continue;
760
+ if (skillMatchesContext(skill, contextTokens)) {
761
+ matched.add(normalized);
762
+ }
763
+ }
764
+ }
720
765
  const ordered = [...matched]
721
766
  .filter(name => installedNames.has(name) && !avoided.has(name))
722
767
  .sort();
723
- return formatSkillActivationBlock(ordered);
768
+ const activationBlock = formatSkillActivationBlock(ordered);
769
+ // Manifest-driven recommendations (additive, does not override explicit intent).
770
+ // Only surface skills the manifest declares for this unit type that are
771
+ // installed and not already in matched/avoided.
772
+ const matchedSet = new Set(ordered);
773
+ const manifestList = resolveSkillManifest(params.unitType);
774
+ const recommendations = (manifestList ?? [])
775
+ .filter(name => installedNames.has(name) && !avoided.has(name) && !matchedSet.has(name))
776
+ .sort();
777
+ const recommendationsBlock = formatSkillRecommendationsBlock(params.unitType, recommendations);
778
+ if (!activationBlock && !recommendationsBlock)
779
+ return "";
780
+ if (!activationBlock)
781
+ return recommendationsBlock;
782
+ if (!recommendationsBlock)
783
+ return activationBlock;
784
+ return `${activationBlock}\n${recommendationsBlock}`;
724
785
  }
725
786
  /**
726
787
  * Build the skill discovery template variables for research prompts.
@@ -16,6 +16,7 @@ import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
16
16
  import { gsdRoot } from "./paths.js";
17
17
  import { createWorktree, removeWorktree, resolveGitDir, worktreePath, isInsideWorktreesDir, } from "./worktree-manager.js";
18
18
  import { detectWorktreeName, nudgeGitBranchCache, } from "./worktree.js";
19
+ import { isGsdWorktreePath, normalizeWorktreePathForCompare, resolveWorktreeProjectRoot, } from "./worktree-root.js";
19
20
  import { MergeConflictError, readIntegrationBranch, RUNTIME_EXCLUSION_PATHS } from "./git-service.js";
20
21
  import { debugLog } from "./debug-logger.js";
21
22
  import { logWarning, logError } from "./workflow-logger.js";
@@ -1038,6 +1039,7 @@ function reconcilePlanCheckboxes(projectRoot, wtPath, milestoneId) {
1038
1039
  }
1039
1040
  }
1040
1041
  export function createAutoWorktree(basePath, milestoneId) {
1042
+ basePath = resolveWorktreeProjectRoot(basePath);
1041
1043
  // Check if repo has commits — git worktree requires a valid HEAD
1042
1044
  try {
1043
1045
  execFileSync("git", ["rev-parse", "--verify", "HEAD"], { cwd: basePath, stdio: "pipe" });
@@ -1171,6 +1173,7 @@ function copyPlanningArtifacts(srcBase, wtPath) {
1171
1173
  * the worktree and its branch.
1172
1174
  */
1173
1175
  export function teardownAutoWorktree(originalBasePath, milestoneId, opts = {}) {
1176
+ originalBasePath = resolveWorktreeProjectRoot(originalBasePath);
1174
1177
  const branch = autoWorktreeBranch(milestoneId);
1175
1178
  const { preserveBranch = false } = opts;
1176
1179
  const previousCwd = process.cwd();
@@ -1212,18 +1215,26 @@ export function teardownAutoWorktree(originalBasePath, milestoneId, opts = {}) {
1212
1215
  }
1213
1216
  /**
1214
1217
  * Detect if the process is currently inside an auto-worktree.
1215
- * Checks both module state and git branch prefix.
1218
+ * Uses the current directory structure plus git branch prefix so detection
1219
+ * still works after process restart when module state has been reset.
1216
1220
  */
1217
1221
  export function isInAutoWorktree(basePath) {
1218
- if (!originalBase)
1219
- return false;
1220
1222
  const cwd = process.cwd();
1221
- const resolvedBase = existsSync(basePath) ? realpathSync(basePath) : basePath;
1222
- const wtDir = join(resolvedBase, ".gsd", "worktrees");
1223
- if (!cwd.startsWith(wtDir))
1223
+ if (!isGsdWorktreePath(cwd))
1224
1224
  return false;
1225
- const branch = nativeGetCurrentBranch(cwd);
1226
- return branch.startsWith("milestone/");
1225
+ const projectRoot = resolveWorktreeProjectRoot(basePath, originalBase);
1226
+ const cwdProjectRoot = resolveWorktreeProjectRoot(cwd, originalBase);
1227
+ if (normalizeWorktreePathForCompare(projectRoot) !==
1228
+ normalizeWorktreePathForCompare(cwdProjectRoot)) {
1229
+ return false;
1230
+ }
1231
+ try {
1232
+ const branch = nativeGetCurrentBranch(cwd);
1233
+ return branch.startsWith("milestone/");
1234
+ }
1235
+ catch {
1236
+ return false;
1237
+ }
1227
1238
  }
1228
1239
  /**
1229
1240
  * Get the filesystem path for an auto-worktree, or null if it doesn't exist
@@ -1234,6 +1245,7 @@ export function isInAutoWorktree(basePath) {
1234
1245
  * mis-detection of leftover directories as active worktrees (#695).
1235
1246
  */
1236
1247
  export function getAutoWorktreePath(basePath, milestoneId) {
1248
+ basePath = resolveWorktreeProjectRoot(basePath);
1237
1249
  const p = worktreePath(basePath, milestoneId);
1238
1250
  if (!existsSync(p))
1239
1251
  return null;
@@ -1260,6 +1272,7 @@ export function getAutoWorktreePath(basePath, milestoneId) {
1260
1272
  * Atomic: chdir + originalBase update in same try block.
1261
1273
  */
1262
1274
  export function enterAutoWorktree(basePath, milestoneId) {
1275
+ basePath = resolveWorktreeProjectRoot(basePath);
1263
1276
  const p = worktreePath(basePath, milestoneId);
1264
1277
  if (!existsSync(p)) {
1265
1278
  throw new GSDError(GSD_IO_ERROR, `Auto-worktree for ${milestoneId} does not exist at ${p}`);
@@ -1298,16 +1311,20 @@ export function enterAutoWorktree(basePath, milestoneId) {
1298
1311
  export function getAutoWorktreeOriginalBase() {
1299
1312
  return originalBase;
1300
1313
  }
1314
+ export function _resetAutoWorktreeOriginalBaseForTests() {
1315
+ originalBase = null;
1316
+ }
1301
1317
  export function getActiveAutoWorktreeContext() {
1302
1318
  if (!originalBase)
1303
1319
  return null;
1304
1320
  const cwd = process.cwd();
1305
- const resolvedBase = existsSync(originalBase)
1306
- ? realpathSync(originalBase)
1307
- : originalBase;
1308
- const wtDir = join(resolvedBase, ".gsd", "worktrees");
1309
- if (!cwd.startsWith(wtDir))
1321
+ if (!isGsdWorktreePath(cwd))
1322
+ return null;
1323
+ const cwdProjectRoot = resolveWorktreeProjectRoot(cwd, originalBase);
1324
+ if (normalizeWorktreePathForCompare(cwdProjectRoot) !==
1325
+ normalizeWorktreePathForCompare(originalBase)) {
1310
1326
  return null;
1327
+ }
1311
1328
  const worktreeName = detectWorktreeName(cwd);
1312
1329
  if (!worktreeName)
1313
1330
  return null;