gsd-pi 2.76.0-dev.355e107a0 → 2.76.0-dev.479ad0e78

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 (92) hide show
  1. package/dist/resources/extensions/gsd/auto/loop.js +9 -0
  2. package/dist/resources/extensions/gsd/auto/phases.js +11 -2
  3. package/dist/resources/extensions/gsd/auto/run-unit.js +11 -2
  4. package/dist/resources/extensions/gsd/auto/session.js +6 -1
  5. package/dist/resources/extensions/gsd/auto-recovery.js +19 -1
  6. package/dist/resources/extensions/gsd/auto.js +11 -0
  7. package/dist/resources/extensions/gsd/gitignore.js +1 -0
  8. package/dist/resources/extensions/gsd/gsd-db.js +23 -6
  9. package/dist/resources/extensions/gsd/worktree-resolver.js +50 -10
  10. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  11. package/dist/web/standalone/.next/BUILD_ID +1 -1
  12. package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
  13. package/dist/web/standalone/.next/build-manifest.json +2 -2
  14. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  15. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/index.html +1 -1
  32. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
  39. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  40. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  41. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  42. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  43. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  44. package/package.json +1 -1
  45. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  46. package/packages/mcp-server/dist/server.js +29 -4
  47. package/packages/mcp-server/dist/server.js.map +1 -1
  48. package/packages/mcp-server/dist/session-manager.d.ts +14 -0
  49. package/packages/mcp-server/dist/session-manager.d.ts.map +1 -1
  50. package/packages/mcp-server/dist/session-manager.js +49 -1
  51. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  52. package/packages/mcp-server/src/mcp-server.test.ts +37 -0
  53. package/packages/mcp-server/src/server.ts +27 -4
  54. package/packages/mcp-server/src/session-manager.ts +43 -1
  55. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  56. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +3 -2
  57. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  58. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +2 -0
  59. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  60. package/packages/pi-coding-agent/dist/core/agent-session.js +7 -0
  61. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  62. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -0
  63. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  64. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  65. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  66. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  68. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +3 -2
  69. package/packages/pi-coding-agent/src/core/agent-session.ts +11 -0
  70. package/packages/pi-coding-agent/src/core/extensions/runner.ts +2 -0
  71. package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
  72. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  73. package/src/resources/extensions/gsd/auto/loop.ts +9 -0
  74. package/src/resources/extensions/gsd/auto/phases.ts +11 -2
  75. package/src/resources/extensions/gsd/auto/run-unit.ts +11 -2
  76. package/src/resources/extensions/gsd/auto/session.ts +6 -1
  77. package/src/resources/extensions/gsd/auto-recovery.ts +11 -1
  78. package/src/resources/extensions/gsd/auto.ts +11 -0
  79. package/src/resources/extensions/gsd/gitignore.ts +1 -1
  80. package/src/resources/extensions/gsd/gsd-db.ts +23 -6
  81. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +69 -1
  82. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +1 -0
  83. package/src/resources/extensions/gsd/tests/integration/gitignore-tracked-gsd.test.ts +1 -0
  84. package/src/resources/extensions/gsd/tests/integration/idle-recovery.test.ts +30 -0
  85. package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +12 -0
  86. package/src/resources/extensions/gsd/tests/resume-dispatch-worktree.test.ts +230 -0
  87. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +35 -0
  88. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +6 -1
  89. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +78 -5
  90. package/src/resources/extensions/gsd/worktree-resolver.ts +54 -9
  91. /package/dist/web/standalone/.next/static/{TKKbktue8R7x5oPjkT9j1 → JgU2F-5N9mTyB7kUSSk9A}/_buildManifest.js +0 -0
  92. /package/dist/web/standalone/.next/static/{TKKbktue8R7x5oPjkT9j1 → JgU2F-5N9mTyB7kUSSk9A}/_ssgManifest.js +0 -0
@@ -190,7 +190,12 @@ describe("worktree journal events", () => {
190
190
  });
191
191
  const resolver = new WorktreeResolver(s, deps);
192
192
 
