@opengsd/gsd-pi 1.0.2-dev.235ebf3 → 1.0.2-dev.2c204d3

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 (151) hide show
  1. package/README.md +63 -12
  2. package/dist/resource-loader.d.ts +7 -0
  3. package/dist/resource-loader.js +42 -9
  4. package/dist/resources/.managed-resources-content-hash +1 -1
  5. package/dist/resources/extensions/context7/index.js +12 -2
  6. package/dist/resources/extensions/gsd/auto/loop.js +19 -0
  7. package/dist/resources/extensions/gsd/auto/phases.js +1 -1
  8. package/dist/resources/extensions/gsd/auto/session.js +3 -0
  9. package/dist/resources/extensions/gsd/auto-start.js +232 -49
  10. package/dist/resources/extensions/gsd/auto-worktree.js +2 -54
  11. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +4 -3
  12. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -15
  13. package/dist/resources/extensions/gsd/closeout-recovery.js +7 -1
  14. package/dist/resources/extensions/gsd/commands/handlers/auto.js +9 -1
  15. package/dist/resources/extensions/gsd/commands-handlers.js +3 -0
  16. package/dist/resources/extensions/gsd/git-conflict-state.js +26 -1
  17. package/dist/resources/extensions/gsd/tools/complete-task.js +9 -0
  18. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +40 -1
  19. package/dist/resources/extensions/gsd/worktree-lifecycle.js +24 -3
  20. package/dist/resources/extensions/gsd/worktree-post-create-hook.js +117 -0
  21. package/dist/resources/extensions/search-the-web/native-search.js +57 -8
  22. package/dist/resources/shared/package-manager-detection.js +36 -0
  23. package/dist/update-check.d.ts +6 -2
  24. package/dist/update-check.js +7 -3
  25. package/dist/web/standalone/.next/BUILD_ID +1 -1
  26. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  27. package/dist/web/standalone/.next/build-manifest.json +2 -2
  28. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  29. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  46. package/dist/web/standalone/.next/server/app/api/session/events/route.js +1 -1
  47. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  48. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  49. package/dist/web/standalone/.next/server/app/index.html +1 -1
  50. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  57. package/dist/web/standalone/.next/server/chunks/1834.js +1 -1
  58. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  59. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  60. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  61. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  62. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  63. package/dist/web/standalone/package.json +0 -1
  64. package/dist/worktree-cli.d.ts +0 -2
  65. package/dist/worktree-cli.js +21 -9
  66. package/package.json +5 -2
  67. package/packages/cloud-mcp-gateway/bin/gsd-cloud-mcp-gateway.js +14 -0
  68. package/packages/cloud-mcp-gateway/package.json +4 -3
  69. package/packages/contracts/package.json +1 -1
  70. package/packages/daemon/package.json +4 -4
  71. package/packages/gsd-agent-core/package.json +5 -5
  72. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  73. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +3 -1
  74. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  75. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
  76. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +0 -1
  77. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  78. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts +1 -0
  79. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
  80. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -0
  81. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
  82. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  83. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +2 -1
  84. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
  85. package/packages/gsd-agent-modes/package.json +7 -7
  86. package/packages/mcp-server/bin/gsd-mcp-server.js +14 -0
  87. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  88. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  89. package/packages/mcp-server/package.json +5 -4
  90. package/packages/native/package.json +1 -1
  91. package/packages/pi-agent-core/dist/agent-loop.js +13 -13
  92. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  93. package/packages/pi-agent-core/package.json +1 -1
  94. package/packages/pi-ai/bin/pi-ai.js +14 -0
  95. package/packages/pi-ai/dist/models.generated.d.ts +40 -17
  96. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  97. package/packages/pi-ai/dist/models.generated.js +49 -30
  98. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  99. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  100. package/packages/pi-ai/dist/providers/anthropic.js +50 -0
  101. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  102. package/packages/pi-ai/dist/types.d.ts +2 -0
  103. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  104. package/packages/pi-ai/dist/types.js.map +1 -1
  105. package/packages/pi-ai/package.json +3 -2
  106. package/packages/pi-coding-agent/dist/core/tools/read.d.ts +2 -2
  107. package/packages/pi-coding-agent/dist/core/tools/read.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/core/tools/read.js +5 -3
  109. package/packages/pi-coding-agent/dist/core/tools/read.js.map +1 -1
  110. package/packages/pi-coding-agent/package.json +8 -8
  111. package/packages/pi-tui/package.json +1 -1
  112. package/packages/rpc-client/package.json +2 -2
  113. package/pkg/package.json +1 -1
  114. package/scripts/install/deps.js +10 -0
  115. package/scripts/install/detect-existing.js +17 -3
  116. package/scripts/install/npm-global.js +103 -33
  117. package/scripts/install.js +1 -0
  118. package/src/resources/extensions/context7/index.ts +15 -2
  119. package/src/resources/extensions/gsd/auto/loop.ts +22 -0
  120. package/src/resources/extensions/gsd/auto/phases.ts +1 -1
  121. package/src/resources/extensions/gsd/auto/session.ts +3 -0
  122. package/src/resources/extensions/gsd/auto-start.ts +307 -56
  123. package/src/resources/extensions/gsd/auto-worktree.ts +2 -56
  124. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +4 -3
  125. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -15
  126. package/src/resources/extensions/gsd/closeout-recovery.ts +6 -1
  127. package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -1
  128. package/src/resources/extensions/gsd/commands-handlers.ts +2 -0
  129. package/src/resources/extensions/gsd/git-conflict-state.ts +25 -1
  130. package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +436 -0
  131. package/src/resources/extensions/gsd/tests/closeout-recovery.test.ts +15 -0
  132. package/src/resources/extensions/gsd/tests/commands-dispatcher-workspace-git.test.ts +15 -2
  133. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +64 -0
  134. package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +70 -10
  135. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +13 -2
  136. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +24 -1
  137. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +60 -0
  138. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +54 -0
  139. package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +16 -1
  140. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +28 -0
  141. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +141 -1
  142. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +45 -1
  143. package/src/resources/extensions/gsd/tools/complete-task.ts +9 -0
  144. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +56 -4
  145. package/src/resources/extensions/gsd/worktree-lifecycle.ts +37 -2
  146. package/src/resources/extensions/gsd/worktree-post-create-hook.ts +127 -0
  147. package/src/resources/extensions/search-the-web/native-search.ts +60 -8
  148. package/src/resources/shared/package-manager-detection.ts +39 -0
  149. package/dist/tsconfig.extensions.tsbuildinfo +0 -1
  150. /package/dist/web/standalone/.next/static/{-P554bKh56nzavKUmvFM2 → mijI90BL1BdUcMUnhC0HU}/_buildManifest.js +0 -0
  151. /package/dist/web/standalone/.next/static/{-P554bKh56nzavKUmvFM2 → mijI90BL1BdUcMUnhC0HU}/_ssgManifest.js +0 -0
