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
|
@@ -30,6 +30,13 @@ function stuckStatePath(basePath) {
|
|
|
30
30
|
function loadStuckState(basePath) {
|
|
31
31
|
try {
|
|
32
32
|
const data = JSON.parse(readFileSync(stuckStatePath(basePath), "utf-8"));
|
|
33
|
+
// Only load state written by a DIFFERENT process (real session restart).
|
|
34
|
+
// If the PID matches the current process, this state was written by an earlier
|
|
35
|
+
// autoLoop call in the same process (e.g., a test that completed before this
|
|
36
|
+
// one), not by a crashed session — skip it to prevent test state pollution.
|
|
37
|
+
if (data.pid === process.pid) {
|
|
38
|
+
return { recentUnits: [], stuckRecoveryAttempts: 0 };
|
|
39
|
+
}
|
|
33
40
|
return {
|
|
34
41
|
recentUnits: Array.isArray(data.recentUnits) ? data.recentUnits : [],
|
|
35
42
|
stuckRecoveryAttempts: typeof data.stuckRecoveryAttempts === "number" ? data.stuckRecoveryAttempts : 0,
|
|
@@ -45,6 +52,7 @@ function saveStuckState(basePath, state) {
|
|
|
45
52
|
const filePath = stuckStatePath(basePath);
|
|
46
53
|
mkdirSync(join(gsdRoot(basePath), "runtime"), { recursive: true });
|
|
47
54
|
writeFileSync(filePath, JSON.stringify({
|
|
55
|
+
pid: process.pid,
|
|
48
56
|
recentUnits: state.recentUnits.slice(-20), // keep last 20 entries
|
|
49
57
|
stuckRecoveryAttempts: state.stuckRecoveryAttempts,
|
|
50
58
|
updatedAt: new Date().toISOString(),
|
|
@@ -497,6 +505,7 @@ export async function autoLoop(ctx, pi, s, deps, options) {
|
|
|
497
505
|
consecutiveCooldowns = 0;
|
|
498
506
|
recentErrorMessages.length = 0;
|
|
499
507
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-end", data: { iteration } });
|
|
508
|
+
saveStuckState(s.basePath, loopState); // persist across session restarts (#4382)
|
|
500
509
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|
|
501
510
|
finishTurn("completed");
|
|
502
511
|
}
|
|
@@ -48,7 +48,11 @@ export function resetSessionTimeoutState() {
|
|
|
48
48
|
* Exported for testing as _resolveReportBasePath.
|
|
49
49
|
*/
|
|
50
50
|
export function _resolveReportBasePath(s) {
|
|
51
|
-
|
|
51
|
+
// Strip /.gsd/worktrees/ suffix when basePath is itself a worktree path and
|
|
52
|
+
// originalBasePath is falsy — prevents reports landing in the worktree (#3729).
|
|
53
|
+
const resolved = s.originalBasePath || s.basePath;
|
|
54
|
+
const markerIdx = resolved.indexOf("/.gsd/worktrees/");
|
|
55
|
+
return markerIdx !== -1 ? resolved.slice(0, markerIdx) : resolved;
|
|
52
56
|
}
|
|
53
57
|
/**
|
|
54
58
|
* Resolve the authoritative project base for dispatch guards.
|
|
@@ -56,7 +60,12 @@ export function _resolveReportBasePath(s) {
|
|
|
56
60
|
* unit is running inside an auto worktree.
|
|
57
61
|
*/
|
|
58
62
|
export function _resolveDispatchGuardBasePath(s) {
|
|
59
|
-
|
|
63
|
+
// Strip /.gsd/worktrees/ suffix when basePath is itself a worktree path and
|
|
64
|
+
// originalBasePath is falsy — prevents guard checks running against the
|
|
65
|
+
// worktree instead of the project root (#3729).
|
|
66
|
+
const resolved = s.originalBasePath || s.basePath;
|
|
67
|
+
const markerIdx = resolved.indexOf("/.gsd/worktrees/");
|
|
68
|
+
return markerIdx !== -1 ? resolved.slice(0, markerIdx) : resolved;
|
|
60
69
|
}
|
|
61
70
|
const PLAN_V2_GATE_PHASES = new Set([
|
|
62
71
|
"executing",
|
|
@@ -26,15 +26,24 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
|
|
|
26
26
|
let sessionResult;
|
|
27
27
|
let sessionTimeoutHandle;
|
|
28
28
|
const mySessionSwitchGeneration = ++sessionSwitchGeneration;
|
|
29
|
+
// #3731: Cancellation controller for newSession(). When the session-creation
|
|
30
|
+
// timeout fires, we abort this controller so that the still-in-flight
|
|
31
|
+
// newSession() discards itself after await this.abort() completes, preventing
|
|
32
|
+
// it from capturing the (now-root) process.cwd() and rebuilding the tool
|
|
33
|
+
// runtime with the wrong cwd.
|
|
34
|
+
const sessionAbortController = new AbortController();
|
|
29
35
|
_setSessionSwitchInFlight(true);
|
|
30
36
|
try {
|
|
31
|
-
const sessionPromise = s.cmdCtx.newSession().finally(() => {
|
|
37
|
+
const sessionPromise = s.cmdCtx.newSession({ abortSignal: sessionAbortController.signal }).finally(() => {
|
|
32
38
|
if (sessionSwitchGeneration === mySessionSwitchGeneration) {
|
|
33
39
|
_setSessionSwitchInFlight(false);
|
|
34
40
|
}
|
|
35
41
|
});
|
|
36
42
|
const timeoutPromise = new Promise((resolve) => {
|
|
37
|
-
sessionTimeoutHandle = setTimeout(() =>
|
|
43
|
+
sessionTimeoutHandle = setTimeout(() => {
|
|
44
|
+
sessionAbortController.abort();
|
|
45
|
+
resolve({ cancelled: true });
|
|
46
|
+
}, NEW_SESSION_TIMEOUT_MS);
|
|
38
47
|
});
|
|
39
48
|
sessionResult = await Promise.race([sessionPromise, timeoutPromise]);
|
|
40
49
|
}
|
|
@@ -150,7 +150,12 @@ export class AutoSession {
|
|
|
150
150
|
this.unitLifetimeDispatches.clear();
|
|
151
151
|
}
|
|
152
152
|
get lockBasePath() {
|
|
153
|
-
|
|
153
|
+
// Prefer originalBasePath (project root); fall back to basePath.
|
|
154
|
+
// Strip /.gsd/worktrees/ suffix if basePath is itself a worktree path
|
|
155
|
+
// to avoid reading/writing the lock inside the worktree (#3729).
|
|
156
|
+
const resolved = this.originalBasePath || this.basePath;
|
|
157
|
+
const markerIdx = resolved.indexOf("/.gsd/worktrees/");
|
|
158
|
+
return markerIdx !== -1 ? resolved.slice(0, markerIdx) : resolved;
|
|
154
159
|
}
|
|
155
160
|
reset() {
|
|
156
161
|
this.clearTimers();
|
|
@@ -11,7 +11,7 @@ import { appendEvent } from "./workflow-events.js";
|
|
|
11
11
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
12
12
|
import { clearParseCache } from "./files.js";
|
|
13
13
|
import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js";
|
|
14
|
-
import { isDbAvailable, getTask, getSlice, getSliceTasks, getPendingGates, updateTaskStatus, updateSliceStatus } from "./gsd-db.js";
|
|
14
|
+
import { isDbAvailable, getTask, getSlice, getSliceTasks, getPendingGates, updateTaskStatus, updateSliceStatus, insertSlice } from "./gsd-db.js";
|
|
15
15
|
import { isValidationTerminal } from "./state.js";
|
|
16
16
|
import { getErrorMessage } from "./error-utils.js";
|
|
17
17
|
import { logWarning, logError } from "./workflow-logger.js";
|
|
@@ -510,6 +510,24 @@ export function writeBlockerPlaceholder(unitType, unitId, base, reason) {
|
|
|
510
510
|
logWarning("recovery", `appendEvent failed for slice recovery: ${e instanceof Error ? e.message : String(e)}`);
|
|
511
511
|
}
|
|
512
512
|
}
|
|
513
|
+
// Insert a placeholder complete slice so deriveState sees activeMilestoneSlices.length > 0
|
|
514
|
+
// and exits the pre-planning phase. Without this, activeMilestoneSlices stays empty
|
|
515
|
+
// after the blocker ROADMAP.md is written, causing deriveState to return phase:'pre-planning'
|
|
516
|
+
// indefinitely and re-dispatching plan-milestone in an infinite loop (#4378).
|
|
517
|
+
if (unitType === "plan-milestone" && mid) {
|
|
518
|
+
try {
|
|
519
|
+
insertSlice({ id: "S00-blocker", milestoneId: mid, title: "Blocker placeholder — planning failed", status: "complete", sequence: 0 });
|
|
520
|
+
}
|
|
521
|
+
catch (e) {
|
|
522
|
+
logWarning("recovery", `insertSlice placeholder failed for plan-milestone recovery: ${e instanceof Error ? e.message : String(e)}`);
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
appendEvent(base, { cmd: "plan-milestone", params: { milestoneId: mid }, ts, actor: "system", trigger_reason: "blocker-placeholder-recovery" });
|
|
526
|
+
}
|
|
527
|
+
catch (e) {
|
|
528
|
+
logWarning("recovery", `appendEvent failed for plan-milestone recovery: ${e instanceof Error ? e.message : String(e)}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
513
531
|
}
|
|
514
532
|
return diagnoseExpectedArtifact(unitType, unitId, base);
|
|
515
533
|
}
|
|
@@ -1174,6 +1174,17 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
1174
1174
|
s.stepMode = requestedStepMode;
|
|
1175
1175
|
s.cmdCtx = ctx;
|
|
1176
1176
|
s.basePath = base;
|
|
1177
|
+
// ── Resume worktree: if the paused session was inside a milestone worktree,
|
|
1178
|
+
// apply that path as the dispatch basePath immediately (#3723).
|
|
1179
|
+
// This ensures the dispatch loop runs from the worktree directory even when
|
|
1180
|
+
// enterMilestone guard conditions differ between the original and resumed
|
|
1181
|
+
// session (e.g. isolation mode changed, detectWorktreeName differs across
|
|
1182
|
+
// process restarts). We guard with existsSync so a stale or deleted
|
|
1183
|
+
// worktree directory safely falls back to the project root.
|
|
1184
|
+
const resumeWorktreePath = freshStartAssessment.pausedSession?.worktreePath;
|
|
1185
|
+
if (resumeWorktreePath && existsSync(resumeWorktreePath)) {
|
|
1186
|
+
s.basePath = resumeWorktreePath;
|
|
1187
|
+
}
|
|
1177
1188
|
// Ensure the workflow-logger audit log is pinned to the project root
|
|
1178
1189
|
// even when auto-mode is entered via a path that bypasses the
|
|
1179
1190
|
// bootstrap/dynamic-tools ensureDbOpen() → setLogBasePath() chain
|
|
@@ -2396,7 +2396,10 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
|
|
|
2396
2396
|
SELECT path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at
|
|
2397
2397
|
FROM wt.artifacts
|
|
2398
2398
|
`).run());
|
|
2399
|
-
// Merge milestones — worktree may have updated status/planning fields
|
|
2399
|
+
// Merge milestones — worktree may have updated status/planning fields.
|
|
2400
|
+
// Never downgrade status: complete > active > pre-planning (#4372).
|
|
2401
|
+
// A stale worktree may carry an older 'active' status for a milestone
|
|
2402
|
+
// that the main DB has already marked 'complete'; preserve the higher status.
|
|
2400
2403
|
merged.milestones = countChanges(adapter.prepare(`
|
|
2401
2404
|
INSERT OR REPLACE INTO milestones (
|
|
2402
2405
|
id, title, status, depends_on, created_at, completed_at,
|
|
@@ -2404,11 +2407,25 @@ export function reconcileWorktreeDb(mainDbPath, worktreeDbPath) {
|
|
|
2404
2407
|
verification_contract, verification_integration, verification_operational, verification_uat,
|
|
2405
2408
|
definition_of_done, requirement_coverage, boundary_map_markdown
|
|
2406
2409
|
)
|
|
2407
|
-
SELECT id, title,
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2410
|
+
SELECT w.id, w.title,
|
|
2411
|
+
CASE
|
|
2412
|
+
WHEN m.status IN ('complete', 'done') AND w.status NOT IN ('complete', 'done')
|
|
2413
|
+
THEN m.status ELSE w.status
|
|
2414
|
+
END,
|
|
2415
|
+
w.depends_on,
|
|
2416
|
+
CASE
|
|
2417
|
+
WHEN m.status IN ('complete', 'done') AND w.status NOT IN ('complete', 'done')
|
|
2418
|
+
THEN m.created_at ELSE w.created_at
|
|
2419
|
+
END,
|
|
2420
|
+
CASE
|
|
2421
|
+
WHEN m.status IN ('complete', 'done') AND w.status NOT IN ('complete', 'done')
|
|
2422
|
+
THEN m.completed_at ELSE w.completed_at
|
|
2423
|
+
END,
|
|
2424
|
+
w.vision, w.success_criteria, w.key_risks, w.proof_strategy,
|
|
2425
|
+
w.verification_contract, w.verification_integration, w.verification_operational, w.verification_uat,
|
|
2426
|
+
w.definition_of_done, w.requirement_coverage, w.boundary_map_markdown
|
|
2427
|
+
FROM wt.milestones w
|
|
2428
|
+
LEFT JOIN milestones m ON m.id = w.id
|
|
2412
2429
|
`).run());
|
|
2413
2430
|
// Merge slices — preserve worktree progress but never downgrade completed status (#2558).
|
|
2414
2431
|
// ADR-011 Phase 1: carry is_sketch + sketch_scope so reconcile doesn't
|
|
@@ -16,8 +16,30 @@ import { existsSync, unlinkSync } from "node:fs";
|
|
|
16
16
|
import { randomUUID } from "node:crypto";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
import { debugLog } from "./debug-logger.js";
|
|
19
|
-
import { MergeConflictError } from "./git-service.js";
|
|
20
19
|
import { emitJournalEvent } from "./journal.js";
|
|
20
|
+
// ─── Path Helpers ──────────────────────────────────────────────────────────
|
|
21
|
+
/**
|
|
22
|
+
* Worktree marker segment — present in any path produced by worktreePath().
|
|
23
|
+
* Used to strip the worktree suffix and recover the project root (#3729).
|
|
24
|
+
*/
|
|
25
|
+
const WORKTREE_MARKER = "/.gsd/worktrees/";
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the project root from session path state.
|
|
28
|
+
*
|
|
29
|
+
* Prefers `originalBasePath` (always the project root when set), but falls
|
|
30
|
+
* back to `basePath` when `originalBasePath` is falsy (e.g. fresh AutoSession
|
|
31
|
+
* with default empty string). If `basePath` itself is inside a worktree
|
|
32
|
+
* directory (contains `/.gsd/worktrees/`), strip that suffix to recover the
|
|
33
|
+
* actual project root — preventing double-nested worktree paths (#3729).
|
|
34
|
+
*/
|
|
35
|
+
export function resolveProjectRoot(originalBasePath, basePath) {
|
|
36
|
+
let resolved = originalBasePath || basePath;
|
|
37
|
+
const markerIdx = resolved.indexOf(WORKTREE_MARKER);
|
|
38
|
+
if (markerIdx !== -1) {
|
|
39
|
+
resolved = resolved.slice(0, markerIdx);
|
|
40
|
+
}
|
|
41
|
+
return resolved;
|
|
42
|
+
}
|
|
21
43
|
// ─── WorktreeResolver ──────────────────────────────────────────────────────
|
|
22
44
|
export class WorktreeResolver {
|
|
23
45
|
s;
|
|
@@ -33,11 +55,11 @@ export class WorktreeResolver {
|
|
|
33
55
|
}
|
|
34
56
|
/** Original project root — always the non-worktree path. */
|
|
35
57
|
get projectRoot() {
|
|
36
|
-
return this.s.originalBasePath
|
|
58
|
+
return resolveProjectRoot(this.s.originalBasePath, this.s.basePath);
|
|
37
59
|
}
|
|
38
60
|
/** Path for auto.lock file — same as the old lockBase(). */
|
|
39
61
|
get lockPath() {
|
|
40
|
-
return this.s.originalBasePath
|
|
62
|
+
return resolveProjectRoot(this.s.originalBasePath, this.s.basePath);
|
|
41
63
|
}
|
|
42
64
|
// ── Private Helpers ────────────────────────────────────────────────────
|
|
43
65
|
rebuildGitService() {
|
|
@@ -99,7 +121,10 @@ export class WorktreeResolver {
|
|
|
99
121
|
});
|
|
100
122
|
return;
|
|
101
123
|
}
|
|
102
|
-
|
|
124
|
+
// Resolve the project root for worktree operations via shared helper.
|
|
125
|
+
// Handles the case where originalBasePath is falsy and basePath is itself
|
|
126
|
+
// a worktree path — prevents double-nested worktree paths (#3729).
|
|
127
|
+
const basePath = resolveProjectRoot(this.s.originalBasePath, this.s.basePath);
|
|
103
128
|
debugLog("WorktreeResolver", {
|
|
104
129
|
action: "enterMilestone",
|
|
105
130
|
milestoneId,
|
|
@@ -429,11 +454,13 @@ export class WorktreeResolver {
|
|
|
429
454
|
/* best-effort */
|
|
430
455
|
}
|
|
431
456
|
}
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
457
|
+
// Restore state before re-throwing so callers always get a consistent
|
|
458
|
+
// session (#4380).
|
|
459
|
+
this.restoreToProjectRoot();
|
|
460
|
+
// Re-throw: MergeConflictError stops the auto loop (#2330); non-conflict
|
|
461
|
+
// errors (permission denied, filesystem failures) must also propagate so
|
|
462
|
+
// broken states are diagnosable (#4380).
|
|
463
|
+
throw err;
|
|
437
464
|
}
|
|
438
465
|
// Always restore basePath and rebuild — whether merge succeeded or failed
|
|
439
466
|
this.restoreToProjectRoot();
|
|
@@ -500,6 +527,8 @@ export class WorktreeResolver {
|
|
|
500
527
|
error: msg,
|
|
501
528
|
});
|
|
502
529
|
ctx.notify(`Milestone merge failed (branch mode): ${msg}`, "warning");
|
|
530
|
+
// Re-throw all errors so callers can apply their own recovery logic (#4380).
|
|
531
|
+
throw err;
|
|
503
532
|
}
|
|
504
533
|
}
|
|
505
534
|
// ── Merge and Enter Next ───────────────────────────────────────────────
|
|
@@ -516,7 +545,18 @@ export class WorktreeResolver {
|
|
|
516
545
|
currentMilestoneId,
|
|
517
546
|
nextMilestoneId,
|
|
518
547
|
});
|
|
519
|
-
|
|
548
|
+
try {
|
|
549
|
+
this.mergeAndExit(currentMilestoneId, ctx);
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
// mergeAndExit emits a warning and restores state when it fails during
|
|
553
|
+
// merge/cleanup. But if it throws before recovery runs (e.g., in
|
|
554
|
+
// validateMilestoneId or emitJournalEvent), basePath won't be restored
|
|
555
|
+
// to projectRoot — re-throw so we don't enter the next milestone with
|
|
556
|
+
// the current one unmerged.
|
|
557
|
+
if (this.s.basePath !== this.projectRoot)
|
|
558
|
+
throw err;
|
|
559
|
+
}
|
|
520
560
|
this.enterMilestone(nextMilestoneId, ctx);
|
|
521
561
|
}
|
|
522
562
|
}
|