gsd-pi 2.76.0-dev.82e249f7b → 2.76.0-dev.fe143342a

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 (139) hide show
  1. package/dist/mcp-server.d.ts +7 -0
  2. package/dist/mcp-server.js +35 -1
  3. package/dist/resource-loader.d.ts +1 -1
  4. package/dist/resource-loader.js +2 -8
  5. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +66 -4
  6. package/dist/resources/extensions/gsd/auto-start.js +27 -14
  7. package/dist/resources/extensions/gsd/auto.js +11 -11
  8. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
  9. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
  10. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  11. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +35 -0
  12. package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
  13. package/dist/resources/extensions/gsd/error-classifier.js +10 -3
  14. package/dist/resources/extensions/gsd/exec-history.js +120 -0
  15. package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
  16. package/dist/resources/extensions/gsd/gsd-db.js +62 -4
  17. package/dist/resources/extensions/gsd/init-wizard.js +15 -1
  18. package/dist/resources/extensions/gsd/key-manager.js +6 -0
  19. package/dist/resources/extensions/gsd/pre-execution-checks.js +13 -3
  20. package/dist/resources/extensions/gsd/preferences-types.js +9 -0
  21. package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
  22. package/dist/resources/extensions/gsd/preferences.js +17 -17
  23. package/dist/resources/extensions/gsd/safety/file-change-validator.js +1 -1
  24. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  25. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  26. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  27. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  28. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  29. package/dist/web/standalone/.next/BUILD_ID +1 -1
  30. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  31. package/dist/web/standalone/.next/build-manifest.json +2 -2
  32. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  33. package/dist/web/standalone/.next/required-server-files.json +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.html +1 -1
  51. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  58. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  59. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  60. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  61. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  62. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  63. package/dist/web/standalone/server.js +1 -1
  64. package/package.json +1 -1
  65. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  66. package/packages/mcp-server/dist/workflow-tools.js +64 -25
  67. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  68. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  69. package/packages/mcp-server/src/workflow-tools.ts +84 -43
  70. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  71. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  72. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  73. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  74. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  75. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  76. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  77. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  78. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  79. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
  81. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  82. package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
  83. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  84. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
  85. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  90. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
  91. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  92. package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
  93. package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
  94. package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
  95. package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
  96. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
  97. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
  98. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  99. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +67 -4
  100. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +137 -2
  101. package/src/resources/extensions/gsd/auto-start.ts +28 -15
  102. package/src/resources/extensions/gsd/auto.ts +11 -11
  103. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  104. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  105. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  106. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +36 -0
  107. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  108. package/src/resources/extensions/gsd/error-classifier.ts +10 -3
  109. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  110. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  111. package/src/resources/extensions/gsd/gsd-db.ts +68 -4
  112. package/src/resources/extensions/gsd/init-wizard.ts +15 -1
  113. package/src/resources/extensions/gsd/key-manager.ts +6 -0
  114. package/src/resources/extensions/gsd/pre-execution-checks.ts +13 -3
  115. package/src/resources/extensions/gsd/preferences-types.ts +38 -0
  116. package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
  117. package/src/resources/extensions/gsd/preferences.ts +17 -17
  118. package/src/resources/extensions/gsd/safety/file-change-validator.ts +1 -1
  119. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  120. package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
  121. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  122. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +20 -0
  123. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +151 -0
  124. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
  125. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  126. package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
  127. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  128. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +19 -0
  129. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  130. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
  131. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  132. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  133. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  134. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  135. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  136. package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
  137. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  138. /package/dist/web/standalone/.next/static/{ecSsu49rxxcpbNmVP4mLD → n21VtX2hZlkpdEUO_nU4z}/_buildManifest.js +0 -0
  139. /package/dist/web/standalone/.next/static/{ecSsu49rxxcpbNmVP4mLD → n21VtX2hZlkpdEUO_nU4z}/_ssgManifest.js +0 -0