193
- resolver.mergeAndExit("M001", makeNotifyCtx());
193
+ // Since #4380, mergeAndExit re-throws all errors after emitting the journal
194
+ // event and restoring state — callers must handle the throw.
195
+ assert.throws(
196
+ () => resolver.mergeAndExit("M001", makeNotifyCtx()),
197
+ /conflict in main/,
198
+ );
194
199
 
195
200
  const entries = readJournalEntries(tmp);
196
201
  const failed = entries.find(e => e.eventType === "worktree-merge-failed");
@@ -313,6 +313,43 @@ test("enterMilestone uses originalBasePath as base for worktree ops", () => {
313
313
  assert.equal(createdFrom, "/project"); // uses originalBasePath, not current basePath
314
314
  });
315
315
 
316
+ test("enterMilestone does not create double-nested worktree when originalBasePath is empty and basePath is a worktree path", () => {
317
+ // Regression test for #3729: when s.originalBasePath is "" (falsy) and
318
+ // s.basePath is already a worktree path, the expression
319
+ // `this.s.originalBasePath || this.s.basePath` evaluates to the worktree
320
+ // path. Passing that to createAutoWorktree produces a doubly-nested path
321
+ // like /project/.gsd/worktrees/M001/.gsd/worktrees/M002.
322
+ const wtPath = "/project/.gsd/worktrees/M001";
323
+ const s = makeSession({
324
+ basePath: wtPath,
325
+ originalBasePath: "/project", // will be overwritten below to simulate the bug
326
+ });
327
+ // Simulate the real bug: originalBasePath is "" (falsy) as it is when AutoSession
328
+ // is constructed fresh or reset() is called without auto-start re-setting it.
329
+ s.originalBasePath = "";
330
+
331
+ let createdFromPath = "";
332
+ const deps = makeDeps({
333
+ getAutoWorktreePath: () => null,
334
+ createAutoWorktree: (basePath: string, _mid: string) => {
335
+ createdFromPath = basePath;
336
+ return `/project/.gsd/worktrees/M002`;
337
+ },
338
+ });
339
+ const ctx = makeNotifyCtx();
340
+ const resolver = new WorktreeResolver(s, deps);
341
+
342
+ resolver.enterMilestone("M002", ctx);
343
+
344
+ // The path passed to createAutoWorktree must be the project root, NOT the
345
+ // worktree path. If it equals wtPath the worktree would be created at
346
+ // /project/.gsd/worktrees/M001/.gsd/worktrees/M002 (double-nesting).
347
+ assert.ok(
348
+ !createdFromPath.includes("/.gsd/worktrees/"),
349
+ `createAutoWorktree must be called with project root, got: "${createdFromPath}"`,
350
+ );
351
+ });
352
+
316
353
  // ─── enterMilestone Tests (branch mode) ──────────────────────────────────────
317
354
 
