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.
- package/dist/resources/extensions/gsd/auto/loop.js +9 -0
- package/dist/resources/extensions/gsd/auto/phases.js +11 -2
- package/dist/resources/extensions/gsd/auto/run-unit.js +11 -2
- package/dist/resources/extensions/gsd/auto/session.js +6 -1
- package/dist/resources/extensions/gsd/auto-recovery.js +19 -1
- package/dist/resources/extensions/gsd/auto.js +11 -0
- package/dist/resources/extensions/gsd/gitignore.js +1 -0
- package/dist/resources/extensions/gsd/gsd-db.js +23 -6
- package/dist/resources/extensions/gsd/worktree-resolver.js +50 -10
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +29 -4
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/session-manager.d.ts +14 -0
- package/packages/mcp-server/dist/session-manager.d.ts.map +1 -1
- package/packages/mcp-server/dist/session-manager.js +49 -1
- package/packages/mcp-server/dist/session-manager.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +37 -0
- package/packages/mcp-server/src/server.ts +27 -4
- package/packages/mcp-server/src/session-manager.ts +43 -1
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +3 -2
- package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +7 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +3 -2
- package/packages/pi-coding-agent/src/core/agent-session.ts +11 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +2 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/loop.ts +9 -0
- package/src/resources/extensions/gsd/auto/phases.ts +11 -2
- package/src/resources/extensions/gsd/auto/run-unit.ts +11 -2
- package/src/resources/extensions/gsd/auto/session.ts +6 -1
- package/src/resources/extensions/gsd/auto-recovery.ts +11 -1
- package/src/resources/extensions/gsd/auto.ts +11 -0
- package/src/resources/extensions/gsd/gitignore.ts +1 -1
- package/src/resources/extensions/gsd/gsd-db.ts +23 -6
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +69 -1
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/integration/gitignore-tracked-gsd.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/integration/idle-recovery.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/resume-dispatch-worktree.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +6 -1
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +78 -5
- package/src/resources/extensions/gsd/worktree-resolver.ts +54 -9
- /package/dist/web/standalone/.next/static/{TKKbktue8R7x5oPjkT9j1 → JgU2F-5N9mTyB7kUSSk9A}/_buildManifest.js +0 -0
- /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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
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
|
}
|
|
File without changes
|
|
File without changes
|