@@ -8,7 +8,7 @@
8
8
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { showNextAction } from "../shared/tui.js";
11
- import { nativeInit } from "./native-git-bridge.js";
11
+ import { nativeInit, nativeAddAll, nativeCommit } from "./native-git-bridge.js";
12
12
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
13
13
  import { gsdRoot } from "./paths.js";
14
14
  import { assertSafeDirectory } from "./validate-directory.js";
@@ -40,6 +40,7 @@ export async function showProjectInit(ctx, pi, basePath, detection) {
40
40
  ctx.ui.notify(`Project detected:\n${detectionSummary.join("\n")}`, "info");
41
41
  }
42
42
  // ── Step 2: Git setup ──────────────────────────────────────────────────────
43
+ let didInitGit = false;
43
44
  if (!signals.isGitRepo) {
44
45
  const gitChoice = await showNextAction(ctx, {
45
46
  title: "GSD — Project Setup",
@@ -54,6 +55,7 @@ export async function showProjectInit(ctx, pi, basePath, detection) {
54
55
  return { completed: false, bootstrapped: false };
55
56
  if (gitChoice === "init_git") {
56
57
  nativeInit(basePath, prefs.mainBranch);
58
+ didInitGit = true;
57
59
  }
58
60
  }
59
61
  else {
@@ -244,6 +246,18 @@ export async function showProjectInit(ctx, pi, basePath, detection) {
244
246
  // Ensure .gitignore
245
247
  ensureGitignore(basePath);
246
248
  untrackRuntimeFiles(basePath);
249
+ // Create initial commit so git log and git worktree work immediately (#4530).
250
+ // Without this, the branch is "unborn" (zero commits) and downstream operations
251
+ // like `git log` and `git worktree add` fail.
252
+ if (didInitGit) {
253
+ try {
254
+ nativeAddAll(basePath);
255
+ nativeCommit(basePath, "chore: init project");
256
+ }
257
+ catch {
258
+ // Non-fatal — user can commit manually; don't block project init
259
+ }
260
+ }
247
261
  // Auto-generate codebase map for instant agent orientation
248
262
  try {
249
263
  const result = generateCodebaseMap(basePath);
@@ -12,6 +12,12 @@ import { getErrorMessage } from "./error-utils.js";
12
12
  export const PROVIDER_REGISTRY = [
13
13
  // LLM Providers
14
14
  { id: "anthropic", label: "Anthropic (Claude)", category: "llm", envVar: "ANTHROPIC_API_KEY", prefixes: ["sk-ant-"], hasOAuth: true, dashboardUrl: "console.anthropic.com" },
15
+ // Claude Code CLI: routes through the local `claude` binary — no API key,
16
+ // authentication is handled by the CLI's own OAuth flow.
17
+ // Referenced by doctor-providers.ts, auto-model-selection.ts, and others;
18
+ // must be in the canonical registry so all consumers see the same catalog.
19
+ // See: https://github.com/gsd-build/gsd-2/issues/4541
20
+ { id: "claude-code", label: "Claude Code CLI", category: "llm", hasOAuth: true },
15
21
  { id: "openai", label: "OpenAI", category: "llm", envVar: "OPENAI_API_KEY", prefixes: ["sk-"], dashboardUrl: "platform.openai.com/api-keys" },
16
22
  { id: "github-copilot", label: "GitHub Copilot", category: "llm", envVar: "GITHUB_TOKEN", hasOAuth: true },
17
23
  { id: "openai-codex", label: "ChatGPT Plus/Pro (Codex)", category: "llm", hasOAuth: true },
@@ -68,8 +68,13 @@ export function extractPackageReferences(description) {
68
68
  }
69
69
  }
70
70
  }
71
- // require('pkg') or import from 'pkg' in code blocks
72
- const importPattern = /(?:require\s*\(\s*['"]|from\s+['"])([a-zA-Z0-9@/_-]+)['"\)]/g;
71
+ // require('pkg') or `import ... from 'pkg'` in code blocks.
72
+ // The `from\s+['"]` branch MUST be preceded by an `import` keyword so that
73
+ // natural-language prose like `from "What's Next"` or `from 'master'` does
74
+ // not produce false package-existence failures. Requiring the leading import
75
+ // keyword anchors the match to JavaScript/TypeScript syntax.
76
+ // See: https://github.com/gsd-build/gsd-2/issues/4388
77
+ const importPattern = /(?:require\s*\(\s*['"]|import\b[\s\S]*?\bfrom\s+['"])([a-zA-Z0-9@/_-]+)['"\)]/g;
73
78
  let importMatch;
74
79
  while ((importMatch = importPattern.exec(description)) !== null) {
75
80
  // Skip relative imports and node builtins
@@ -278,7 +283,12 @@ function extractPathFromAnnotation(raw) {
278
283
  }
279
284
  const annotatedMatch = trimmed.match(/^(.+?)\s+[—–-]\s+.+$/);
280
285
  if (annotatedMatch) {
281
- return annotatedMatch[1].trim();
286
+ const prefix = annotatedMatch[1].trim();
287
+ const prefixBacktickMatch = prefix.match(/`([^`]+)`/);
288
+ if (prefixBacktickMatch && looksLikePathOrUrl(prefixBacktickMatch[1].trim())) {
289
+ return prefixBacktickMatch[1].trim();
290
+ }
291
+ return prefix.replace(/`/g, "").trim();
282
292
  }
283
293
  // Fallback: scan all backticked tokens and return the first one that looks
284
294
  // like a path or URL. Handles prose-annotated bullets such as:
@@ -5,6 +5,14 @@
5
5
  * both the validation and runtime modules can import them without pulling
6
6
  * in filesystem or loading logic.
7
7
  */
8
+ /**
9
+ * Resolve whether context-mode features (gsd_exec sandbox + compaction
10
+ * snapshot) should be active. Default is ON: missing config or missing
11
+ * `enabled` is treated as true. Only `enabled: false` disables.
12
+ */
13
+ export function isContextModeEnabled(prefs) {
14
+ return prefs?.context_mode?.enabled !== false;
15
+ }
8
16
  /** Default preference values for each workflow mode. */
9
17
  export const MODE_DEFAULTS = {
10
18
  solo: {
@@ -87,6 +95,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set([
87
95
  "flat_rate_providers",
88
96
  "language",
89
97
  "context_window_override",
98
+ "context_mode",
90
99
  ]);
91
100
  /** Canonical list of all dispatch unit types. */
92
101
  export const KNOWN_UNIT_TYPES = [
@@ -660,6 +660,55 @@ export function validatePreferences(preferences) {
660
660
  errors.push("context_management must be an object");
661
661
  }
662
662
  }
663
+ // ─── Context Mode (gsd_exec sandbox) ────────────────────────────────────
664
+ if (preferences.context_mode !== undefined) {
665
+ if (typeof preferences.context_mode === "object" && preferences.context_mode !== null) {
666
+ const cmode = preferences.context_mode;
667
+ const validCmode = {};
668
+ if (cmode.enabled !== undefined) {
669
+ if (typeof cmode.enabled === "boolean")
670
+ validCmode.enabled = cmode.enabled;
671
+ else
672
+ errors.push("context_mode.enabled must be a boolean");
673
+ }
674
+ if (cmode.exec_timeout_ms !== undefined) {
675
+ const t = cmode.exec_timeout_ms;
676
+ if (typeof t === "number" && t >= 1000 && t <= 600_000)
677
+ validCmode.exec_timeout_ms = Math.floor(t);
678
+ else
679
+ errors.push("context_mode.exec_timeout_ms must be a number between 1000 and 600000");
680
+ }
681
+ if (cmode.exec_stdout_cap_bytes !== undefined) {
682
+ const b = cmode.exec_stdout_cap_bytes;
683
+ if (typeof b === "number" && b >= 4096 && b <= 16_777_216)
684
+ validCmode.exec_stdout_cap_bytes = Math.floor(b);
685
+ else
686
+ errors.push("context_mode.exec_stdout_cap_bytes must be a number between 4096 and 16777216");
687
+ }
688
+ if (cmode.exec_digest_chars !== undefined) {
689
+ const c = cmode.exec_digest_chars;
690
+ if (typeof c === "number" && c >= 0 && c <= 4000)
691
+ validCmode.exec_digest_chars = Math.floor(c);
692
+ else
693
+ errors.push("context_mode.exec_digest_chars must be a number between 0 and 4000");
694
+ }
695
+ if (cmode.exec_env_allowlist !== undefined) {
696
+ if (Array.isArray(cmode.exec_env_allowlist) &&
697
+ cmode.exec_env_allowlist.every((v) => typeof v === "string" && /^[A-Z_][A-Z0-9_]*$/i.test(v))) {
698
+ validCmode.exec_env_allowlist = cmode.exec_env_allowlist;
699
+ }
700
+ else {
701
+ errors.push("context_mode.exec_env_allowlist must be an array of valid env var names");
702
+ }
703
+ }
704
+ if (Object.keys(validCmode).length > 0) {
705
+ validated.context_mode = validCmode;
706
+ }
707
+ }
708
+ else {
709
+ errors.push("context_mode must be an object");
710
+ }
711
+ }
663
712
  // ─── Parallel Config ────────────────────────────────────────────────────
664
713
  if (preferences.parallel && typeof preferences.parallel === "object") {
665
714
  const p = preferences.parallel;
@@ -716,6 +765,40 @@ export function validatePreferences(preferences) {
716
765
  validated.parallel = parallel;
717
766
  }
718
767
  }
768
+ // ─── Slice Parallel Config ───────────────────────────────────────────────
769
+ if (preferences.slice_parallel !== undefined) {
770
+ if (typeof preferences.slice_parallel === "object" && preferences.slice_parallel !== null) {
771
+ const sp = preferences.slice_parallel;
772
+ const validSp = {};
773
+ if (sp.enabled !== undefined) {
774
+ if (typeof sp.enabled === "boolean")
775
+ validSp.enabled = sp.enabled;
776
+ else
777
+ errors.push("slice_parallel.enabled must be a boolean");
778
+ }
779
+ if (sp.max_workers !== undefined) {
780
+ const maxWorkers = typeof sp.max_workers === "number" ? sp.max_workers : Number(sp.max_workers);
781
+ if (Number.isFinite(maxWorkers) && maxWorkers >= 1 && maxWorkers <= 8) {
782
+ validSp.max_workers = Math.floor(maxWorkers);
783
+ }
784
+ else {
785
+ errors.push("slice_parallel.max_workers must be a number between 1 and 8");
786
+ }
787
+ }
788
+ const knownSliceParallelKeys = new Set(["enabled", "max_workers"]);
789
+ for (const key of Object.keys(sp)) {
790
+ if (!knownSliceParallelKeys.has(key)) {
791
+ warnings.push(`unknown slice_parallel key "${key}" — ignored`);
792
+ }
793
+ }
794
+ if (Object.keys(validSp).length > 0) {
795
+ validated.slice_parallel = validSp;
796
+ }
797
+ }
798
+ else {
799
+ errors.push("slice_parallel must be an object");
800
+ }
801
+ }
719
802
  // ─── Reactive Execution ─────────────────────────────────────────────────
720
803
  if (preferences.reactive_execution !== undefined) {
721
804
  if (typeof preferences.reactive_execution === "object" && preferences.reactive_execution !== null) {
@@ -26,12 +26,12 @@ export { resolveAllSkillReferences } from "./preferences-skills.js";
26
26
  // These lived in preferences-skills.ts but imported loadEffectiveGSDPreferences
27
27
  // back from this file, creating a circular dependency. Moved here since they
28
28
  // are trivial wrappers over loadEffectiveGSDPreferences.
29
- export function resolveSkillDiscoveryMode() {
30
- const prefs = loadEffectiveGSDPreferences();
29
+ export function resolveSkillDiscoveryMode(basePath) {
30
+ const prefs = loadEffectiveGSDPreferences(basePath);
31
31
  return prefs?.preferences.skill_discovery ?? "suggest";
32
32
  }
33
- export function resolveSkillStalenessDays() {
34
- const prefs = loadEffectiveGSDPreferences();
33
+ export function resolveSkillStalenessDays(basePath) {
34
+ const prefs = loadEffectiveGSDPreferences(basePath);
35
35
  return prefs?.preferences.skill_staleness_days ?? 60;
36
36
  }
37
37
  // ─── Re-exports: models ─────────────────────────────────────────────────────
@@ -46,16 +46,16 @@ function globalPreferencesPath() {
46
46
  function legacyGlobalPreferencesPath() {
47
47
  return join(homedir(), ".pi", "agent", "gsd-preferences.md");
48
48
  }
49
- function projectPreferencesPath() {
50
- return join(gsdRoot(process.cwd()), "PREFERENCES.md");
49
+ function projectPreferencesPath(basePath = process.cwd()) {
50
+ return join(gsdRoot(basePath), "PREFERENCES.md");
51
51
  }
52
52
  // Legacy lowercase files can still exist in older projects. Keep them as a
53
53
  // compatibility-only fallback, but route new reads/writes through PREFERENCES.md.
54
54
  function legacyGlobalPreferencesPathLowercase() {
55
55
  return join(gsdHome(), "preferences.md");
56
56
  }
57
- function legacyProjectPreferencesPathLowercase() {
58
- return join(gsdRoot(process.cwd()), "preferences.md");
57
+ function legacyProjectPreferencesPathLowercase(basePath = process.cwd()) {
58
+ return join(gsdRoot(basePath), "preferences.md");
59
59
  }
60
60
  export function getGlobalGSDPreferencesPath() {
61
61
  return globalPreferencesPath();
@@ -63,8 +63,8 @@ export function getGlobalGSDPreferencesPath() {
63
63
  export function getLegacyGlobalGSDPreferencesPath() {
64
64
  return legacyGlobalPreferencesPath();
65
65
  }
66
- export function getProjectGSDPreferencesPath() {
67
- return projectPreferencesPath();
66
+ export function getProjectGSDPreferencesPath(basePath) {
67
+ return projectPreferencesPath(basePath);
68
68
  }
69
69
  // ─── Loading ────────────────────────────────────────────────────────────────
70
70
  export function loadGlobalGSDPreferences() {
@@ -72,13 +72,13 @@ export function loadGlobalGSDPreferences() {
72
72
  ?? loadPreferencesFile(legacyGlobalPreferencesPathLowercase(), "global")
73
73
  ?? loadPreferencesFile(legacyGlobalPreferencesPath(), "global");
74
74
  }
75
- export function loadProjectGSDPreferences() {
76
- return loadPreferencesFile(projectPreferencesPath(), "project")
77
- ?? loadPreferencesFile(legacyProjectPreferencesPathLowercase(), "project");
75
+ export function loadProjectGSDPreferences(basePath) {
76
+ return loadPreferencesFile(projectPreferencesPath(basePath), "project")
77
+ ?? loadPreferencesFile(legacyProjectPreferencesPathLowercase(basePath), "project");
78
78
  }
79
- export function loadEffectiveGSDPreferences() {
79
+ export function loadEffectiveGSDPreferences(basePath) {
80
80
  const globalPreferences = loadGlobalGSDPreferences();
81
- const projectPreferences = loadProjectGSDPreferences();
81
+ const projectPreferences = loadProjectGSDPreferences(basePath);
82
82
  if (!globalPreferences && !projectPreferences)
83
83
  return null;
84
84
  let result;
@@ -489,8 +489,8 @@ export function resolvePreDispatchHooks() {
489
489
  * Worktree isolation requires explicit opt-in because it depends on git
490
490
  * branch infrastructure that must be set up before use.
491
491
  */
492
- export function getIsolationMode() {
493
- const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
492
+ export function getIsolationMode(basePath) {
493
+ const prefs = loadEffectiveGSDPreferences(basePath)?.preferences?.git;
494
494
  if (prefs?.isolation === "worktree")
495
495
  return "worktree";
496
496
  if (prefs?.isolation === "branch")
@@ -62,7 +62,7 @@ export function validateFileChanges(basePath, expectedOutput, plannedFiles) {
62
62
  // ─── Internals ──────────────────────────────────────────────────────────────
63
63
  function getChangedFilesFromLastCommit(basePath) {
64
64
  try {
65
- const result = execFileSync("git", ["diff", "--name-only", "HEAD~1", "HEAD"], { cwd: basePath, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
65
+ const result = execFileSync("git", ["diff-tree", "--root", "--no-commit-id", "-r", "--name-only", "HEAD"], { cwd: basePath, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
66
66
  return result ? result.split("\n").filter(Boolean) : [];
67
67
  }
68
68
  catch (e) {
@@ -0,0 +1,59 @@
1
+ // GSD Exec Search Tool — lists and filters prior gsd_exec runs.
2
+ //
3
+ // Scans .gsd/exec/*.meta.json and returns a ranked summary so agents can
4
+ // re-discover past runs without re-executing. Read-only; no DB writes.
5
+ import { searchExecHistory } from "../exec-history.js";
6
+ export function executeExecSearch(params, opts) {
7
+ const searchOpts = {
8
+ query: typeof params.query === "string" ? params.query : undefined,
9
+ runtime: params.runtime,
10
+ failing_only: params.failing_only === true,
11
+ limit: typeof params.limit === "number" ? params.limit : undefined,
12
+ };
13
+ const hits = searchExecHistory(opts.baseDir, searchOpts);
14
+ if (hits.length === 0) {
15
+ return {
16
+ content: [{ type: "text", text: "No prior gsd_exec runs match those filters." }],
17
+ details: { operation: "gsd_exec_search", matches: 0 },
18
+ };
19
+ }
20
+ const lines = [`Found ${hits.length} exec run(s), most recent first:`];
21
+ for (const hit of hits) {
22
+ const e = hit.entry;
23
+ const status = formatStatus(e);
24
+ const purpose = e.purpose ? ` — ${e.purpose}` : "";
25
+ const truncated = e.stdout_truncated ? " (stdout truncated)" : "";
26
+ lines.push(`- [${e.id}] ${e.runtime} ${status} ${e.duration_ms}ms${truncated}${purpose}`, ` stdout: ${e.stdout_path}`);
27
+ if (hit.digest_preview) {
28
+ const preview = hit.digest_preview.replace(/\n/g, "\n ");
29
+ lines.push(` preview:\n ${preview}`);
30
+ }
31
+ }
32
+ return {
33
+ content: [{ type: "text", text: lines.join("\n") }],
34
+ details: {
35
+ operation: "gsd_exec_search",
36
+ matches: hits.length,
37
+ results: hits.map((hit) => ({
38
+ id: hit.entry.id,
39
+ runtime: hit.entry.runtime,
40
+ exit_code: hit.entry.exit_code,
41
+ timed_out: hit.entry.timed_out,
42
+ duration_ms: hit.entry.duration_ms,
43
+ purpose: hit.entry.purpose,
44
+ stdout_path: hit.entry.stdout_path,
45
+ stderr_path: hit.entry.stderr_path,
46
+ meta_path: hit.entry.meta_path,
47
+ })),
48
+ },
49
+ };
50
+ }
51
+ function formatStatus(entry) {
52
+ if (entry.timed_out)
53
+ return "timeout";
54
+ if (entry.signal)
55
+ return `signal:${entry.signal}`;
56
+ if (entry.exit_code === null)
57
+ return "exit:null";
58
+ return `exit:${entry.exit_code}`;
59
+ }
@@ -0,0 +1,126 @@
1
+ // GSD Exec Tool — executor for the gsd_exec MCP tool.
2
+ //
3
+ // Thin wrapper around exec-sandbox.ts that reads effective options from
4
+ // the project preferences (context_mode block) and formats the result
5
+ // for MCP return.
6
+ import { EXEC_DEFAULTS, runExecSandbox, } from "../exec-sandbox.js";
7
+ import { isContextModeEnabled } from "../preferences-types.js";
8
+ export function buildExecOptions(baseDir, cfg, extras) {
9
+ const allowlist = Array.isArray(cfg?.exec_env_allowlist) ? cfg.exec_env_allowlist : EXEC_DEFAULTS.envAllowlist;
10
+ const stdoutCap = clampNumber(cfg?.exec_stdout_cap_bytes, EXEC_DEFAULTS.stdoutCapBytes, 4_096, 16_777_216);
11
+ const defaultTimeout = clampNumber(cfg?.exec_timeout_ms, EXEC_DEFAULTS.defaultTimeoutMs, 1_000, EXEC_DEFAULTS.clampTimeoutMs);
12
+ const digestChars = clampNumber(cfg?.exec_digest_chars, EXEC_DEFAULTS.digestChars, 0, 4_000);
13
+ return {
14
+ baseDir,
15
+ clamp_timeout_ms: EXEC_DEFAULTS.clampTimeoutMs,
16
+ default_timeout_ms: defaultTimeout,
17
+ stdout_cap_bytes: stdoutCap,
18
+ stderr_cap_bytes: EXEC_DEFAULTS.stderrCapBytes,
19
+ digest_chars: digestChars,
20
+ env_allowlist: allowlist,
21
+ ...extras,
22
+ };
23
+ }
24
+ function clampNumber(value, fallback, min, max) {
25
+ if (typeof value !== "number" || !Number.isFinite(value))
26
+ return fallback;
27
+ if (value < min)
28
+ return min;
29
+ if (value > max)
30
+ return max;
31
+ return Math.floor(value);
32
+ }
33
+ function isEnabled(prefs) {
34
+ return isContextModeEnabled(prefs);
35
+ }
36
+ function disabledResult() {
37
+ return {
38
+ content: [
39
+ {
40
+ type: "text",
41
+ text: "gsd_exec is disabled by `context_mode.enabled: false` in preferences. Remove that " +
42
+ "override (or set it to true) to re-enable sandboxed tool-output execution.",
43
+ },
44
+ ],
45
+ details: { operation: "gsd_exec", error: "context_mode_disabled" },
46
+ isError: true,
47
+ };
48
+ }
49
+ function paramError(message) {
50
+ return {
51
+ content: [{ type: "text", text: `Error: ${message}` }],
52
+ details: { operation: "gsd_exec", error: "invalid_params", detail: message },
53
+ isError: true,
54
+ };
55
+ }
56
+ export async function executeGsdExec(params, deps) {
57
+ if (!isEnabled(deps.preferences))
58
+ return disabledResult();
59
+ const runtime = params.runtime;
60
+ if (runtime !== "bash" && runtime !== "node" && runtime !== "python") {
61
+ return paramError(`invalid runtime "${String(runtime)}" — must be bash | node | python`);
62
+ }
63
+ const script = typeof params.script === "string" ? params.script : "";
64
+ if (script.trim().length === 0) {
65
+ return paramError("script is required and must be a non-empty string");
66
+ }
67
+ if (Buffer.byteLength(script, "utf8") > 200_000) {
68
+ return paramError("script exceeds the 200 KB length limit");
69
+ }
70
+ const opts = buildExecOptions(deps.baseDir, deps.preferences?.context_mode, { now: deps.now, generateId: deps.generateId });
71
+ const run = deps.run ?? runExecSandbox;
72
+ try {
73
+ const result = await run({
74
+ runtime,
75
+ script,
76
+ ...(typeof params.purpose === "string" ? { purpose: params.purpose } : {}),
77
+ ...(typeof params.timeout_ms === "number" ? { timeout_ms: params.timeout_ms } : {}),
78
+ }, opts);
79
+ return formatResult(result);
80
+ }
81
+ catch (err) {
82
+ const message = err instanceof Error ? err.message : String(err);
83
+ return {
84
+ content: [{ type: "text", text: `Error: gsd_exec failed — ${message}` }],
85
+ details: { operation: "gsd_exec", error: message },
86
+ isError: true,
87
+ };
88
+ }
89
+ }
90
+ function formatResult(result) {
91
+ const headerLines = [
92
+ `gsd_exec[${result.id}] runtime=${result.runtime} exit=${formatExit(result)} duration=${result.duration_ms}ms`,
93
+ ` stdout: ${result.stdout_bytes}B${result.stdout_truncated ? " (truncated)" : ""} → ${result.stdout_path}`,
94
+ ` stderr: ${result.stderr_bytes}B${result.stderr_truncated ? " (truncated)" : ""} → ${result.stderr_path}`,
95
+ ];
96
+ const summary = `${headerLines.join("\n")}\n--- digest ---\n${result.digest}`.trimEnd();
97
+ return {
98
+ content: [{ type: "text", text: summary }],
99
+ details: {
100
+ operation: "gsd_exec",
101
+ id: result.id,
102
+ runtime: result.runtime,
103
+ exit_code: result.exit_code,
104
+ signal: result.signal,
105
+ timed_out: result.timed_out,
106
+ duration_ms: result.duration_ms,
107
+ stdout_bytes: result.stdout_bytes,
108
+ stderr_bytes: result.stderr_bytes,
109
+ stdout_truncated: result.stdout_truncated,
110
+ stderr_truncated: result.stderr_truncated,
111
+ stdout_path: result.stdout_path,
112
+ stderr_path: result.stderr_path,
113
+ meta_path: result.meta_path,
114
+ },
115
+ isError: result.timed_out || result.signal !== null || result.exit_code !== 0,
116
+ };
117
+ }
118
+ function formatExit(result) {
119
+ if (result.timed_out)
120
+ return "timeout";
121
+ if (result.signal)
122
+ return `signal:${result.signal}`;
123
+ if (result.exit_code === null)
124
+ return "null";
125
+ return String(result.exit_code);
126
+ }
@@ -0,0 +1,23 @@
1
+ // GSD Resume Tool — returns the contents of .gsd/last-snapshot.md so
2
+ // agents can re-orient after compaction or session resume without
3
+ // re-deriving project memory state.
4
+ import { readCompactionSnapshot } from "../compaction-snapshot.js";
5
+ export function executeResume(_params, opts) {
6
+ const snapshot = readCompactionSnapshot(opts.baseDir);
7
+ if (snapshot == null) {
8
+ return {
9
+ content: [
10
+ {
11
+ type: "text",
12
+ text: "No snapshot found at .gsd/last-snapshot.md. The snapshot is written automatically " +
13
+ "on session_before_compact (enabled by default; set context_mode.enabled=false to opt out).",
14
+ },
15
+ ],
16
+ details: { operation: "gsd_resume", found: false },
17
+ };
18
+ }
19
+ return {
20
+ content: [{ type: "text", text: snapshot }],
21
+ details: { operation: "gsd_resume", found: true, bytes: Buffer.byteLength(snapshot, "utf-8") },
22
+ };
23
+ }
@@ -5,6 +5,9 @@ import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  const MCP_WORKFLOW_TOOL_SURFACE = new Set([
6
6
  "ask_user_questions",
7
7
  "gsd_decision_save",
8
+ "gsd_exec",
9
+ "gsd_exec_search",
10
+ "gsd_resume",
8
11
  "gsd_complete_milestone",
9
12
  "gsd_complete_task",
10
13
  "gsd_complete_slice",