@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
@@ -48,6 +48,7 @@ import {
48
48
  nativeBranchDelete,
49
49
  nativeWorktreeRemove,
50
50
  nativeCommitCountBetween,
51
+ nativeHasChanges,
51
52
  } from "./native-git-bridge.js";
52
53
  import { GitServiceImpl } from "./git-service.js";
53
54
  import {
@@ -242,24 +243,126 @@ export function resolveSurvivorRecoveryIsolationMode(
242
243
  return isolationMode;
243
244
  }
244
245
 
246
+ export type StrandedWorkRecoveryMode = "worktree" | "branch";
247
+
248
+ export type OrphanAuditActionKind =
249
+ | "in-progress-stranded-work"
250
+ | "complete-merged-branch"
251
+ | "complete-merged-worktree"
252
+ | "complete-unmerged-branch"
253
+ | "complete-branchless-worktree";
254
+
255
+ export interface OrphanAuditAction {
256
+ kind: OrphanAuditActionKind;
257
+ milestoneId: string;
258
+ message: string;
259
+ severity: "info" | "warning";
260
+ branch?: string;
261
+ commitsAhead?: number;
262
+ dirtyWorktree?: boolean;
263
+ worktreeDirExists?: boolean;
264
+ recoveryMode?: StrandedWorkRecoveryMode;
265
+ blocksAuto: boolean;
266
+ }
267
+
268
+ export interface OrphanAuditResult {
269
+ recovered: string[];
270
+ warnings: string[];
271
+ actions: OrphanAuditAction[];
272
+ blockingStrandedWork: OrphanAuditAction | null;
273
+ }
274
+
275
+ function isBlockingStrandedWorkAction(action: OrphanAuditAction): boolean {
276
+ return action.kind === "in-progress-stranded-work" && action.blocksAuto;
277
+ }
278
+
279
+ function detectWorktreeEvidence(
280
+ basePath: string,
281
+ milestoneId: string,
282
+ hasChanges: typeof nativeHasChanges,
283
+ ): { path: string | null; dirExists: boolean; dirty: boolean } {
284
+ const wtDir = getWorktreeDir(basePath, milestoneId);
285
+ const wtPath = getAutoWorktreePath(basePath, milestoneId);
286
+ let dirty = false;
287
+ if (wtPath) {
288
+ try {
289
+ dirty = hasChanges(wtPath);
290
+ } catch {
291
+ dirty = false;
292
+ }
293
+ }
294
+ return {
295
+ path: wtPath,
296
+ dirExists: existsSync(wtDir),
297
+ dirty,
298
+ };
299
+ }
300
+
301
+ function strandedWorkMessage(args: {
302
+ milestoneId: string;
303
+ branch?: string;
304
+ commitsAhead: number;
305
+ mainBranch: string;
306
+ dirtyWorktree: boolean;
307
+ worktreeDirExists: boolean;
308
+ recoveryMode: StrandedWorkRecoveryMode;
309
+ }): string {
310
+ const evidence: string[] = [];
311
+ if (args.branch && args.commitsAhead > 0) {
312
+ evidence.push(
313
+ `branch ${args.branch} has ${args.commitsAhead} commit(s) ahead of ${args.mainBranch}`,
314
+ );
315
+ }
316
+ if (args.dirtyWorktree) {
317
+ evidence.push("the worktree has uncommitted changes");
318
+ }
319
+ if (evidence.length === 0) {
320
+ evidence.push("physical git evidence exists");
321
+ }
322
+
323
+ const wtSuffix = args.worktreeDirExists
324
+ ? ` Worktree directory at .gsd/worktrees/${args.milestoneId}/ holds live work.`
325
+ : "";
326
+ const recovery = args.recoveryMode === "worktree"
327
+ ? "Recovering will adopt the existing worktree."
328
+ : "Recovering will adopt the milestone branch.";
329
+
330
+ return (
331
+ `Stranded work for in-progress milestone ${args.milestoneId}: ${evidence.join("; ")}.` +
332
+ wtSuffix +
333
+ ` ${recovery} Park or discard explicitly if abandoning.`
334
+ );
335
+ }
336
+
245
337
  export function auditOrphanedMilestoneBranches(
246
338
  basePath: string,
247
- isolationMode: "worktree" | "branch" | "none",
339
+ _isolationMode: "worktree" | "branch" | "none",
248
340
  gitDeps: {
249
341
  branchList?: typeof nativeBranchList;
250
342
  branchExists?: typeof nativeBranchExists;
343
+ hasChanges?: typeof nativeHasChanges;
251
344
  } = {},
252
- ): { recovered: string[]; warnings: string[] } {
345
+ ): OrphanAuditResult {
253
346
  const recovered: string[] = [];
254
347
  const warnings: string[] = [];
348
+ const actions: OrphanAuditAction[] = [];
255
349
  const branchList = gitDeps.branchList ?? nativeBranchList;
256
350
  const branchExists = gitDeps.branchExists ?? nativeBranchExists;
351
+ const hasChanges = gitDeps.hasChanges ?? nativeHasChanges;
257
352
 
258
- // Skip in none mode no milestone branches are created
259
- if (isolationMode === "none") return { recovered, warnings };
353
+ const pushAction = (action: OrphanAuditAction): void => {
354
+ actions.push(action);
355
+ if (action.severity === "info") {
356
+ recovered.push(action.message);
357
+ } else {
358
+ warnings.push(action.message);
359
+ }
360
+ };
260
361
 
261
362
  // Skip if DB not available — can't determine completion status
262
- if (!isDbAvailable()) return { recovered, warnings };
363
+ if (!isDbAvailable()) {
364
+ return { recovered, warnings, actions, blockingStrandedWork: null };
365
+ }
263
366
 
264
367
  let milestoneBranches: string[];
265
368
  let milestoneBranchListAvailable = true;
@@ -295,6 +398,7 @@ export function auditOrphanedMilestoneBranches(
295
398
  if (!milestone) continue;
296
399
 
297
400
  const isMerged = mergedBranches.has(branch);
401
+ const worktreeEvidence = detectWorktreeEvidence(basePath, milestoneId, hasChanges);
298
402
 
299
403
  // #4762 — in-progress milestone branch with unmerged commits ahead of
300
404
  // main. This is the pre-completion orphan case: auto-mode exited without
@@ -307,33 +411,45 @@ export function auditOrphanedMilestoneBranches(
307
411
  // Parked/other closed statuses go through the legacy complete/unmerged
308
412
  // path below where appropriate.
309
413
  if (!isClosedStatus(milestone.status)) {
310
- if (isMerged) continue; // nothing to recover
311
414
  let commitsAhead = 0;
312
415
  try {
313
416
  commitsAhead = nativeCommitCountBetween(basePath, mainBranch, branch);
314
417
  } catch {
315
- // Rev-walk failure — skip rather than noise
316
- continue;
418
+ commitsAhead = 0;
317
419
  }
318
- if (commitsAhead === 0) continue;
319
-
320
- const wtDir = getWorktreeDir(basePath, milestoneId);
321
- const wtDirExists = existsSync(wtDir);
322
- const wtSuffix = wtDirExists
323
- ? ` Worktree directory at .gsd/worktrees/${milestoneId}/ holds the live work.`
324
- : "";
325
- warnings.push(
326
- `Branch ${branch} has ${commitsAhead} commit(s) ahead of ${mainBranch} for in-progress milestone ${milestoneId}.` +
327
- wtSuffix +
328
- ` Run \`/gsd auto\` to resume, or merge manually if abandoning.`,
329
- );
420
+ if ((isMerged || commitsAhead === 0) && !worktreeEvidence.dirty) continue;
421
+
422
+ const recoveryMode: StrandedWorkRecoveryMode = worktreeEvidence.path
423
+ ? "worktree"
424
+ : "branch";
425
+ const message = strandedWorkMessage({
426
+ milestoneId,
427
+ branch,
428
+ commitsAhead,
429
+ mainBranch,
430
+ dirtyWorktree: worktreeEvidence.dirty,
431
+ worktreeDirExists: worktreeEvidence.dirExists,
432
+ recoveryMode,
433
+ });
434
+ pushAction({
435
+ kind: "in-progress-stranded-work",
436
+ milestoneId,
437
+ branch,
438
+ commitsAhead,
439
+ dirtyWorktree: worktreeEvidence.dirty,
440
+ worktreeDirExists: worktreeEvidence.dirExists,
441
+ recoveryMode,
442
+ message,
443
+ severity: "warning",
444
+ blocksAuto: true,
445
+ });
330
446
 
331
447
  // #4764 telemetry
332
448
  try {
333
449
  emitWorktreeOrphaned(basePath, milestoneId, {
334
450
  reason: "in-progress-unmerged",
335
451
  commitsAhead,
336
- worktreeDirExists: wtDirExists,
452
+ worktreeDirExists: worktreeEvidence.dirExists,
337
453
  });
338
454
  } catch (err) {
339
455
  logWarning("engine", `worktree-orphaned telemetry failed for ${milestoneId}: ${err instanceof Error ? err.message : String(err)}`);
@@ -351,7 +467,14 @@ export function auditOrphanedMilestoneBranches(
351
467
  // Branch is merged — safe to delete branch and clean up worktree dir
352
468
  try {
353
469
  nativeBranchDelete(basePath, branch, true);
354
- recovered.push(`Deleted merged branch ${branch} for completed milestone ${milestoneId}.`);
470
+ pushAction({
471
+ kind: "complete-merged-branch",
472
+ milestoneId,
473
+ branch,
474
+ message: `Deleted merged branch ${branch} for completed milestone ${milestoneId}.`,
475
+ severity: "info",
476
+ blocksAuto: false,
477
+ });
355
478
  } catch (err) {
356
479
  warnings.push(`Failed to delete merged branch ${branch}: ${err instanceof Error ? err.message : String(err)}`);
357
480
  }
@@ -374,7 +497,15 @@ export function auditOrphanedMilestoneBranches(
374
497
  if (isInsideWorktreesDir(basePath, wtDir)) {
375
498
  try {
376
499
  rmSync(wtDir, { recursive: true, force: true });
377
- recovered.push(`Removed orphaned worktree directory for ${milestoneId}.`);
500
+ pushAction({
501
+ kind: "complete-merged-worktree",
502
+ milestoneId,
503
+ branch,
504
+ worktreeDirExists: true,
505
+ message: `Removed orphaned worktree directory for ${milestoneId}.`,
506
+ severity: "info",
507
+ blocksAuto: false,
508
+ });
378
509
  } catch (err2) {
379
510
  warnings.push(`Failed to remove worktree directory for ${milestoneId}: ${err2 instanceof Error ? err2.message : String(err2)}`);
380
511
  }
@@ -382,15 +513,30 @@ export function auditOrphanedMilestoneBranches(
382
513
  warnings.push(`Orphaned worktree directory for ${milestoneId} is outside .gsd/worktrees/ — skipping removal for safety.`);
383
514
  }
384
515
  } else {
385
- recovered.push(`Removed orphaned worktree directory for ${milestoneId}.`);
516
+ pushAction({
517
+ kind: "complete-merged-worktree",
518
+ milestoneId,
519
+ branch,
520
+ worktreeDirExists: true,
521
+ message: `Removed orphaned worktree directory for ${milestoneId}.`,
522
+ severity: "info",
523
+ blocksAuto: false,
524
+ });
386
525
  }
387
526
  }
388
527
  } else {
389
528
  // Branch is NOT merged — preserve for safety, warn the user
390
- warnings.push(
391
- `Branch ${branch} exists for completed milestone ${milestoneId} but is NOT merged into ${mainBranch}. ` +
392
- `This may contain unmerged work. Merge manually or run \`/gsd doctor fix\` to resolve.`,
393
- );
529
+ pushAction({
530
+ kind: "complete-unmerged-branch",
531
+ milestoneId,
532
+ branch,
533
+ worktreeDirExists: worktreeEvidence.dirExists,
534
+ message:
535
+ `Branch ${branch} exists for completed milestone ${milestoneId} but is NOT merged into ${mainBranch}. ` +
536
+ `This may contain unmerged work. Merge manually or run \`/gsd doctor fix\` to resolve.`,
537
+ severity: "warning",
538
+ blocksAuto: false,
539
+ });
394
540
 
395
541
  // #4764 telemetry
396
542
  try {
@@ -428,6 +574,41 @@ export function auditOrphanedMilestoneBranches(
428
574
  completedMilestones = [];
429
575
  }
430
576
  for (const m of completedMilestones) {
577
+ if (!isClosedStatus(m.status)) {
578
+ if (seenMilestoneIds.has(m.id)) continue;
579
+ const worktreeEvidence = detectWorktreeEvidence(basePath, m.id, hasChanges);
580
+ if (!worktreeEvidence.dirty) continue;
581
+ const message = strandedWorkMessage({
582
+ milestoneId: m.id,
583
+ commitsAhead: 0,
584
+ mainBranch,
585
+ dirtyWorktree: true,
586
+ worktreeDirExists: worktreeEvidence.dirExists,
587
+ recoveryMode: "worktree",
588
+ });
589
+ pushAction({
590
+ kind: "in-progress-stranded-work",
591
+ milestoneId: m.id,
592
+ commitsAhead: 0,
593
+ dirtyWorktree: true,
594
+ worktreeDirExists: worktreeEvidence.dirExists,
595
+ recoveryMode: "worktree",
596
+ message,
597
+ severity: "warning",
598
+ blocksAuto: true,
599
+ });
600
+ try {
601
+ emitWorktreeOrphaned(basePath, m.id, {
602
+ reason: "in-progress-unmerged",
603
+ commitsAhead: 0,
604
+ worktreeDirExists: worktreeEvidence.dirExists,
605
+ });
606
+ } catch (err) {
607
+ logWarning("engine", `worktree-orphaned telemetry failed for ${m.id}: ${err instanceof Error ? err.message : String(err)}`);
608
+ }
609
+ continue;
610
+ }
611
+
431
612
  if (m.status !== "complete") continue;
432
613
  if (seenMilestoneIds.has(m.id)) continue; // already processed in the branch loop
433
614
  if (!milestoneBranchListAvailable) {
@@ -461,18 +642,37 @@ export function auditOrphanedMilestoneBranches(
461
642
  if (existsSync(wtDir)) {
462
643
  try {
463
644
  rmSync(wtDir, { recursive: true, force: true });
464
- recovered.push(`Removed orphaned worktree directory for ${m.id} (branch already deleted).`);
645
+ pushAction({
646
+ kind: "complete-branchless-worktree",
647
+ milestoneId: m.id,
648
+ worktreeDirExists: true,
649
+ message: `Removed orphaned worktree directory for ${m.id} (branch already deleted).`,
650
+ severity: "info",
651
+ blocksAuto: false,
652
+ });
465
653
  } catch (err) {
466
654
  warnings.push(
467
655
  `Failed to remove orphaned worktree directory for ${m.id}: ${err instanceof Error ? err.message : String(err)}`,
468
656
  );
469
657
  }
470
658
  } else {
471
- recovered.push(`Removed orphaned worktree directory for ${m.id} (branch already deleted).`);
659
+ pushAction({
660
+ kind: "complete-branchless-worktree",
661
+ milestoneId: m.id,
662
+ worktreeDirExists: true,
663
+ message: `Removed orphaned worktree directory for ${m.id} (branch already deleted).`,
664
+ severity: "info",
665
+ blocksAuto: false,
666
+ });
472
667
  }
473
668
  }
474
669
 
475
- return { recovered, warnings };
670
+ return {
671
+ recovered,
672
+ warnings,
673
+ actions,
674
+ blockingStrandedWork: actions.find(isBlockingStrandedWorkAction) ?? null,
675
+ };
476
676
  }
477
677
 
478
678
  /**
@@ -894,17 +1094,27 @@ export async function bootstrapAutoSession(
894
1094
  // was lost due to session ending between completion and teardown.
895
1095
  // Must run after DB open and before worktree entry.
896
1096
  let orphanAuditRecovered = false;
1097
+ let strandedRecoveryActions: OrphanAuditAction[] = [];
1098
+ let strandedRecoveryAction: OrphanAuditAction | null = null;
897
1099
  try {
898
1100
  const auditResult = auditOrphanedMilestoneBranches(base, getIsolationMode(base));
1101
+ strandedRecoveryActions = auditResult.actions.filter(isBlockingStrandedWorkAction);
1102
+ strandedRecoveryAction = strandedRecoveryActions[0] ?? null;
899
1103
  for (const msg of auditResult.recovered) {
900
1104
  ctx.ui.notify(`Orphan audit: ${msg}`, "info");
901
1105
  }
902
1106
  for (const msg of auditResult.warnings) {
903
- ctx.ui.notify(`Orphan audit: ${msg}`, "warning");
1107
+ const prefix = msg.startsWith("Stranded work") ? "" : "Orphan audit: ";
1108
+ ctx.ui.notify(`${prefix}${msg}`, "warning");
904
1109
  }
905
1110
  if (auditResult.recovered.length > 0) {
906
1111
  orphanAuditRecovered = true;
907
- debugLog("orphan-audit", { recovered: auditResult.recovered, warnings: auditResult.warnings });
1112
+ debugLog("orphan-audit", {
1113
+ recovered: auditResult.recovered,
1114
+ warnings: auditResult.warnings,
1115
+ strandedRecoveryAction,
1116
+ strandedRecoveryActions,
1117
+ });
908
1118
  }
909
1119
  } catch (err) {
910
1120
  // Non-fatal — the audit is defensive, never block bootstrap
@@ -946,6 +1156,46 @@ export async function bootstrapAutoSession(
946
1156
 
947
1157
  let state = await deriveState(base);
948
1158
 
1159
+ // Stale worktree state recovery (#654)
1160
+ if (
1161
+ state.activeMilestone &&
1162
+ shouldUseWorktreeIsolation(base) &&
1163
+ !detectWorktreeName(base)
1164
+ ) {
1165
+ const wtPath = getAutoWorktreePath(base, state.activeMilestone.id);
1166
+ if (wtPath) {
1167
+ state = await deriveState(wtPath);
1168
+ }
1169
+ }
1170
+
1171
+ const blockingStrandedRecoveryAction = state.activeMilestone
1172
+ ? strandedRecoveryActions.find(
1173
+ (action) => action.milestoneId !== state.activeMilestone?.id,
1174
+ ) ?? strandedRecoveryAction
1175
+ : strandedRecoveryAction;
1176
+
1177
+ if (blockingStrandedRecoveryAction) {
1178
+ if (!state.activeMilestone) {
1179
+ ctx.ui.notify(
1180
+ `Stranded work for ${blockingStrandedRecoveryAction.milestoneId} blocks auto-mode, but that milestone is not active in project state. Park or discard it explicitly before continuing.`,
1181
+ "error",
1182
+ );
1183
+ return releaseLockAndReturn();
1184
+ }
1185
+ if (state.activeMilestone.id !== blockingStrandedRecoveryAction.milestoneId) {
1186
+ ctx.ui.notify(
1187
+ `Stranded work for ${blockingStrandedRecoveryAction.milestoneId} blocks auto-mode before ${state.activeMilestone.id}. Recover, park, or discard ${blockingStrandedRecoveryAction.milestoneId} explicitly before continuing.`,
1188
+ "error",
1189
+ );
1190
+ return releaseLockAndReturn();
1191
+ }
1192
+ strandedRecoveryAction = blockingStrandedRecoveryAction;
1193
+ ctx.ui.notify(
1194
+ `Recovering stranded work for ${strandedRecoveryAction.milestoneId} before dispatching new units.`,
1195
+ "info",
1196
+ );
1197
+ }
1198
+
949
1199
  if (
950
1200
  process.env.GSD_HEADLESS === "1" &&
951
1201
  orphanAuditRecovered &&
@@ -959,18 +1209,6 @@ export async function bootstrapAutoSession(
959
1209
  return releaseLockAndReturn();
960
1210
  }
961
1211
 
962
- // Stale worktree state recovery (#654)
963
- if (
964
- state.activeMilestone &&
965
- shouldUseWorktreeIsolation(base) &&
966
- !detectWorktreeName(base)
967
- ) {
968
- const wtPath = getAutoWorktreePath(base, state.activeMilestone.id);
969
- if (wtPath) {
970
- state = await deriveState(wtPath);
971
- }
972
- }
973
-
974
1212
  // Milestone branch recovery (#601, #2358)
975
1213
  // Detect survivor milestone branches in both pre-planning and complete phases.
976
1214
  // In phase=complete, the milestone artifacts exist but finalization (merge,
@@ -1005,7 +1243,10 @@ export async function bootstrapAutoSession(
1005
1243
  // The worktree/branch was created but the milestone only has CONTEXT-DRAFT.md.
1006
1244
  // Route to the interactive discussion handler instead of falling through to
1007
1245
  // auto-mode, which would immediately stop with "needs discussion".
1008
- if (decideSurvivorAction(hasSurvivorBranch, state.phase) === "discuss") {
1246
+ if (
1247
+ !strandedRecoveryAction &&
1248
+ decideSurvivorAction(hasSurvivorBranch, state.phase) === "discuss"
1249
+ ) {
1009
1250
  const { showSmartEntry } = await import("./guided-flow.js");
1010
1251
  await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
1011
1252
 
@@ -1101,7 +1342,7 @@ export async function bootstrapAutoSession(
1101
1342
  { hasSurvivorBranch },
1102
1343
  );
1103
1344
 
1104
- if (deepProjectStagePending) {
1345
+ if (deepProjectStagePending && !strandedRecoveryAction) {
1105
1346
  // Deep project-level setup runs before the first milestone exists. Let
1106
1347
  // the auto loop dispatch workflow-preferences / project / requirements
1107
1348
  // units instead of recursing back through showSmartEntry while this
@@ -1109,7 +1350,7 @@ export async function bootstrapAutoSession(
1109
1350
  s.currentMilestoneId = null;
1110
1351
  }
1111
1352
 
1112
- if (!hasSurvivorBranch && !deepProjectStagePending) {
1353
+ if (!hasSurvivorBranch && !deepProjectStagePending && !strandedRecoveryAction) {
1113
1354
  // No active work — start a new milestone via discuss flow
1114
1355
  if (!state.activeMilestone || state.phase === "complete") {
1115
1356
  // Guard against recursive dialog loop (#1348):
@@ -1185,7 +1426,7 @@ export async function bootstrapAutoSession(
1185
1426
  }
1186
1427
 
1187
1428
  // Unreachable safety check
1188
- if (!state.activeMilestone && !deepProjectStagePending) {
1429
+ if (!state.activeMilestone && !deepProjectStagePending && !strandedRecoveryAction) {
1189
1430
  const { showSmartEntry } = await import("./guided-flow.js");
1190
1431
  await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
1191
1432
  return releaseLockAndReturn();
@@ -1222,7 +1463,9 @@ export async function bootstrapAutoSession(
1222
1463
  s.resourceVersionOnStart = readResourceVersion();
1223
1464
  s.pendingQuickTasks = [];
1224
1465
  s.currentUnit = null;
1225
- s.currentMilestoneId ??= deepProjectStagePending ? null : state.activeMilestone?.id ?? null;
1466
+ s.currentMilestoneId ??=
1467
+ strandedRecoveryAction?.milestoneId ??
1468
+ (deepProjectStagePending ? null : state.activeMilestone?.id ?? null);
1226
1469
  s.originalModelId = startModelSnapshot?.id ?? ctx.model?.id ?? null;
1227
1470
  s.originalModelProvider = startModelSnapshot?.provider ?? ctx.model?.provider ?? null;
1228
1471
  s.originalThinkingLevel = startThinkingSnapshot ?? null;
@@ -1232,7 +1475,7 @@ export async function bootstrapAutoSession(
1232
1475
 
1233
1476
  // Capture integration branch
1234
1477
  if (s.currentMilestoneId) {
1235
- if (getIsolationMode(base) !== "none") {
1478
+ if (getIsolationMode(base) !== "none" || strandedRecoveryAction) {
1236
1479
  captureIntegrationBranch(base, s.currentMilestoneId);
1237
1480
  }
1238
1481
  setActiveMilestoneId(base, s.currentMilestoneId);
@@ -1243,7 +1486,7 @@ export async function bootstrapAutoSession(
1243
1486
  // milestone/<MID>. Auto-checkout back to the integration branch.
1244
1487
  const isolationMode = getIsolationMode(base);
1245
1488
  const isRepo = nativeIsRepo(base);
1246
- if (isolationMode === "none" && isRepo) {
1489
+ if (isolationMode === "none" && isRepo && !strandedRecoveryAction) {
1247
1490
  try {
1248
1491
  const currentBranch = nativeGetCurrentBranch(base);
1249
1492
  const integrationBranch = nativeDetectMainBranch(base);
@@ -1282,13 +1525,21 @@ export async function bootstrapAutoSession(
1282
1525
 
1283
1526
  if (
1284
1527
  s.currentMilestoneId &&
1285
- getIsolationMode(base) !== "none" &&
1528
+ (getIsolationMode(base) !== "none" || strandedRecoveryAction?.recoveryMode) &&
1286
1529
  !detectWorktreeName(base) &&
1287
1530
  !isUnderGsdWorktrees(base)
1288
1531
  ) {
1289
- const enterResult = buildLifecycle().enterMilestone(s.currentMilestoneId, {
1290
- notify: ctx.ui.notify.bind(ctx.ui),
1291
- });
1532
+ const lifecycle = buildLifecycle();
1533
+ const enterResult = strandedRecoveryAction?.recoveryMode
1534
+ ? lifecycle.adoptStrandedMilestone(
1535
+ s.currentMilestoneId,
1536
+ base,
1537
+ { notify: ctx.ui.notify.bind(ctx.ui) },
1538
+ { mode: strandedRecoveryAction.recoveryMode },
1539
+ )
1540
+ : lifecycle.enterMilestone(s.currentMilestoneId, {
1541
+ notify: ctx.ui.notify.bind(ctx.ui),
1542
+ });
1292
1543
  if (!enterResult.ok) {
1293
1544
  s.active = false;
1294
1545
  if (enterResult.reason === "lease-conflict") {
@@ -59,6 +59,7 @@ import { debugLog } from "./debug-logger.js";
59
59
  import { logWarning, logError } from "./workflow-logger.js";
60
60
  import { loadEffectiveGSDPreferences } from "./preferences.js";
61
61
  import { MILESTONE_ID_RE } from "./milestone-ids.js";
62
+ import { runWorktreePostCreateHook } from "./worktree-post-create-hook.js";
62
63
  import {
63
64
  nativeGetCurrentBranch,
64
65
  nativeDetectMainBranch,
@@ -912,62 +913,7 @@ export function syncWorktreeStateBack(
912
913
  ): { synced: string[] } {
913
914
  return _finalizeProjectionForMergeImpl(mainBasePath, worktreePath, milestoneId);
914
915
  }
915
- // ─── Worktree Post-Create Hook (#597) ────────────────────────────────────────
916
-
917
- /**
918
- * Run the user-configured post-create hook script after worktree creation.
919
- * The script receives SOURCE_DIR and WORKTREE_DIR as environment variables.
920
- * Failure is non-fatal — returns the error message or null on success.
921
- *
922
- * Reads the hook path from git.worktree_post_create in preferences.
923
- * Pass hookPath directly to bypass preference loading (useful for testing).
924
- */
925
- export function runWorktreePostCreateHook(
926
- sourceDir: string,
927
- worktreeDir: string,
928
- hookPath?: string,
929
- ): string | null {
930
- if (hookPath === undefined) {
931
- const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
932
- hookPath = prefs?.worktree_post_create;
933
- }
934
- if (!hookPath) return null;
935
-
936
- // Resolve relative paths against the source project root.
937
- // On Windows, convert 8.3 short paths (e.g. RUNNER~1) to long paths
938
- // so execFileSync can locate the file correctly.
939
- let resolved = isAbsolute(hookPath) ? hookPath : join(sourceDir, hookPath);
940
- if (!existsSync(resolved)) {
941
- return `Worktree post-create hook not found: ${resolved}`;
942
- }
943
- if (process.platform === "win32") {
944
- try { resolved = realpathSync.native(resolved); } catch (err) { /* keep original */
945
- logWarning("worktree", `realpath failed: ${err instanceof Error ? err.message : String(err)}`);
946
- }
947
- }
948
-
949
- try {
950
- // .bat/.cmd files on Windows require shell mode — execFileSync cannot
951
- // spawn them directly (EINVAL).
952
- const needsShell = process.platform === "win32" && /\.(bat|cmd)$/i.test(resolved);
953
- execFileSync(resolved, [], {
954
- cwd: worktreeDir,
955
- env: {
956
- ...process.env,
957
- SOURCE_DIR: sourceDir,
958
- WORKTREE_DIR: worktreeDir,
959
- },
960
- stdio: ["ignore", "pipe", "pipe"],
961
- encoding: "utf-8",
962
- timeout: 30_000, // 30 second timeout
963
- shell: needsShell,
964
- });
965
- return null;
966
- } catch (err) {
967
- const msg = err instanceof Error ? err.message : String(err);
968
- return `Worktree post-create hook failed: ${msg}`;
969
- }
970
- }
916
+ export { runWorktreePostCreateHook } from "./worktree-post-create-hook.js";
971
917
 
972
918
  // ─── Auto-Worktree Branch Naming ───────────────────────────────────────────
973
919
 
@@ -715,8 +715,9 @@ export function registerDbTools(pi: ExtensionAPI): void {
715
715
  promptSnippet: "Complete a GSD task (DB write + summary render + checkbox toggle)",
716
716
  promptGuidelines: [
717
717
  "Use gsd_task_complete (or gsd_complete_task) when a task is finished and needs to be recorded.",
718
- "All string fields are required. verificationEvidence is an array of objects with command, exitCode, verdict, durationMs.",
719
- "The tool validates required fields and returns an error message if any are missing.",
718
+ "Include verification whenever possible. If verification is omitted, the executor derives it from verificationEvidence when possible.",
719
+ "verificationEvidence is an array of objects with command, exitCode, verdict, durationMs.",
720
+ "The tool validates required fields and returns an error message if verification cannot be derived.",
720
721
  "On success, returns the summaryPath where the SUMMARY.md was written.",
721
722
  "Idempotent — calling with the same params twice will upsert (INSERT OR REPLACE) without error.",
722
723
  ],
@@ -727,7 +728,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
727
728
  milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }),
728
729
  oneLiner: Type.String({ description: "One-line summary of what was accomplished" }),
729
730
  narrative: Type.String({ description: "Detailed narrative of what happened during the task" }),
730
- verification: Type.String({ description: "What was verified and how — commands run, tests passed, behavior confirmed" }),
731
+ verification: Type.Optional(Type.String({ description: "What was verified and how — commands run, tests passed, behavior confirmed. If omitted, derived from verificationEvidence when possible." })),
731
732
  // ── Enrichment metadata (optional — defaults to empty) ────────────
732
733
  deviations: Type.Optional(Type.String({ description: "Deviations from the task plan, or 'None.'" })),
733
734
  knownIssues: Type.Optional(Type.String({ description: "Known issues discovered but not fixed, or 'None.'" })),