318
355
  test("enterMilestone in branch mode calls enterBranchModeForMilestone and rebuilds GitService", () => {
@@ -577,9 +614,11 @@ test("mergeAndExit in worktree mode restores to project root on merge failure",
577
614
  const ctx = makeNotifyCtx();
578
615
  const resolver = new WorktreeResolver(s, deps);
579
616
 
580
- resolver.mergeAndExit("M001", ctx);
617
+ // Error propagates (#4380) — callers handle recovery. restoreToProjectRoot()
618
+ // still runs before re-throw so state is consistent for the caller.
619
+ assert.throws(() => resolver.mergeAndExit("M001", ctx), /conflict in main/);
581
620
 
582
- assert.equal(s.basePath, "/project"); // error recovery — restored
621
+ assert.equal(s.basePath, "/project"); // error recovery — restored before re-throw
583
622
  assert.ok(
584
623
  ctx.messages.some(
585
624
  (m) => m.level === "warning" && m.msg.includes("conflict in main"),
@@ -607,7 +646,8 @@ test("mergeAndExit failure message tells user worktree and branch are preserved
607
646
  const ctx = makeNotifyCtx();
608
647
  const resolver = new WorktreeResolver(s, deps);
609
648
 
610
- resolver.mergeAndExit("M001", ctx);
649
+ // Error propagates (#4380) — notification is still emitted before re-throw
650
+ assert.throws(() => resolver.mergeAndExit("M001", ctx), /pathspec 'main' did not match/);
611
651
 
612
652
  const warning = ctx.messages.find((m) => m.level === "warning");
613
653
  assert.ok(warning, "a warning message is emitted");
@@ -643,7 +683,8 @@ test("mergeAndExit failure message references /gsd dispatch complete-milestone,
643
683
  const ctx = makeNotifyCtx();
644
684
  const resolver = new WorktreeResolver(s, deps);
645
685
 
646
- resolver.mergeAndExit("M001", ctx);
686
+ // Error propagates (#4380) — notification is still emitted before re-throw
687
+ assert.throws(() => resolver.mergeAndExit("M001", ctx), /dirty working tree/);
647
688
 
648
689
  const warning = ctx.messages.find((m) => m.level === "warning");
649
690
  assert.ok(warning, "a warning message is emitted");
@@ -709,7 +750,8 @@ test("mergeAndExit in branch mode handles merge failure gracefully", () => {
709
750
  const ctx = makeNotifyCtx();
710
751
  const resolver = new WorktreeResolver(s, deps);
711
752
 
712
- resolver.mergeAndExit("M001", ctx);
753
+ // Error propagates (#4380) — notification is still emitted before re-throw
754
+ assert.throws(() => resolver.mergeAndExit("M001", ctx), /branch merge conflict/);
713
755
 
714
756
  assert.ok(
715
757
  ctx.messages.some(
@@ -1069,3 +1111,34 @@ test("mergeAndExit in none mode remains a no-op when NOT in a worktree (#2625)",
1069
1111
  assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0,
1070
1112
  "must NOT merge when not in a worktree and mode is none");
1071
1113
  });
1114
+
1115
+ // ─── #4380 — Non-MergeConflictError must not be swallowed ────────────────────
1116
+
1117
+ test("mergeAndExit propagates non-MergeConflictError to caller (#4380)", () => {
1118
+ // Regression test: previously the catch block in _mergeWorktreeMode only
1119
+ // re-threw MergeConflictError. Permission errors, filesystem errors, and other
1120
+ // non-conflict failures were swallowed silently, making broken states impossible
1121
+ // to diagnose and preventing callers (phases.ts) from applying their own
1122
+ // error-recovery logic.
1123
+ const permissionError = new Error("EACCES: permission denied, open '/project/.git/SQUASH_MSG'");
1124
+ const s = makeSession({
1125
+ basePath: "/project/.gsd/worktrees/M001",
1126
+ originalBasePath: "/project",
1127
+ });
1128
+ const deps = makeDeps({
1129
+ isInAutoWorktree: () => true,
1130
+ getIsolationMode: () => "worktree",
1131
+ mergeMilestoneToMain: () => {
1132
+ throw permissionError;
1133
+ },
1134
+ });
1135
+ const ctx = makeNotifyCtx();
1136
+ const resolver = new WorktreeResolver(s, deps);
1137
+
1138
+ // The error must propagate — callers need it to apply their own recovery logic
1139
+ assert.throws(
1140
+ () => resolver.mergeAndExit("M001", ctx),
1141
+ (err: unknown) => err === permissionError,
1142
+ "non-MergeConflictError must propagate to the caller, not be swallowed",
1143
+ );
1144
+ });
@@ -79,6 +79,35 @@ export interface NotifyCtx {
79
79
  ) => void;
80
80
  }
81
81
 
82
+ // ─── Path Helpers ──────────────────────────────────────────────────────────
83
+
84
+ /**
85
+ * Worktree marker segment — present in any path produced by worktreePath().
86
+ * Used to strip the worktree suffix and recover the project root (#3729).
87
+ */
88
+ const WORKTREE_MARKER = "/.gsd/worktrees/";
89
+
90
+ /**
91
+ * Resolve the project root from session path state.
92
+ *
93
+ * Prefers `originalBasePath` (always the project root when set), but falls
94
+ * back to `basePath` when `originalBasePath` is falsy (e.g. fresh AutoSession
95
+ * with default empty string). If `basePath` itself is inside a worktree
96
+ * directory (contains `/.gsd/worktrees/`), strip that suffix to recover the
97
+ * actual project root — preventing double-nested worktree paths (#3729).
98
+ */
99
+ export function resolveProjectRoot(
100
+ originalBasePath: string,
101
+ basePath: string,
102
+ ): string {
103
+ let resolved = originalBasePath || basePath;
104
+ const markerIdx = resolved.indexOf(WORKTREE_MARKER);
105
+ if (markerIdx !== -1) {
106
+ resolved = resolved.slice(0, markerIdx);
107
+ }
108
+ return resolved;
109
+ }
110
+
82
111
  // ─── WorktreeResolver ──────────────────────────────────────────────────────
83
112
 
84
113
  export class WorktreeResolver {
@@ -99,12 +128,12 @@ export class WorktreeResolver {
99
128
 
100
129
  /** Original project root — always the non-worktree path. */
101
130
  get projectRoot(): string {
102
- return this.s.originalBasePath || this.s.basePath;
131
+ return resolveProjectRoot(this.s.originalBasePath, this.s.basePath);
103
132
  }
104
133
 
105
134
  /** Path for auto.lock file — same as the old lockBase(). */
106
135
  get lockPath(): string {
107
- return this.s.originalBasePath || this.s.basePath;
136
+ return resolveProjectRoot(this.s.originalBasePath, this.s.basePath);
108
137
  }
109
138
 
110
139
  // ── Private Helpers ────────────────────────────────────────────────────
@@ -182,7 +211,10 @@ export class WorktreeResolver {
182
211
  return;
183
212
  }
184
213
 
185
- const basePath = this.s.originalBasePath || this.s.basePath;
214
+ // Resolve the project root for worktree operations via shared helper.
215
+ // Handles the case where originalBasePath is falsy and basePath is itself
216
+ // a worktree path — prevents double-nested worktree paths (#3729).
217
+ const basePath = resolveProjectRoot(this.s.originalBasePath, this.s.basePath);
186
218
  debugLog("WorktreeResolver", {
187
219
  action: "enterMilestone",
188
220
  milestoneId,
@@ -569,11 +601,13 @@ export class WorktreeResolver {
569
601
  }
570
602
  }
571
603
 
572
- // Re-throw MergeConflictError so the auto loop can detect real code
573
- // conflicts and stop instead of retrying forever (#2330).
574
- if (err instanceof MergeConflictError) {
575
- throw err;
576
- }
604
+ // Restore state before re-throwing so callers always get a consistent
605
+ // session (#4380).
606
+ this.restoreToProjectRoot();
607
+ // Re-throw: MergeConflictError stops the auto loop (#2330); non-conflict
608
+ // errors (permission denied, filesystem failures) must also propagate so
609
+ // broken states are diagnosable (#4380).
610
+ throw err;
577
611
  }
578
612
 
579
613
  // Always restore basePath and rebuild — whether merge succeeded or failed
@@ -659,6 +693,8 @@ export class WorktreeResolver {
659
693
  error: msg,
660
694
  });
661
695
  ctx.notify(`Milestone merge failed (branch mode): ${msg}`, "warning");
696
+ // Re-throw all errors so callers can apply their own recovery logic (#4380).
697
+ throw err;
662
698
  }
663
699
  }
664
700
 
@@ -681,7 +717,16 @@ export class WorktreeResolver {
681
717
  currentMilestoneId,
682
718
  nextMilestoneId,
683
719
  });
684
- this.mergeAndExit(currentMilestoneId, ctx);
720
+ try {
721
+ this.mergeAndExit(currentMilestoneId, ctx);
722
+ } catch (err) {
723
+ // mergeAndExit emits a warning and restores state when it fails during
724
+ // merge/cleanup. But if it throws before recovery runs (e.g., in
725
+ // validateMilestoneId or emitJournalEvent), basePath won't be restored
726
+ // to projectRoot — re-throw so we don't enter the next milestone with
727
+ // the current one unmerged.
728
+ if (this.s.basePath !== this.projectRoot) throw err;
729
+ }
685
730
  this.enterMilestone(nextMilestoneId, ctx);
686
731
  }
687
732
  }