gsd-pi 2.80.0-dev.e51d2c88c → 2.80.0-dev.e6c48c3af
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/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +30 -6
- package/dist/resources/extensions/gsd/auto/run-unit.js +4 -1
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto.js +18 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +54 -4
- package/dist/resources/extensions/gsd/clean-root-preflight.js +24 -6
- package/dist/resources/extensions/gsd/git-service.js +36 -4
- package/dist/resources/extensions/gsd/pre-execution-checks.js +7 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +33 -17
- 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 +11 -11
- 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 +11 -11
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- 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/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +15 -0
- package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.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 +4 -3
- 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 +5 -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-tool-refresh.test.ts +18 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +6 -3
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +2 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +5 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -2
- package/src/resources/extensions/gsd/auto/phases.ts +50 -15
- package/src/resources/extensions/gsd/auto/run-unit.ts +4 -1
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto.ts +18 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +66 -4
- package/src/resources/extensions/gsd/clean-root-preflight.ts +32 -7
- package/src/resources/extensions/gsd/git-service.ts +46 -8
- package/src/resources/extensions/gsd/pre-execution-checks.ts +7 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +144 -1
- package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +166 -4
- package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +15 -6
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +63 -1
- package/src/resources/extensions/gsd/worktree-resolver.ts +36 -15
- /package/dist/web/standalone/.next/static/{8F5YpnZNBaooIWGF4GBV3 → 4dQ9NTZJ8pEvFwBgDUX93}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{8F5YpnZNBaooIWGF4GBV3 → 4dQ9NTZJ8pEvFwBgDUX93}/_ssgManifest.js +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
533c696bf1b57db9
|
|
@@ -129,6 +129,22 @@ async function closeoutAndStop(ctx, pi, s, deps, reason) {
|
|
|
129
129
|
}
|
|
130
130
|
await deps.stopAuto(ctx, pi, reason);
|
|
131
131
|
}
|
|
132
|
+
async function stopOnPostflightRecoveryNeeded(ic, result, milestoneId) {
|
|
133
|
+
if (!result.needsManualRecovery)
|
|
134
|
+
return null;
|
|
135
|
+
const { ctx, pi, deps } = ic;
|
|
136
|
+
const reason = `Post-merge stash restore failed for milestone ${milestoneId}`;
|
|
137
|
+
ctx.ui.notify(`${reason}. Resolve the working tree before resuming auto-mode. ${result.message}`, "error");
|
|
138
|
+
await deps.stopAuto(ctx, pi, reason);
|
|
139
|
+
return { action: "break", reason: "postflight-stash-restore-failed" };
|
|
140
|
+
}
|
|
141
|
+
async function restorePreflightStashOrStop(ic, preflight, milestoneId) {
|
|
142
|
+
if (!preflight.stashPushed)
|
|
143
|
+
return null;
|
|
144
|
+
const { ctx, s, deps } = ic;
|
|
145
|
+
const result = deps.postflightPopStash(s.originalBasePath || s.basePath, milestoneId, preflight.stashMarker, ctx.ui.notify.bind(ctx.ui));
|
|
146
|
+
return stopOnPostflightRecoveryNeeded(ic, result, milestoneId);
|
|
147
|
+
}
|
|
132
148
|
async function emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, errorContext) {
|
|
133
149
|
ic.deps.emitJournalEvent({
|
|
134
150
|
ts: new Date().toISOString(),
|
|
@@ -475,6 +491,8 @@ export async function runPreDispatch(ic, loopState) {
|
|
|
475
491
|
const preflightTransition = deps.preflightCleanRoot(s.originalBasePath || s.basePath, s.currentMilestoneId, ctx.ui.notify.bind(ctx.ui));
|
|
476
492
|
try {
|
|
477
493
|
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
494
|
+
// Prevent stopAuto() from attempting the same merge again if postflight recovery stops here.
|
|
495
|
+
s.milestoneMergedInPhases = true;
|
|
478
496
|
}
|
|
479
497
|
catch (mergeErr) {
|
|
480
498
|
if (mergeErr instanceof MergeConflictError) {
|
|
@@ -490,8 +508,10 @@ export async function runPreDispatch(ic, loopState) {
|
|
|
490
508
|
return { action: "break", reason: "merge-failed" };
|
|
491
509
|
}
|
|
492
510
|
// #2909: postflight — restore stashed changes after successful merge
|
|
493
|
-
|
|
494
|
-
|
|
511
|
+
{
|
|
512
|
+
const postflightStop = await restorePreflightStashOrStop(ic, preflightTransition, s.currentMilestoneId);
|
|
513
|
+
if (postflightStop)
|
|
514
|
+
return postflightStop;
|
|
495
515
|
}
|
|
496
516
|
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
497
517
|
deps.invalidateAllCaches();
|
|
@@ -566,8 +586,10 @@ export async function runPreDispatch(ic, loopState) {
|
|
|
566
586
|
return { action: "break", reason: "merge-failed" };
|
|
567
587
|
}
|
|
568
588
|
// #2909: postflight — restore stashed changes after successful merge
|
|
569
|
-
|
|
570
|
-
|
|
589
|
+
{
|
|
590
|
+
const postflightStop = await restorePreflightStashOrStop(ic, preflightAllComplete, s.currentMilestoneId);
|
|
591
|
+
if (postflightStop)
|
|
592
|
+
return postflightStop;
|
|
571
593
|
}
|
|
572
594
|
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
573
595
|
}
|
|
@@ -652,8 +674,10 @@ export async function runPreDispatch(ic, loopState) {
|
|
|
652
674
|
return { action: "break", reason: "merge-failed" };
|
|
653
675
|
}
|
|
654
676
|
// #2909: postflight — restore stashed changes after successful merge
|
|
655
|
-
|
|
656
|
-
|
|
677
|
+
{
|
|
678
|
+
const postflightStop = await restorePreflightStashOrStop(ic, preflightComplete, s.currentMilestoneId);
|
|
679
|
+
if (postflightStop)
|
|
680
|
+
return postflightStop;
|
|
657
681
|
}
|
|
658
682
|
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
659
683
|
}
|
|
@@ -55,7 +55,10 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
|
|
|
55
55
|
const sessionAbortController = new AbortController();
|
|
56
56
|
_setSessionSwitchInFlight(true);
|
|
57
57
|
try {
|
|
58
|
-
const sessionPromise = s.cmdCtx.newSession({
|
|
58
|
+
const sessionPromise = s.cmdCtx.newSession({
|
|
59
|
+
abortSignal: sessionAbortController.signal,
|
|
60
|
+
cwd: s.basePath,
|
|
61
|
+
}).finally(() => {
|
|
59
62
|
if (sessionSwitchGeneration === mySessionSwitchGeneration) {
|
|
60
63
|
_setSessionSwitchInFlight(false);
|
|
61
64
|
}
|
|
@@ -246,7 +246,7 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
|
|
|
246
246
|
ctx.ui.notify(`${msg}. Cancelling dispatch to avoid running in the wrong directory.`, "error");
|
|
247
247
|
return;
|
|
248
248
|
}
|
|
249
|
-
const result = await ctx.newSession();
|
|
249
|
+
const result = await ctx.newSession({ cwd: dispatchBase });
|
|
250
250
|
if (result.cancelled) {
|
|
251
251
|
ctx.ui.notify("Session creation cancelled.", "warning");
|
|
252
252
|
return;
|
|
@@ -720,6 +720,8 @@ export async function stopAuto(ctx, pi, reason) {
|
|
|
720
720
|
if (s.workerId) {
|
|
721
721
|
markWorkerStopping(s.workerId);
|
|
722
722
|
}
|
|
723
|
+
s.workerId = null;
|
|
724
|
+
s.milestoneLeaseToken = null;
|
|
723
725
|
}
|
|
724
726
|
catch (e) {
|
|
725
727
|
debugLog("stop-cleanup-coordination", { error: e instanceof Error ? e.message : String(e) });
|
|
@@ -852,6 +854,21 @@ export async function stopAuto(ctx, pi, reason) {
|
|
|
852
854
|
catch (e) {
|
|
853
855
|
debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
|
|
854
856
|
}
|
|
857
|
+
// Re-root the active command session/tool runtime after worktree teardown.
|
|
858
|
+
// mergeAndExit restores process.cwd(), but AgentSession has already captured
|
|
859
|
+
// its own cwd for tools and system prompt; refresh it before returning to the
|
|
860
|
+
// user so follow-up commands do not target a removed milestone worktree.
|
|
861
|
+
if (s.originalBasePath && ctx && s.cmdCtx) {
|
|
862
|
+
try {
|
|
863
|
+
const result = await s.cmdCtx.newSession({ cwd: s.basePath });
|
|
864
|
+
if (result.cancelled) {
|
|
865
|
+
logWarning("engine", "post-stop session re-root was cancelled", { file: "auto.ts", basePath: s.basePath });
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
catch (err) {
|
|
869
|
+
logWarning("engine", `post-stop session re-root failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts", basePath: s.basePath });
|
|
870
|
+
}
|
|
871
|
+
}
|
|
855
872
|
// ── Step 8: Ledger notification ──
|
|
856
873
|
try {
|
|
857
874
|
const ledger = getLedger();
|
|
@@ -1831,7 +1848,7 @@ export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, trigg
|
|
|
1831
1848
|
}
|
|
1832
1849
|
return false;
|
|
1833
1850
|
}
|
|
1834
|
-
const result = await s.cmdCtx.newSession();
|
|
1851
|
+
const result = await s.cmdCtx.newSession({ cwd: s.basePath });
|
|
1835
1852
|
if (result.cancelled) {
|
|
1836
1853
|
await stopAuto(ctx, pi);
|
|
1837
1854
|
return false;
|
|
@@ -23,6 +23,7 @@ import { approvalGateIdForUnit, isExplicitApprovalResponse, shouldPauseForUserAp
|
|
|
23
23
|
// printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
|
|
24
24
|
let isFirstSession = true;
|
|
25
25
|
let approvalQuestionAbortInFlight = false;
|
|
26
|
+
let deferredApprovalGate = null;
|
|
26
27
|
async function deriveGsdState(basePath) {
|
|
27
28
|
const { deriveState } = await import("../state.js");
|
|
28
29
|
return deriveState(basePath);
|
|
@@ -71,6 +72,43 @@ async function applyCompactionThresholdOverride(ctx) {
|
|
|
71
72
|
// Non-fatal: leave any existing override in place.
|
|
72
73
|
}
|
|
73
74
|
}
|
|
75
|
+
function clearDeferredApprovalGate(basePath) {
|
|
76
|
+
if (!basePath || deferredApprovalGate?.basePath === basePath) {
|
|
77
|
+
deferredApprovalGate = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function deferApprovalGate(gateId, basePath) {
|
|
81
|
+
deferredApprovalGate = { gateId, basePath };
|
|
82
|
+
}
|
|
83
|
+
function activateDeferredApprovalGate(basePath) {
|
|
84
|
+
if (deferredApprovalGate?.basePath !== basePath)
|
|
85
|
+
return;
|
|
86
|
+
setPendingGate(deferredApprovalGate.gateId, basePath);
|
|
87
|
+
deferredApprovalGate = null;
|
|
88
|
+
}
|
|
89
|
+
function isContextDraftSummarySave(toolName, input) {
|
|
90
|
+
if (toolName !== "gsd_summary_save" && toolName !== "summary_save")
|
|
91
|
+
return false;
|
|
92
|
+
if (!input || typeof input !== "object")
|
|
93
|
+
return false;
|
|
94
|
+
return input.artifact_type === "CONTEXT-DRAFT";
|
|
95
|
+
}
|
|
96
|
+
function shouldBlockDeferredApprovalTool(toolName, input, basePath) {
|
|
97
|
+
if (deferredApprovalGate?.basePath !== basePath)
|
|
98
|
+
return { block: false };
|
|
99
|
+
if (toolName === "ask_user_questions")
|
|
100
|
+
return { block: false };
|
|
101
|
+
if (isContextDraftSummarySave(toolName, input))
|
|
102
|
+
return { block: false };
|
|
103
|
+
return {
|
|
104
|
+
block: true,
|
|
105
|
+
reason: [
|
|
106
|
+
`HARD BLOCK: Approval question "${deferredApprovalGate.gateId}" has been shown to the user.`,
|
|
107
|
+
`Only CONTEXT-DRAFT persistence may finish in this same assistant turn.`,
|
|
108
|
+
`Wait for the user's answer before calling additional tools.`,
|
|
109
|
+
].join(" "),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
74
112
|
export function resolveNotificationStoreBasePath(cwd = process.cwd()) {
|
|
75
113
|
return resolveWorktreeProjectRoot(cwd);
|
|
76
114
|
}
|
|
@@ -117,6 +155,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
117
155
|
resetWriteGateState(process.cwd());
|
|
118
156
|
resetToolCallLoopGuard();
|
|
119
157
|
approvalQuestionAbortInFlight = false;
|
|
158
|
+
clearDeferredApprovalGate();
|
|
120
159
|
await resetAskUserQuestionsTurnCache();
|
|
121
160
|
await syncServiceTierStatus(ctx);
|
|
122
161
|
await applyDisabledModelProviderPolicy(ctx);
|
|
@@ -165,6 +204,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
165
204
|
initSessionNotifications(ctx);
|
|
166
205
|
resetWriteGateState(process.cwd());
|
|
167
206
|
resetToolCallLoopGuard();
|
|
207
|
+
clearDeferredApprovalGate();
|
|
168
208
|
await resetAskUserQuestionsTurnCache();
|
|
169
209
|
clearDiscussionFlowState(process.cwd());
|
|
170
210
|
await syncServiceTierStatus(ctx);
|
|
@@ -201,6 +241,7 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
201
241
|
markDepthVerified(milestoneId, beforeAgentBasePath);
|
|
202
242
|
clearPendingGate(beforeAgentBasePath);
|
|
203
243
|
}
|
|
244
|
+
clearDeferredApprovalGate(beforeAgentBasePath);
|
|
204
245
|
// GSD's own context injection (existing behavior — unchanged).
|
|
205
246
|
const { buildBeforeAgentStartResult } = await import("./system-context.js");
|
|
206
247
|
const gsdResult = await buildBeforeAgentStartResult(event, ctx);
|
|
@@ -244,7 +285,12 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
244
285
|
resetToolCallLoopGuard();
|
|
245
286
|
await resetAskUserQuestionsTurnCache();
|
|
246
287
|
const { handleAgentEnd } = await import("./agent-end-recovery.js");
|
|
247
|
-
|
|
288
|
+
try {
|
|
289
|
+
await handleAgentEnd(pi, event, ctx);
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
activateDeferredApprovalGate(process.cwd());
|
|
293
|
+
}
|
|
248
294
|
});
|
|
249
295
|
// Squash-merge quick-task branch back to the original branch after the
|
|
250
296
|
// agent turn completes (#2668). cleanupQuickBranch is a no-op when no
|
|
@@ -342,11 +388,12 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
342
388
|
return;
|
|
343
389
|
const gateId = approvalGateIdForUnit(unitType, unitId);
|
|
344
390
|
if (gateId)
|
|
345
|
-
|
|
391
|
+
deferApprovalGate(gateId, process.cwd());
|
|
346
392
|
approvalQuestionAbortInFlight = true;
|
|
347
393
|
ctx.ui.notify(`${unitType}${unitId ? ` ${unitId}` : ""} is waiting for your approval - pausing before more tool calls run.`, "info");
|
|
348
|
-
// The pending gate
|
|
349
|
-
//
|
|
394
|
+
// The durable pending gate is activated at agent_end so same-turn
|
|
395
|
+
// CONTEXT-DRAFT persistence can finish after the text boundary streams.
|
|
396
|
+
// The tool_call hook below still blocks non-draft tools in this turn.
|
|
350
397
|
// Aborting mid-stream eats the model's question text on external CLI
|
|
351
398
|
// providers (Claude Code SDK) because lastTextContent isn't populated
|
|
352
399
|
// from in-flight builder state — the user only ever sees "Claude Code
|
|
@@ -377,6 +424,9 @@ export function registerHooks(pi, ecosystemHandlers) {
|
|
|
377
424
|
if (loopCheck.block) {
|
|
378
425
|
return { block: true, reason: loopCheck.reason };
|
|
379
426
|
}
|
|
427
|
+
const deferredGateGuard = shouldBlockDeferredApprovalTool(toolName, event.input, discussionBasePath);
|
|
428
|
+
if (deferredGateGuard.block)
|
|
429
|
+
return deferredGateGuard;
|
|
380
430
|
// ── Discussion gate enforcement: track pending gate questions ─────────
|
|
381
431
|
// Only gate-shaped ask_user_questions calls should block execution.
|
|
382
432
|
// The gate stays pending until the user selects the approval option.
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* #2909: Adds a fast-path git status check before milestone completion merges.
|
|
5
5
|
* When the working tree is dirty the user is warned and changes are auto-stashed
|
|
6
|
-
* so the merge can proceed cleanly.
|
|
7
|
-
* restores the stashed changes.
|
|
6
|
+
* so the merge can proceed cleanly. After the merge completes, postflightPopStash
|
|
7
|
+
* restores the stashed changes and reports whether manual recovery is needed.
|
|
8
8
|
*
|
|
9
9
|
* Design constraints (from Trek-e approval):
|
|
10
10
|
* - Warn the user before stashing (no silent surprises)
|
|
11
11
|
* - git stash push / git stash pop only — no custom stash management layer
|
|
12
|
-
* - Stash/pop errors are logged but MUST NOT block the merge
|
|
12
|
+
* - Stash/pop errors are logged but MUST NOT block the merge itself
|
|
13
13
|
* - Fast-path status check — clean trees pay no extra cost
|
|
14
14
|
*/
|
|
15
15
|
import { execFileSync } from "node:child_process";
|
|
@@ -98,7 +98,8 @@ export function preflightCleanRoot(basePath, milestoneId, notify) {
|
|
|
98
98
|
*
|
|
99
99
|
* Only called when preflightCleanRoot returned stashPushed=true.
|
|
100
100
|
* Any pop error (e.g. conflict) is logged and notified but does NOT throw —
|
|
101
|
-
* the merge already completed successfully.
|
|
101
|
+
* the merge already completed successfully. Callers must treat
|
|
102
|
+
* needsManualRecovery=true as a dirty workspace stop, not a clean completion.
|
|
102
103
|
*/
|
|
103
104
|
export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
|
|
104
105
|
let stashRef = null;
|
|
@@ -108,7 +109,11 @@ export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
|
|
|
108
109
|
const msg = `No matching GSD preflight stash found for milestone ${milestoneId}; leaving stash list untouched.`;
|
|
109
110
|
logWarning("preflight", msg);
|
|
110
111
|
notify(msg, "warning");
|
|
111
|
-
return
|
|
112
|
+
return {
|
|
113
|
+
restored: false,
|
|
114
|
+
needsManualRecovery: true,
|
|
115
|
+
message: msg,
|
|
116
|
+
};
|
|
112
117
|
}
|
|
113
118
|
execFileSync("git", ["stash", "pop", stashRef], {
|
|
114
119
|
cwd: basePath,
|
|
@@ -116,7 +121,14 @@ export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
|
|
|
116
121
|
encoding: "utf-8",
|
|
117
122
|
env: GIT_NO_PROMPT_ENV,
|
|
118
123
|
});
|
|
119
|
-
|
|
124
|
+
const msg = `Restored stashed changes after milestone ${milestoneId} merge.`;
|
|
125
|
+
notify(msg, "info");
|
|
126
|
+
return {
|
|
127
|
+
restored: true,
|
|
128
|
+
needsManualRecovery: false,
|
|
129
|
+
message: msg,
|
|
130
|
+
stashRef,
|
|
131
|
+
};
|
|
120
132
|
}
|
|
121
133
|
catch (err) {
|
|
122
134
|
// Pop conflicts mean the merged code collides with the stashed changes.
|
|
@@ -127,5 +139,11 @@ export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
|
|
|
127
139
|
const msg = `git stash pop ${stashRef ?? ""}`.trim() + ` failed after merge of milestone ${milestoneId}: ${err instanceof Error ? err.message : String(err)}. ${restoreHint}`;
|
|
128
140
|
logWarning("preflight", msg);
|
|
129
141
|
notify(msg, "warning");
|
|
142
|
+
return {
|
|
143
|
+
restored: false,
|
|
144
|
+
needsManualRecovery: true,
|
|
145
|
+
message: msg,
|
|
146
|
+
...(stashRef ? { stashRef } : {}),
|
|
147
|
+
};
|
|
130
148
|
}
|
|
131
149
|
}
|
|
@@ -13,6 +13,7 @@ import { isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
|
13
13
|
import { gsdRoot } from "./paths.js";
|
|
14
14
|
import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
|
|
15
15
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
16
|
+
import { logWarning } from "./workflow-logger.js";
|
|
16
17
|
import { detectWorktreeName, } from "./worktree.js";
|
|
17
18
|
import { SLICE_BRANCH_RE, QUICK_BRANCH_RE, WORKFLOW_BRANCH_RE } from "./branch-patterns.js";
|
|
18
19
|
import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchExists, nativeHasChanges, nativeAddAllWithExclusions, nativeHasStagedChanges, nativeCommit, nativeRmCached, nativeUpdateRef, nativeAddPaths, nativeResetSoft, nativeCommitSubject, _resetHasChangesCache, } from "./native-git-bridge.js";
|
|
@@ -535,14 +536,45 @@ export class GitServiceImpl {
|
|
|
535
536
|
if (keyFiles.length === 0)
|
|
536
537
|
return false;
|
|
537
538
|
const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...extraExclusions];
|
|
538
|
-
const
|
|
539
|
+
const normalized = keyFiles
|
|
539
540
|
.map(file => normalizeRepoRelativePath(this.basePath, file))
|
|
540
541
|
.filter((file) => file !== null)
|
|
541
|
-
.filter(file => !isExcludedScopedPath(file, allExclusions))
|
|
542
|
+
.filter(file => !isExcludedScopedPath(file, allExclusions));
|
|
543
|
+
// Drop entries that don't exist on disk. The LLM occasionally lists files
|
|
544
|
+
// it intended to write but didn't (or names them with wrong casing/path).
|
|
545
|
+
// Pre-`b304f738b` `git add -A` swallowed these silently; the scoped
|
|
546
|
+
// pathspec form passes each path explicitly, so a single bad entry made
|
|
547
|
+
// the whole commit fail (see #5500). Filter so valid paths still commit.
|
|
548
|
+
const missing = [];
|
|
549
|
+
const existing = [];
|
|
550
|
+
for (const path of normalized) {
|
|
551
|
+
if (existsSync(join(this.basePath, path))) {
|
|
552
|
+
existing.push(path);
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
missing.push(path);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (missing.length > 0) {
|
|
559
|
+
logWarning("engine", `scoped stage: dropping ${missing.length} non-existent keyFile(s) from task commit: ${missing.join(", ")}`, { file: "git-service.ts" });
|
|
560
|
+
}
|
|
561
|
+
const paths = Array.from(new Set(existing));
|
|
542
562
|
if (paths.length === 0)
|
|
543
563
|
return false;
|
|
544
|
-
|
|
545
|
-
|
|
564
|
+
try {
|
|
565
|
+
nativeAddPaths(this.basePath, paths);
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
// Defense-in-depth: even after existence filtering, libgit2/git can
|
|
570
|
+
// still reject paths (gitignore matches, case-only differences on
|
|
571
|
+
// case-insensitive FS, submodule boundaries). Returning false lets
|
|
572
|
+
// autoCommit fall through to smartStage so the commit still goes out
|
|
573
|
+
// — restoring the resilience the unscoped path used to provide.
|
|
574
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
575
|
+
logWarning("engine", `scoped stage failed (${msg}); falling back to smartStage`, { file: "git-service.ts" });
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
546
578
|
}
|
|
547
579
|
/** Tracks whether runtime file cleanup has run this session. */
|
|
548
580
|
_runtimeFilesCleanedUp = false;
|
|
@@ -328,6 +328,8 @@ function shouldValidateInputAsPath(raw) {
|
|
|
328
328
|
const trimmed = raw.trim();
|
|
329
329
|
if (!trimmed)
|
|
330
330
|
return false;
|
|
331
|
+
if (isRuntimeOnlyInput(trimmed))
|
|
332
|
+
return false;
|
|
331
333
|
const candidate = extractPathFromAnnotation(trimmed);
|
|
332
334
|
if (!candidate)
|
|
333
335
|
return false;
|
|
@@ -349,6 +351,9 @@ function shouldValidateInputAsPath(raw) {
|
|
|
349
351
|
/[\\/]/.test(candidate) ||
|
|
350
352
|
/[*?[\]{}]/.test(candidate));
|
|
351
353
|
}
|
|
354
|
+
function isRuntimeOnlyInput(raw) {
|
|
355
|
+
return /\(\s*runtime\s*\)/i.test(raw);
|
|
356
|
+
}
|
|
352
357
|
function containsGlobPattern(candidate) {
|
|
353
358
|
return ["*", "?", "[", "]", "{", "}"].some((char) => candidate.includes(char));
|
|
354
359
|
}
|
|
@@ -461,6 +466,8 @@ export function checkTaskOrdering(tasks, basePath) {
|
|
|
461
466
|
const task = tasks[i];
|
|
462
467
|
const filesToCheck = [...task.inputs];
|
|
463
468
|
for (const file of filesToCheck) {
|
|
469
|
+
if (isRuntimeOnlyInput(file))
|
|
470
|
+
continue;
|
|
464
471
|
if (!shouldValidateInputAsPath(file))
|
|
465
472
|
continue;
|
|
466
473
|
const normalizedFile = normalizeFilePath(file);
|
|
@@ -6,6 +6,8 @@ You are executing GSD auto-mode.
|
|
|
6
6
|
|
|
7
7
|
Your working directory is `{{workingDirectory}}`. All file reads, writes, and shell commands MUST operate relative to this directory. Do NOT `cd` to any other directory.
|
|
8
8
|
|
|
9
|
+
If any inlined plan, summary, verification command, or prior artifact names an absolute path outside `{{workingDirectory}}`, treat that path as stale context. Convert it to the equivalent relative path under `{{workingDirectory}}` before reading, writing, or executing. If no equivalent path exists under `{{workingDirectory}}`, record a verification failure and stop; do not edit or run commands in another checkout.
|
|
10
|
+
|
|
9
11
|
## Mission
|
|
10
12
|
|
|
11
13
|
All slices are complete. Verify the integrated work, persist milestone completion, refresh project state, and write the final record future milestones will rely on.
|
|
@@ -22,7 +22,7 @@ import { emitWorktreeCreated, emitWorktreeMerged } from "./worktree-telemetry.js
|
|
|
22
22
|
import { getCollapseCadence, getMilestoneResquash, resquashMilestoneOnMain } from "./slice-cadence.js";
|
|
23
23
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
24
24
|
import { resolveWorktreeProjectRoot, normalizeWorktreePathForCompare } from "./worktree-root.js";
|
|
25
|
-
import { claimMilestoneLease, releaseMilestoneLease } from "./db/milestone-leases.js";
|
|
25
|
+
import { claimMilestoneLease, refreshMilestoneLease, releaseMilestoneLease } from "./db/milestone-leases.js";
|
|
26
26
|
// ─── Path Comparison Helper ────────────────────────────────────────────────
|
|
27
27
|
/**
|
|
28
28
|
* Compare two paths for physical identity, tolerating trailing slashes,
|
|
@@ -119,24 +119,40 @@ export class WorktreeResolver {
|
|
|
119
119
|
// milestone (re-entry within the same session).
|
|
120
120
|
if (this.s.workerId) {
|
|
121
121
|
if (this.s.currentMilestoneId === milestoneId && this.s.milestoneLeaseToken !== null) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
122
|
+
const refreshed = refreshMilestoneLease(this.s.workerId, milestoneId, this.s.milestoneLeaseToken);
|
|
123
|
+
if (refreshed) {
|
|
124
|
+
debugLog("WorktreeResolver", {
|
|
125
|
+
action: "enterMilestone",
|
|
126
|
+
milestoneId,
|
|
127
|
+
leaseRefreshed: true,
|
|
128
|
+
fencingToken: this.s.milestoneLeaseToken,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
debugLog("WorktreeResolver", {
|
|
133
|
+
action: "enterMilestone",
|
|
134
|
+
milestoneId,
|
|
135
|
+
staleLeaseToken: this.s.milestoneLeaseToken,
|
|
136
|
+
});
|
|
138
137
|
this.s.milestoneLeaseToken = null;
|
|
139
138
|
}
|
|
139
|
+
}
|
|
140
|
+
// If we held a different milestone, release it first so other
|
|
141
|
+
// workers don't have to wait for TTL.
|
|
142
|
+
if (this.s.currentMilestoneId && this.s.currentMilestoneId !== milestoneId && this.s.milestoneLeaseToken !== null) {
|
|
143
|
+
try {
|
|
144
|
+
releaseMilestoneLease(this.s.workerId, this.s.currentMilestoneId, this.s.milestoneLeaseToken);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
debugLog("WorktreeResolver", {
|
|
148
|
+
action: "enterMilestone",
|
|
149
|
+
milestoneId,
|
|
150
|
+
releasePriorLeaseError: err instanceof Error ? err.message : String(err),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
this.s.milestoneLeaseToken = null;
|
|
154
|
+
}
|
|
155
|
+
if (this.s.milestoneLeaseToken === null) {
|
|
140
156
|
try {
|
|
141
157
|
const claim = claimMilestoneLease(this.s.workerId, milestoneId);
|
|
142
158
|
if (claim.ok) {
|