@@ -17,6 +17,7 @@ import { isAutoActive, checkRemoteAutoSession } from "./auto.js";
17
17
  import { getAutoWorktreePath } from "./auto-worktree.js";
18
18
  import { currentDirectoryRoot, projectRoot } from "./commands/context.js";
19
19
  import { loadPrompt } from "./prompt-loader.js";
20
+ import { isPnpmInstall } from "../../shared/package-manager-detection.js";
20
21
  import { buildDoctorHealIssuePayload, buildDoctorHealSummary, buildWorkflowDispatchContent, } from "./workflow-protocol.js";
21
22
  import { restoreGsdWorkflowTools, scopeGsdWorkflowToolsForDispatch, } from "./bootstrap/register-hooks.js";
22
23
  const UPDATE_REGISTRY_URL = "https://registry.npmjs.org/@opengsd%2fgsd-pi/latest";
@@ -42,6 +43,8 @@ function isBunInstall(argv1 = process.argv[1]) {
42
43
  function resolveInstallCommand(pkg) {
43
44
  if (isBunInstall())
44
45
  return `bun add -g ${pkg}`;
46
+ if (isPnpmInstall())
47
+ return `pnpm add -g ${pkg}`;
45
48
  return `npm install -g ${pkg}`;
46
49
  }
47
50
  async function fetchLatestVersionForCommand() {
@@ -2,7 +2,7 @@
2
2
  // File Purpose: Detect and reconcile unresolved Git conflict state before automation runs.
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { existsSync } from "node:fs";
5
- import { join } from "node:path";
5
+ import { dirname, join, resolve } from "node:path";
6
6
  import { autoResolveSafeConflictPaths } from "./git-conflict-resolve.js";
7
7
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
8
8
  import { abortAndReset } from "./git-self-heal.js";
@@ -10,6 +10,23 @@ import { logWarning } from "./workflow-logger.js";
10
10
  function splitZeroDelimited(output) {
11
11
  return output.split("\0").filter(Boolean);
12
12
  }
13
+ function hasGitMarker(basePath) {
14
+ try {
15
+ let dir = resolve(basePath);
16
+ for (let i = 0; i < 30; i++) {
17
+ if (existsSync(join(dir, ".git")))
18
+ return true;
19
+ const parent = dirname(dir);
20
+ if (parent === dir)
21
+ break;
22
+ dir = parent;
23
+ }
24
+ }
25
+ catch {
26
+ // Fall through to the git probes, which will report unknown on failure.
27
+ }
28
+ return false;
29
+ }
13
30
  export function listUnmergedGitPaths(basePath) {
14
31
  try {
15
32
  const output = spawnSync("git", ["diff", "--name-only", "--diff-filter=U", "-z"], {
@@ -57,6 +74,14 @@ export function listMergeStateBlockers(basePath) {
57
74
  return MERGE_STATE_MARKERS.filter((marker) => existsSync(join(gitDir, marker)));
58
75
  }
59
76
  export function probeGitConflictState(basePath) {
77
+ if (!hasGitMarker(basePath)) {
78
+ return {
79
+ status: "clean",
80
+ unmerged: [],
81
+ checkFailures: [],
82
+ mergeStateBlockers: [],
83
+ };
84
+ }
60
85
  const unmerged = listUnmergedGitPaths(basePath);
61
86
  if (unmerged === null) {
62
87
  return {
@@ -116,6 +116,15 @@ export async function handleCompleteTask(params, basePath) {
116
116
  if (!params.milestoneId || typeof params.milestoneId !== "string" || params.milestoneId.trim() === "") {
117
117
  return { error: "milestoneId is required and must be a non-empty string" };
118
118
  }
119
+ if (!params.oneLiner || typeof params.oneLiner !== "string" || params.oneLiner.trim() === "") {
120
+ return { error: "oneLiner is required and must be a non-empty string" };
121
+ }
122
+ if (!params.narrative || typeof params.narrative !== "string" || params.narrative.trim() === "") {
123
+ return { error: "narrative is required and must be a non-empty string" };
124
+ }
125
+ if (!params.verification || typeof params.verification !== "string" || params.verification.trim() === "") {
126
+ return { error: "verification is required and must be a non-empty string" };
127
+ }
119
128
  const artifactBasePath = resolveCanonicalMilestoneRoot(basePath, params.milestoneId);
120
129
  // ── Ownership check (opt-in: only enforced when claim file exists) ──────
121
130
  const ownershipErr = checkOwnership(artifactBasePath, taskUnitKey(params.milestoneId, params.sliceId, params.taskId), params.actorName);
@@ -238,6 +238,24 @@ export async function executeSummarySave(params, basePath = process.cwd()) {
238
238
  };
239
239
  }
240
240
  }
241
+ function normalizeVerificationEvidence(evidence) {
242
+ return (evidence ?? []).map((entry) => typeof entry === "string"
243
+ ? { command: entry, exitCode: -1, verdict: "unknown (coerced from string)", durationMs: 0 }
244
+ : entry);
245
+ }
246
+ function deriveVerificationSummary(evidence) {
247
+ if (evidence.length === 0)
248
+ return null;
249
+ const rendered = evidence.slice(0, 3).map((entry) => {
250
+ const command = entry.command.trim() || "(unspecified command)";
251
+ const verdict = entry.verdict.trim() || "recorded";
252
+ return `\`${command}\` exited ${entry.exitCode} (${verdict})`;
253
+ });
254
+ const suffix = evidence.length > rendered.length
255
+ ? `; ${evidence.length - rendered.length} more check(s) recorded`
256
+ : "";
257
+ return `Verification evidence recorded: ${rendered.join("; ")}${suffix}.`;
258
+ }
241
259
  export async function executeTaskComplete(params, basePath = process.cwd()) {
242
260
  const dbAvailable = await ensureDbOpen(basePath);
243
261
  if (!dbAvailable) {
@@ -249,7 +267,28 @@ export async function executeTaskComplete(params, basePath = process.cwd()) {
249
267
  }
250
268
  try {
251
269
  const coerced = { ...params };
252
- coerced.verificationEvidence = (params.verificationEvidence ?? []).map((v) => typeof v === "string" ? { command: v, exitCode: -1, verdict: "unknown (coerced from string)", durationMs: 0 } : v);
270
+ const verificationEvidence = normalizeVerificationEvidence(params.verificationEvidence);
271
+ coerced.verificationEvidence = verificationEvidence;
272
+ const verification = typeof params.verification === "string" ? params.verification.trim() : "";
273
+ if (verification.length === 0) {
274
+ const derived = deriveVerificationSummary(verificationEvidence);
275
+ if (derived) {
276
+ coerced.verification = derived;
277
+ }
278
+ else if (params.blockerDiscovered === true) {
279
+ coerced.verification = "Not run: blocker discovered before verification.";
280
+ }
281
+ else {
282
+ return {
283
+ content: [{
284
+ type: "text",
285
+ text: "Error completing task: verification is required unless verificationEvidence is provided or blockerDiscovered is true.",
286
+ }],
287
+ details: { operation: "complete_task", error: "verification_required" },
288
+ isError: true,
289
+ };
290
+ }
291
+ }
253
292
  const result = await handleCompleteTask(coerced, basePath);
254
293
  if ("error" in result) {
255
294
  return {
@@ -203,7 +203,7 @@ function validateMilestoneId(milestoneId) {
203
203
  * - emits worktree-created telemetry on successful entry
204
204
  * - notifies the caller via `ctx.notify` for every user-visible outcome
205
205
  */
206
- export function _enterMilestoneCore(s, deps, milestoneId, ctx) {
206
+ export function _enterMilestoneCore(s, deps, milestoneId, ctx, opts = {}) {
207
207
  if (!isValidMilestoneId(milestoneId)) {
208
208
  debugLog("WorktreeLifecycle", {
209
209
  action: "enterMilestone",
@@ -305,7 +305,7 @@ export function _enterMilestoneCore(s, deps, milestoneId, ctx) {
305
305
  // Handles the case where originalBasePath is falsy and basePath is itself
306
306
  // a worktree path — prevents double-nested worktree paths (#3729).
307
307
  const basePath = resolveWorktreeProjectRoot(s.basePath, s.originalBasePath);
308
- const mode = getIsolationMode(basePath);
308
+ const mode = opts.modeOverride ?? getIsolationMode(basePath);
309
309
  if (s.isolationDegraded) {
310
310
  if (mode === "worktree") {
311
311
  try {
@@ -841,7 +841,8 @@ export function mergeMilestoneStandalone(deps, mctx) {
841
841
  pushed: false,
842
842
  };
843
843
  }
844
- const mode = getIsolationMode(originalBasePath || worktreeBasePath);
844
+ const mode = mctx.isolationModeOverride ??
845
+ getIsolationMode(originalBasePath || worktreeBasePath);
845
846
  debugLog("WorktreeLifecycle", {
846
847
  action: "mergeAndExit",
847
848
  milestoneId,
@@ -1137,6 +1138,7 @@ export class WorktreeLifecycle {
1137
1138
  originalBasePath: this.s.originalBasePath,
1138
1139
  worktreeBasePath: this.s.basePath,
1139
1140
  milestoneId,
1141
+ isolationModeOverride: this.s.strandedRecoveryIsolationMode ?? undefined,
1140
1142
  isolationDegraded: this.s.isolationDegraded,
1141
1143
  notify: ctx.notify,
1142
1144
  });
@@ -1223,6 +1225,7 @@ export class WorktreeLifecycle {
1223
1225
  // Rebuild GitService after merge (branch HEAD changed)
1224
1226
  rebuildGitService(this.s, this.deps);
1225
1227
  }
1228
+ this.s.strandedRecoveryIsolationMode = null;
1226
1229
  return result;
1227
1230
  }
1228
1231
  // ── Removed: _mergeWorktreeMode / _mergeBranchMode bodies ────────────
@@ -1345,6 +1348,24 @@ export class WorktreeLifecycle {
1345
1348
  resumeFromPausedSession(base, persistedWorktreePath) {
1346
1349
  this.s.basePath = resolvePausedResumeBasePath(base, persistedWorktreePath);
1347
1350
  }
1351
+ /**
1352
+ * Adopt in-progress stranded work during bootstrap.
1353
+ *
1354
+ * Unlike completed-orphan recovery, this does not merge, delete, or commit.
1355
+ * It only moves the live session onto the branch/worktree proven by the
1356
+ * audit evidence, while preserving that mode for the eventual merge.
1357
+ */
1358
+ adoptStrandedMilestone(milestoneId, base, ctx, opts) {
1359
+ this.adoptSessionRoot(base);
1360
+ this.s.strandedRecoveryIsolationMode = opts.mode;
1361
+ const result = _enterMilestoneCore(this.s, this.deps, milestoneId, ctx, {
1362
+ modeOverride: opts.mode,
1363
+ });
1364
+ if (!result.ok) {
1365
+ this.s.strandedRecoveryIsolationMode = null;
1366
+ }
1367
+ return result;
1368
+ }
1348
1369
  /**
1349
1370
  * Adopt an orphan worktree for a bootstrap-time merge (ADR-016 phase 2 / B4,
1350
1371
  * issue #5622).
@@ -0,0 +1,117 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Lightweight worktree post-create hook runner.
3
+ import { execFileSync } from "node:child_process";
4
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { isAbsolute, join } from "node:path";
7
+ import { parse as parseYaml } from "yaml";
8
+ import { gsdHome } from "./gsd-home.js";
9
+ import { gsdRoot } from "./paths.js";
10
+ function readPreferencesObject(path) {
11
+ if (!existsSync(path))
12
+ return null;
13
+ const content = readFileSync(path, "utf-8");
14
+ try {
15
+ const startMarker = content.startsWith("---\r\n") ? "---\r\n" : "---\n";
16
+ if (content.startsWith(startMarker)) {
17
+ const searchStart = startMarker.length;
18
+ const endIdx = content.indexOf("\n---", searchStart);
19
+ if (endIdx === -1)
20
+ return null;
21
+ const parsed = parseYaml(content.slice(searchStart, endIdx).replace(/\r/g, ""));
22
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
23
+ ? parsed
24
+ : null;
25
+ }
26
+ const gitLines = [];
27
+ let inGitSection = false;
28
+ for (const rawLine of content.split("\n")) {
29
+ const line = rawLine.replace(/\r$/, "");
30
+ const heading = line.match(/^##\s+(.+)$/);
31
+ if (heading) {
32
+ inGitSection = heading[1].trim().toLowerCase().replace(/\s+/g, "_") === "git";
33
+ continue;
34
+ }
35
+ if (inGitSection && line.trim() && !line.trimStart().startsWith("#")) {
36
+ gitLines.push(line.replace(/^\s*-\s*/, ""));
37
+ }
38
+ }
39
+ if (gitLines.length === 0)
40
+ return null;
41
+ const parsed = parseYaml(gitLines.join("\n"));
42
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
43
+ ? { git: parsed }
44
+ : null;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ function extractHookPath(preferences) {
51
+ const git = preferences?.git;
52
+ if (!git || typeof git !== "object" || Array.isArray(git))
53
+ return null;
54
+ const hookPath = git.worktree_post_create;
55
+ return typeof hookPath === "string" && hookPath.trim() ? hookPath : null;
56
+ }
57
+ function resolveConfiguredHookPath(sourceDir) {
58
+ const paths = [
59
+ join(homedir(), ".pi", "agent", "gsd-preferences.md"),
60
+ join(gsdHome(), "preferences.md"),
61
+ join(gsdHome(), "PREFERENCES.md"),
62
+ join(gsdRoot(sourceDir), "preferences.md"),
63
+ join(gsdRoot(sourceDir), "PREFERENCES.md"),
64
+ ];
65
+ let hookPath = null;
66
+ for (const path of paths) {
67
+ hookPath = extractHookPath(readPreferencesObject(path)) ?? hookPath;
68
+ }
69
+ return hookPath;
70
+ }
71
+ /**
72
+ * Run the user-configured post-create hook script after worktree creation.
73
+ * The script receives SOURCE_DIR and WORKTREE_DIR as environment variables.
74
+ * Failure is non-fatal -- returns the error message or null on success.
75
+ *
76
+ * Reads git.worktree_post_create from effective global/project preferences
77
+ * unless hookPath is provided directly.
78
+ */
79
+ export function runWorktreePostCreateHook(sourceDir, worktreeDir, hookPath) {
80
+ if (hookPath === undefined) {
81
+ hookPath = resolveConfiguredHookPath(sourceDir) ?? undefined;
82
+ }
83
+ if (!hookPath)
84
+ return null;
85
+ let resolved = isAbsolute(hookPath) ? hookPath : join(sourceDir, hookPath);
86
+ if (!existsSync(resolved)) {
87
+ return `Worktree post-create hook not found: ${resolved}`;
88
+ }
89
+ if (process.platform === "win32") {
90
+ try {
91
+ resolved = realpathSync.native(resolved);
92
+ }
93
+ catch {
94
+ // Keep the original path; the exec error below will include the failure.
95
+ }
96
+ }
97
+ try {
98
+ const needsShell = process.platform === "win32" && /\.(bat|cmd)$/i.test(resolved);
99
+ execFileSync(resolved, [], {
100
+ cwd: worktreeDir,
101
+ env: {
102
+ ...process.env,
103
+ SOURCE_DIR: sourceDir,
104
+ WORKTREE_DIR: worktreeDir,
105
+ },
106
+ stdio: ["ignore", "pipe", "pipe"],
107
+ encoding: "utf-8",
108
+ timeout: 30_000,
109
+ shell: needsShell,
110
+ });
111
+ return null;
112
+ }
113
+ catch (err) {
114
+ const msg = err instanceof Error ? err.message : String(err);
115
+ return `Worktree post-create hook failed: ${msg}`;
116
+ }
117
+ }
@@ -12,6 +12,45 @@ export const BRAVE_TOOL_NAMES = ["search-the-web", "search_and_read"];
12
12
  export const CUSTOM_SEARCH_TOOL_NAMES = ["search-the-web", "search_and_read", "google_search"];
13
13
  /** Thinking block types that require signature validation by the API */
14
14
  const THINKING_TYPES = new Set(["thinking", "redacted_thinking"]);
15
+ const NATIVE_SERVER_TOOL_USE_TYPES = new Set([
16
+ "server_tool_use",
17
+ "serverToolUse",
18
+ ]);
19
+ const NATIVE_WEB_SEARCH_RESULT_TYPES = new Set([
20
+ "web_search_tool_result",
21
+ "webSearchResult",
22
+ ]);
23
+ function nativeServerToolId(block) {
24
+ if (!NATIVE_SERVER_TOOL_USE_TYPES.has(block?.type))
25
+ return undefined;
26
+ return typeof block.id === "string" ? block.id : undefined;
27
+ }
28
+ function nativeWebSearchResultId(block) {
29
+ if (!NATIVE_WEB_SEARCH_RESULT_TYPES.has(block?.type))
30
+ return undefined;
31
+ const id = block.type === "webSearchResult" ? block.toolUseId : block.tool_use_id;
32
+ return typeof id === "string" ? id : undefined;
33
+ }
34
+ function hasCompleteNativeServerToolReplay(content) {
35
+ const pendingToolUseIds = new Set();
36
+ let sawNativeServerToolUse = false;
37
+ for (const block of content) {
38
+ const toolUseId = nativeServerToolId(block);
39
+ if (toolUseId !== undefined) {
40
+ if (pendingToolUseIds.has(toolUseId))
41
+ return false;
42
+ sawNativeServerToolUse = true;
43
+ pendingToolUseIds.add(toolUseId);
44
+ continue;
45
+ }
46
+ const resultId = nativeWebSearchResultId(block);
47
+ if (resultId !== undefined) {
48
+ if (!pendingToolUseIds.delete(resultId))
49
+ return false;
50
+ }
51
+ }
52
+ return sawNativeServerToolUse && pendingToolUseIds.size === 0;
53
+ }
15
54
  /**
16
55
  * Providers whose Anthropic-Messages endpoint is known to accept the native
17
56
  * `web_search_20250305` server tool. Anthropic-shaped transports NOT in this
@@ -30,6 +69,10 @@ const NATIVE_WEB_SEARCH_PROVIDERS = new Set([
30
69
  "anthropic-vertex",
31
70
  "vercel-ai-gateway",
32
71
  ]);
72
+ function looksLikeAnthropicModelName(modelName) {
73
+ const normalized = modelName.trim().toLowerCase();
74
+ return normalized.startsWith("claude-") || normalized.startsWith("anthropic/claude-");
75
+ }
33
76
  /**
34
77
  * True when the model is an Anthropic-shaped transport AND the provider is
35
78
  * known to accept the native `web_search_20250305` tool. Gate both on api
@@ -74,11 +117,10 @@ export function preferBraveSearch() {
74
117
  * those blocks. The Anthropic API detects the modification and rejects the
75
118
  * request with "thinking blocks cannot be modified."
76
119
  *
77
- * Fix: Remove thinking blocks from all assistant messages in the history.
78
- * In Anthropic's Messages API, the messages array always ends with a user
79
- * message, so every assistant message is from a previous turn that has been
80
- * through a store/replay cycle. The model generates fresh thinking for the
81
- * current turn regardless.
120
+ * Fix: Remove thinking blocks only from assistant messages that do not carry
121
+ * native server-tool blocks. Complete native server-tool histories can be
122
+ * replayed as-is; stripping thinking from those messages is itself a latest
123
+ * assistant message modification.
82
124
  */
83
125
  export function stripThinkingFromHistory(messages) {
84
126
  for (const msg of messages) {
@@ -87,6 +129,9 @@ export function stripThinkingFromHistory(messages) {
87
129
  const content = msg.content;
88
130
  if (!Array.isArray(content))
89
131
  continue;
132
+ if (hasCompleteNativeServerToolReplay(content)) {
133
+ continue;
134
+ }
90
135
  msg.content = content.filter((block) => !THINKING_TYPES.has(block?.type));
91
136
  }
92
137
  }
@@ -152,6 +197,8 @@ export function registerNativeSearchHooks(pi) {
152
197
  // The model name heuristic is needed for session restores where
153
198
  // modelsAreEqual suppresses model_select AND the SDK doesn't pass model.
154
199
  const eventModel = event.model;
200
+ const payloadModelName = typeof payload.model === "string" ? payload.model : "";
201
+ const payloadLooksAnthropic = payloadModelName ? looksLikeAnthropicModelName(payloadModelName) : undefined;
155
202
  let isAnthropic;
156
203
  if (eventModel?.api || eventModel?.provider) {
157
204
  // Preferred path: gate on api shape + provider allowlist. Both fields
@@ -161,13 +208,15 @@ export function registerNativeSearchHooks(pi) {
161
208
  isAnthropic = supportsNativeWebSearch(eventModel);
162
209
  }
163
210
  else if (modelSelectFired) {
164
- isAnthropic = isAnthropicProvider;
211
+ // The model_select flag can be stale if the next request omits event.model
212
+ // after a provider switch. A concrete non-Claude payload must win so an
213
+ // Anthropic-only tool never leaks into OpenAI Responses requests.
214
+ isAnthropic = isAnthropicProvider && payloadLooksAnthropic !== false;
165
215
  }
166
216
  else {
167
217
  // Last resort: session-restore paths where the SDK doesn't pass model.
168
218
  // The model-name prefix is best-effort and assumes direct Anthropic.
169
- const modelName = typeof payload.model === "string" ? payload.model : "";
170
- isAnthropic = modelName.startsWith("claude-");
219
+ isAnthropic = payloadLooksAnthropic === true;
171
220
  }
172
221
  if (!isAnthropic)
173
222
  return;
@@ -0,0 +1,36 @@
1
+ import { homedir } from 'node:os';
2
+ import { join, resolve as resolvePath, sep } from 'node:path';
3
+ function hasPnpmPath(value) {
4
+ if (!value)
5
+ return false;
6
+ const normalized = value.replace(/\\/g, '/').toLowerCase();
7
+ return (normalized.includes('/.pnpm/') ||
8
+ normalized.endsWith('/pnpm') ||
9
+ normalized.endsWith('/pnpm.cjs') ||
10
+ normalized.endsWith('/pnpm.js'));
11
+ }
12
+ function pathStartsWith(pathValue, dir) {
13
+ if (!pathValue)
14
+ return false;
15
+ const resolvedPath = resolvePath(pathValue);
16
+ const resolvedDir = resolvePath(dir);
17
+ return resolvedPath === resolvedDir || resolvedPath.startsWith(resolvedDir + sep);
18
+ }
19
+ // Shared by update-check.ts and gsd command handlers. The JS installer keeps a
20
+ // parallel copy because it runs before TypeScript output exists.
21
+ export function isPnpmInstall(argv1 = process.argv[1], env = process.env) {
22
+ if (env.npm_config_user_agent?.startsWith('pnpm/'))
23
+ return true;
24
+ if (hasPnpmPath(env.npm_execpath))
25
+ return true;
26
+ if (hasPnpmPath(argv1))
27
+ return true;
28
+ if (!argv1)
29
+ return false;
30
+ const pnpmBinDirs = [];
31
+ if (env.PNPM_HOME)
32
+ pnpmBinDirs.push(env.PNPM_HOME);
33
+ pnpmBinDirs.push(join(homedir(), 'Library', 'pnpm'));
34
+ pnpmBinDirs.push(join(homedir(), '.local', 'share', 'pnpm'));
35
+ return pnpmBinDirs.some((dir) => pathStartsWith(argv1, dir) || pathStartsWith(env.npm_execpath, dir));
36
+ }
@@ -1,3 +1,5 @@
1
+ import { isPnpmInstall } from './resources/shared/package-manager-detection.js';
2
+ export { isPnpmInstall };
1
3
  interface UpdateCheckCache {
2
4
  lastCheck: number;
3
5
  latestVersion: string;
@@ -22,7 +24,10 @@ export declare function fetchLatestVersionFromRegistry(registryUrl?: string, fet
22
24
  * (PR #4147) misses this path. Inspect the unresolved invocation path instead.
23
25
  */
24
26
  export declare function isBunInstall(argv1?: string | undefined): boolean;
25
- export declare function resolveInstallCommand(pkg: string): string;
27
+ export declare function resolveInstallCommand(pkg: string, options?: {
28
+ argv1?: string;
29
+ env?: NodeJS.ProcessEnv;
30
+ }): string;
26
31
  export interface UpdateCheckOptions {
27
32
  currentVersion?: string;
28
33
  cachePath?: string;
@@ -45,4 +50,3 @@ export declare function checkForUpdates(options?: UpdateCheckOptions): Promise<v
45
50
  * Returns true if an update was performed, false otherwise.
46
51
  */
47
52
  export declare function checkAndPromptForUpdates(options?: UpdateCheckOptions): Promise<boolean>;
48
- export {};
@@ -1,9 +1,11 @@
1
1
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { execSync } from 'node:child_process';
2
3
  import { dirname, join, resolve as resolvePath, sep } from 'node:path';
3
4
  import { homedir } from 'node:os';
4
5
  import chalk from 'chalk';
5
6
  import { appRoot } from './app-paths.js';
6
- import { execSync } from 'node:child_process';
7
+ import { isPnpmInstall } from './resources/shared/package-manager-detection.js';
8
+ export { isPnpmInstall };
7
9
  const CACHE_FILE = join(appRoot, '.update-check');
8
10
  const NPM_PACKAGE_NAME = '@opengsd/gsd-pi';
9
11
  const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
@@ -91,9 +93,11 @@ export function isBunInstall(argv1 = process.argv[1]) {
91
93
  const resolved = resolvePath(argv1);
92
94
  return bunBinDirs.some((dir) => resolved.startsWith(resolvePath(dir) + sep));
93
95
  }
94
- export function resolveInstallCommand(pkg) {
95
- if (isBunInstall())
96
+ export function resolveInstallCommand(pkg, options = {}) {
97
+ if (isBunInstall(options.argv1))
96
98
  return `bun add -g ${pkg}`;
99
+ if (isPnpmInstall(options.argv1, options.env))
100
+ return `pnpm add -g ${pkg}`;
97
101
  return `npm install -g ${pkg}`;
98
102
  }
99
103
  function printUpdateBanner(current, latest) {
@@ -1 +1 @@
1
- -P554bKh56nzavKUmvFM2
1
+ mijI90BL1BdUcMUnhC0HU
@@ -1,27 +1,27 @@
1
1
  {
2
2
  "/_not-found/page": "/_not-found",
3
3
  "/_global-error/page": "/_global-error",
4
- "/api/bridge-terminal/resize/route": "/api/bridge-terminal/resize",
5
4
  "/api/boot/route": "/api/boot",
5
+ "/api/bridge-terminal/input/route": "/api/bridge-terminal/input",
6
+ "/api/bridge-terminal/resize/route": "/api/bridge-terminal/resize",
6
7
  "/api/bridge-terminal/stream/route": "/api/bridge-terminal/stream",
8
+ "/api/browse-directories/route": "/api/browse-directories",
9
+ "/api/cleanup/route": "/api/cleanup",
7
10
  "/api/captures/route": "/api/captures",
8
11
  "/api/dev-mode/route": "/api/dev-mode",
9
- "/api/cleanup/route": "/api/cleanup",
10
- "/api/browse-directories/route": "/api/browse-directories",
11
- "/api/bridge-terminal/input/route": "/api/bridge-terminal/input",
12
12
  "/api/doctor/route": "/api/doctor",
13
13
  "/api/export-data/route": "/api/export-data",
14
- "/api/experimental/route": "/api/experimental",
15
14
  "/api/forensics/route": "/api/forensics",
16
15
  "/api/git/route": "/api/git",
16
+ "/api/experimental/route": "/api/experimental",
17
17
  "/api/history/route": "/api/history",
18
18
  "/api/hooks/route": "/api/hooks",
19
19
  "/api/inspect/route": "/api/inspect",
20
20
  "/api/knowledge/route": "/api/knowledge",
21
21
  "/api/live-state/route": "/api/live-state",
22
- "/api/files/route": "/api/files",
23
22
  "/api/mcp-connections/route": "/api/mcp-connections",
24
23
  "/api/notifications/route": "/api/notifications",
24
+ "/api/files/route": "/api/files",
25
25
  "/api/onboarding/route": "/api/onboarding",
26
26
  "/api/preferences/route": "/api/preferences",
27
27
  "/api/recovery/route": "/api/recovery",
@@ -29,8 +29,8 @@
29
29
  "/api/session/browser/route": "/api/session/browser",
30
30
  "/api/session/command/route": "/api/session/command",
31
31
  "/api/session/events/route": "/api/session/events",
32
- "/api/settings-data/route": "/api/settings-data",
33
32
  "/api/session/manage/route": "/api/session/manage",
33
+ "/api/settings-data/route": "/api/settings-data",
34
34
  "/api/shutdown/route": "/api/shutdown",
35
35
  "/api/skill-health/route": "/api/skill-health",
36
36
  "/api/remote-questions/route": "/api/remote-questions",
@@ -4,8 +4,8 @@
4
4
  ],
5
5
  "devFiles": [],
6
6
  "lowPriorityFiles": [
7
- "static/-P554bKh56nzavKUmvFM2/_buildManifest.js",
8
- "static/-P554bKh56nzavKUmvFM2/_ssgManifest.js"
7
+ "static/mijI90BL1BdUcMUnhC0HU/_buildManifest.js",
8
+ "static/mijI90BL1BdUcMUnhC0HU/_ssgManifest.js"
9
9
  ],
10
10
  "rootMainFiles": [
11
11
  "static/chunks/webpack-41a6d3c17ba63b62.js",
@@ -78,8 +78,8 @@
78
78
  "dynamicRoutes": {},
79
79
  "notFoundRoutes": [],
80
80
  "preview": {
81
- "previewModeId": "8509b2b059764aaca9a314862c495677",
82
- "previewModeSigningKey": "111025ead699007f28945705063708efd3c1f4282efe22060d5caa54f48f8410",
83
- "previewModeEncryptionKey": "c74a2da9c40cae424b15ebea096385efbbe45b67de04619cb672f968f2d3aa1c"
81
+ "previewModeId": "b0c8967ff02bb3be0998b3c2be0d6bec",
82
+ "previewModeSigningKey": "6e4615d1d87113363594bc054198319a74821f3b9e5242aad52e96e18261e56c",
83
+ "previewModeEncryptionKey": "5658cf050af19d64a3d3d918a64d88f5d24b6eaaec39c243ab405d032f435c0b"
84
84
  }
85
85
  }