gsd-pi 2.82.0-dev.dfbc5f58f → 2.82.0-dev.e7a7f1ed5
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/README.md +1 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +73 -30
- package/dist/resources/extensions/gsd/auto-dashboard.js +66 -1
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +1 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +10 -16
- package/dist/resources/extensions/gsd/auto-recovery.js +40 -13
- package/dist/resources/extensions/gsd/auto-start.js +3 -3
- package/dist/resources/extensions/gsd/auto-verification.js +17 -4
- package/dist/resources/extensions/gsd/auto-worktree.js +65 -9
- package/dist/resources/extensions/gsd/auto.js +7 -2
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +27 -6
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -2
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +7 -2
- package/dist/resources/extensions/gsd/crash-recovery.js +16 -4
- package/dist/resources/extensions/gsd/db/milestone-leases.js +24 -0
- package/dist/resources/extensions/gsd/doctor-git-checks.js +46 -1
- package/dist/resources/extensions/gsd/git-service.js +6 -2
- package/dist/resources/extensions/gsd/gsd-db.js +20 -6
- package/dist/resources/extensions/gsd/guided-flow-queue.js +4 -3
- package/dist/resources/extensions/gsd/guided-flow.js +95 -116
- package/dist/resources/extensions/gsd/guided-unit-context.js +23 -0
- package/dist/resources/extensions/gsd/migration-auto-check.js +12 -17
- package/dist/resources/extensions/gsd/pending-auto-start.js +52 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -8
- package/dist/resources/extensions/gsd/prompts/discuss.md +9 -9
- package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +4 -4
- package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +3 -3
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +4 -4
- package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
- package/dist/resources/extensions/gsd/queue-reorder-ui.js +30 -13
- package/dist/resources/extensions/gsd/smart-entry-routing.js +36 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +9 -14
- package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +19 -24
- package/dist/resources/extensions/gsd/status-guards.js +7 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +17 -1
- 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 +9 -9
- 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/api/browse-directories/route.js +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 +9 -9
- 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-ai/dist/providers/google-gemini-cli.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-gemini-cli.js +5 -0
- package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/google-gemini-cli.test.js +41 -0
- package/packages/pi-ai/dist/providers/google-gemini-cli.test.js.map +1 -0
- package/packages/pi-ai/src/providers/google-gemini-cli.test.ts +49 -0
- package/packages/pi-ai/src/providers/google-gemini-cli.ts +7 -0
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +24 -6
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +23 -7
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/__tests__/terminal.test.d.ts +2 -0
- package/packages/pi-tui/dist/__tests__/terminal.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/__tests__/terminal.test.js +103 -0
- package/packages/pi-tui/dist/__tests__/terminal.test.js.map +1 -0
- package/packages/pi-tui/dist/terminal.d.ts +2 -0
- package/packages/pi-tui/dist/terminal.d.ts.map +1 -1
- package/packages/pi-tui/dist/terminal.js +12 -0
- package/packages/pi-tui/dist/terminal.js.map +1 -1
- package/packages/pi-tui/src/__tests__/terminal.test.ts +121 -0
- package/packages/pi-tui/src/terminal.ts +11 -0
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +1 -1
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +9 -0
- package/src/resources/extensions/gsd/auto/phases.ts +83 -37
- package/src/resources/extensions/gsd/auto-dashboard.ts +72 -1
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +10 -16
- package/src/resources/extensions/gsd/auto-recovery.ts +45 -11
- package/src/resources/extensions/gsd/auto-start.ts +2 -3
- package/src/resources/extensions/gsd/auto-verification.ts +22 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +74 -9
- package/src/resources/extensions/gsd/auto.ts +8 -2
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +36 -6
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -2
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +8 -3
- package/src/resources/extensions/gsd/crash-recovery.ts +16 -2
- package/src/resources/extensions/gsd/db/milestone-leases.ts +26 -0
- package/src/resources/extensions/gsd/doctor-git-checks.ts +45 -1
- package/src/resources/extensions/gsd/doctor-types.ts +1 -0
- package/src/resources/extensions/gsd/git-service.ts +6 -3
- package/src/resources/extensions/gsd/gsd-db.ts +18 -6
- package/src/resources/extensions/gsd/guided-flow-queue.ts +4 -3
- package/src/resources/extensions/gsd/guided-flow.ts +128 -133
- package/src/resources/extensions/gsd/guided-unit-context.ts +30 -0
- package/src/resources/extensions/gsd/migration-auto-check.ts +15 -23
- package/src/resources/extensions/gsd/pending-auto-start.ts +79 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -8
- package/src/resources/extensions/gsd/prompts/discuss.md +9 -9
- package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +4 -4
- package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +3 -3
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +4 -4
- package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +31 -13
- package/src/resources/extensions/gsd/smart-entry-routing.ts +77 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +12 -15
- package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +17 -25
- package/src/resources/extensions/gsd/status-guards.ts +8 -0
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +29 -1
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +53 -2
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +76 -5
- package/src/resources/extensions/gsd/tests/auto-stop-notification.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/checkout-branch-stash-guard.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/clear-stale-autostart.test.ts +11 -2
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +5 -9
- package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/db-authority-regression.test.ts +208 -0
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/doctor-empty-worktree.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +11 -0
- package/src/resources/extensions/gsd/tests/guided-discuss-project-prompt-rendering.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/guided-dispatch-root.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +59 -11
- package/src/resources/extensions/gsd/tests/guided-tool-contract.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/headless-milestone-parity.test.ts +7 -7
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +46 -0
- package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +179 -0
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +26 -18
- package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +29 -5
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +37 -1
- package/src/resources/extensions/gsd/tests/queue-reorder-ui.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/run-uat-replay-cap.test.ts +2 -3
- package/src/resources/extensions/gsd/tests/smart-entry-routing.test.ts +113 -0
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +22 -1
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +119 -23
- package/src/resources/extensions/gsd/tests/status-guards.test.ts +13 -1
- package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +29 -2
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +18 -0
- package/src/resources/extensions/gsd/workflow-mcp.ts +18 -1
- /package/dist/web/standalone/.next/static/{q0WYuDVbHeFFYbdd-fei2 → 4dSwdrs__8NwCZggxP9KF}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{q0WYuDVbHeFFYbdd-fei2 → 4dSwdrs__8NwCZggxP9KF}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -503,7 +503,7 @@ On first run, GSD launches a branded setup wizard that walks you through LLM pro
|
|
|
503
503
|
| `/gsd mcp` | MCP server status and connectivity |
|
|
504
504
|
| `/gsd status` | Progress dashboard |
|
|
505
505
|
| `/gsd brief <mode>` | Generate a visual HTML brief (diagram, plan, diff, recap, table, slides) |
|
|
506
|
-
| `/gsd queue` | Queue future milestones (safe during auto mode)
|
|
506
|
+
| `/gsd queue` | Queue/reorder future milestones (`pending`, `queued`, or legacy `planned`; safe during auto mode) |
|
|
507
507
|
| `/gsd prefs` | Model selection, timeouts, budget ceiling |
|
|
508
508
|
| `/gsd migrate` | Migrate a v1 `.planning` directory to `.gsd` format |
|
|
509
509
|
| `/gsd help` | Categorized command reference for all GSD subcommands |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
68e342bd44fe9191
|
|
@@ -269,7 +269,7 @@ function makeErrorMessage(model, errorMsg) {
|
|
|
269
269
|
export function isClaudeCodeAbortErrorMessage(message) {
|
|
270
270
|
if (!message)
|
|
271
271
|
return false;
|
|
272
|
-
return /\b(?:claude code process aborted by user|request aborted by user|process aborted by user)\b/i.test(message);
|
|
272
|
+
return /\b(?:claude code process aborted by user|request aborted by user|process aborted by user|aborterror)\b/i.test(message);
|
|
273
273
|
}
|
|
274
274
|
function isBareClaudeCodeAbortErrorMessage(message) {
|
|
275
275
|
if (!message)
|
|
@@ -44,6 +44,7 @@ import { resolveManifest } from "../unit-context-manifest.js";
|
|
|
44
44
|
import { createWorktreeSafetyModule } from "../worktree-safety.js";
|
|
45
45
|
import { isSuspiciousGhostCompletion } from "../auto-unit-closeout.js";
|
|
46
46
|
import { decideVerificationRetry, verificationRetryKey } from "./verification-retry-policy.js";
|
|
47
|
+
import { buildPhaseHandoffOutcome, setAutoOutcomeWidget } from "../auto-dashboard.js";
|
|
47
48
|
// ─── Path Comparison Helper ───────────────────────────────────────────────
|
|
48
49
|
/** Compare two paths for physical identity, tolerating trailing slashes and symlinks. */
|
|
49
50
|
function isSamePathLocal(a, b) {
|
|
@@ -103,6 +104,12 @@ function unitWritesSource(unitType) {
|
|
|
103
104
|
function formatWorktreeSafetyFailure(result) {
|
|
104
105
|
return `Worktree Safety failed (${result.kind}): ${result.reason} ${result.remediation}`;
|
|
105
106
|
}
|
|
107
|
+
function formatWorktreeSafetyStopReason(result) {
|
|
108
|
+
if (result.kind === "empty-worktree-with-project-content") {
|
|
109
|
+
return `Worktree Safety failed (${result.kind}). Run /gsd doctor fix, then /gsd auto.`;
|
|
110
|
+
}
|
|
111
|
+
return `Worktree Safety failed (${result.kind}).`;
|
|
112
|
+
}
|
|
106
113
|
function resolveEmptyWorktreeWithProjectContent(unitRoot, projectRoot) {
|
|
107
114
|
if (isSamePathLocal(unitRoot, projectRoot))
|
|
108
115
|
return false;
|
|
@@ -176,7 +183,7 @@ async function validateSourceWriteWorktreeSafety(ic, unitType, unitId, milestone
|
|
|
176
183
|
projectRoot,
|
|
177
184
|
});
|
|
178
185
|
ctx.ui.notify(msg, "error");
|
|
179
|
-
await deps.stopAuto(ctx, pi,
|
|
186
|
+
await deps.stopAuto(ctx, pi, formatWorktreeSafetyStopReason(result));
|
|
180
187
|
return { action: "break", reason: result.kind };
|
|
181
188
|
}
|
|
182
189
|
// ─── Session timeout auto-resume state ────────────────────────────────────────
|
|
@@ -1331,35 +1338,8 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
1331
1338
|
s.currentUnit.type === unitType &&
|
|
1332
1339
|
s.currentUnit.id === unitId);
|
|
1333
1340
|
const previousTier = s.currentUnitRouting?.tier;
|
|
1334
|
-
// Scope workflow-logger buffer to this unit so post-finalize drains are
|
|
1335
|
-
// per-unit. Without this, the module-level _buffer accumulates across every
|
|
1336
|
-
// unit in the same Node process (see workflow-logger.ts module header).
|
|
1337
|
-
_resetLogs();
|
|
1338
1341
|
const dispatchKey = `${unitType}/${unitId}`;
|
|
1339
|
-
|
|
1340
|
-
s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
1341
|
-
s.lastGitActionFailure = null;
|
|
1342
|
-
s.lastGitActionStatus = null;
|
|
1343
|
-
s.lastUnitAgentEndMessages = null;
|
|
1344
|
-
setCurrentPhase(unitType, {
|
|
1345
|
-
basePath: s.basePath,
|
|
1346
|
-
traceId: ic.flowId,
|
|
1347
|
-
turnId: `iter-${ic.iteration}`,
|
|
1348
|
-
causedBy: "unit-start",
|
|
1349
|
-
});
|
|
1350
|
-
s.lastToolInvocationError = null; // #2883: clear stale error from previous unit
|
|
1351
|
-
const unitStartSeq = ic.nextSeq();
|
|
1352
|
-
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } });
|
|
1353
|
-
deps.captureAvailableSkills();
|
|
1354
|
-
writeUnitRuntimeRecord(s.basePath, unitType, unitId, s.currentUnit.startedAt, {
|
|
1355
|
-
phase: "dispatched",
|
|
1356
|
-
wrapupWarningSent: false,
|
|
1357
|
-
timeoutAt: null,
|
|
1358
|
-
lastProgressAt: s.currentUnit.startedAt,
|
|
1359
|
-
progressCount: 0,
|
|
1360
|
-
lastProgressKind: "dispatch",
|
|
1361
|
-
recoveryAttempts: 0, // Reset so re-dispatched units get full recovery budget (#2322)
|
|
1362
|
-
});
|
|
1342
|
+
const nextDispatchCount = (s.unitDispatchCount.get(dispatchKey) ?? 0) + 1;
|
|
1363
1343
|
// Status bar (widget + preconditions deferred until after model selection — see #2899)
|
|
1364
1344
|
ctx.ui.setStatus("gsd-auto", "auto");
|
|
1365
1345
|
if (mid)
|
|
@@ -1413,7 +1393,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
1413
1393
|
finalPrompt = `${capped}\n\n---\n\n${finalPrompt}`;
|
|
1414
1394
|
s.pendingCrashRecovery = null;
|
|
1415
1395
|
}
|
|
1416
|
-
else if (
|
|
1396
|
+
else if (nextDispatchCount > 1) {
|
|
1417
1397
|
const diagnostic = deps.getDeepDiagnostic(s.basePath);
|
|
1418
1398
|
if (diagnostic) {
|
|
1419
1399
|
const cappedDiag = diagnostic.length > MAX_RECOVERY_CHARS
|
|
@@ -1452,6 +1432,11 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
1452
1432
|
logWarning("engine", "Prompt reorder failed", { error: msg });
|
|
1453
1433
|
}
|
|
1454
1434
|
// Select and apply model (with tier escalation on retry — normal units only)
|
|
1435
|
+
const prevUnitRouting = s.currentUnitRouting;
|
|
1436
|
+
const prevUnitModel = s.currentUnitModel;
|
|
1437
|
+
const prevDispatchedModelId = s.currentDispatchedModelId;
|
|
1438
|
+
const prevSessionModel = ctx.model;
|
|
1439
|
+
const prevSessionThinkingLevel = pi.getThinkingLevel();
|
|
1455
1440
|
const modelResult = await deps.selectAndApplyModel(ctx, pi, unitType, unitId, s.basePath, prefs, s.verbose, s.autoModeStartModel, sidecarItem ? undefined : { isRetry, previousTier }, undefined, s.manualSessionModelOverride, s.autoModeStartThinkingLevel);
|
|
1456
1441
|
s.currentUnitRouting =
|
|
1457
1442
|
modelResult.routing;
|
|
@@ -1495,12 +1480,58 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
1495
1480
|
? ctx.modelRegistry.getProviderAuthMode(ctx.model.provider)
|
|
1496
1481
|
: undefined,
|
|
1497
1482
|
baseUrl: s.currentUnitModel?.baseUrl ?? ctx.model?.baseUrl,
|
|
1483
|
+
activeTools: typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [],
|
|
1498
1484
|
});
|
|
1499
1485
|
if (compatibilityError) {
|
|
1486
|
+
s.currentUnitRouting = prevUnitRouting;
|
|
1487
|
+
s.currentUnitModel = prevUnitModel;
|
|
1488
|
+
s.currentDispatchedModelId = prevDispatchedModelId;
|
|
1489
|
+
if (s.checkpointSha) {
|
|
1490
|
+
cleanupCheckpoint(s.basePath, unitId);
|
|
1491
|
+
s.checkpointSha = null;
|
|
1492
|
+
}
|
|
1493
|
+
if (prevSessionModel) {
|
|
1494
|
+
const ok = await pi.setModel(prevSessionModel, { persist: false });
|
|
1495
|
+
if (!ok) {
|
|
1496
|
+
ctx.ui.notify("Failed to restore previous session model after compatibility check failure.", "warning");
|
|
1497
|
+
}
|
|
1498
|
+
if (prevSessionThinkingLevel) {
|
|
1499
|
+
pi.setThinkingLevel(prevSessionThinkingLevel);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1500
1502
|
ctx.ui.notify(compatibilityError, "error");
|
|
1501
1503
|
await deps.stopAuto(ctx, pi, compatibilityError);
|
|
1502
1504
|
return { action: "break", reason: "workflow-capability" };
|
|
1503
1505
|
}
|
|
1506
|
+
// Scope workflow-logger buffer to this unit so post-finalize drains are
|
|
1507
|
+
// per-unit. Without this, the module-level _buffer accumulates across every
|
|
1508
|
+
// unit in the same Node process (see workflow-logger.ts module header).
|
|
1509
|
+
_resetLogs();
|
|
1510
|
+
const unitStartedAt = Date.now();
|
|
1511
|
+
s.unitDispatchCount.set(dispatchKey, nextDispatchCount);
|
|
1512
|
+
s.currentUnit = { type: unitType, id: unitId, startedAt: unitStartedAt };
|
|
1513
|
+
s.lastGitActionFailure = null;
|
|
1514
|
+
s.lastGitActionStatus = null;
|
|
1515
|
+
s.lastUnitAgentEndMessages = null;
|
|
1516
|
+
setCurrentPhase(unitType, {
|
|
1517
|
+
basePath: s.basePath,
|
|
1518
|
+
traceId: ic.flowId,
|
|
1519
|
+
turnId: `iter-${ic.iteration}`,
|
|
1520
|
+
causedBy: "unit-start",
|
|
1521
|
+
});
|
|
1522
|
+
s.lastToolInvocationError = null; // #2883: clear stale error from previous unit
|
|
1523
|
+
const unitStartSeq = ic.nextSeq();
|
|
1524
|
+
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } });
|
|
1525
|
+
deps.captureAvailableSkills();
|
|
1526
|
+
writeUnitRuntimeRecord(s.basePath, unitType, unitId, unitStartedAt, {
|
|
1527
|
+
phase: "dispatched",
|
|
1528
|
+
wrapupWarningSent: false,
|
|
1529
|
+
timeoutAt: null,
|
|
1530
|
+
lastProgressAt: unitStartedAt,
|
|
1531
|
+
progressCount: 0,
|
|
1532
|
+
lastProgressKind: "dispatch",
|
|
1533
|
+
recoveryAttempts: 0, // Reset so re-dispatched units get full recovery budget (#2322)
|
|
1534
|
+
});
|
|
1504
1535
|
// Progress widget + preconditions — deferred to after model selection so the
|
|
1505
1536
|
// widget's first render tick shows the correct model (#2899).
|
|
1506
1537
|
deps.updateProgressWidget(ctx, unitType, unitId, state);
|
|
@@ -1930,6 +1961,18 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
|
|
|
1930
1961
|
lastProgressAt: Date.now(),
|
|
1931
1962
|
lastProgressKind: "finalize-success",
|
|
1932
1963
|
});
|
|
1964
|
+
if (!preUnitSnapshot.type.startsWith("hook/") &&
|
|
1965
|
+
preUnitSnapshot.type !== "custom-step" &&
|
|
1966
|
+
preUnitSnapshot.type !== "complete-milestone") {
|
|
1967
|
+
setAutoOutcomeWidget(ctx, {
|
|
1968
|
+
...buildPhaseHandoffOutcome({
|
|
1969
|
+
unitType: preUnitSnapshot.type,
|
|
1970
|
+
unitId: preUnitSnapshot.id,
|
|
1971
|
+
agentEndMessages: s.lastUnitAgentEndMessages,
|
|
1972
|
+
}),
|
|
1973
|
+
startedAt: s.autoStartTime,
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1933
1976
|
}
|
|
1934
1977
|
s.currentUnit = null;
|
|
1935
1978
|
clearCurrentPhase();
|
|
@@ -27,6 +27,19 @@ export function extractUatSliceId(unitId) {
|
|
|
27
27
|
return slice;
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
|
+
export function buildPhaseHandoffOutcome(input) {
|
|
31
|
+
const phase = unitPhaseLabel(input.unitType);
|
|
32
|
+
const detail = extractLastAssistantSummary(input.agentEndMessages) ??
|
|
33
|
+
`Completed ${unitVerb(input.unitType)} ${input.unitId}.`;
|
|
34
|
+
return {
|
|
35
|
+
status: "complete",
|
|
36
|
+
title: `${phase} complete`,
|
|
37
|
+
detail,
|
|
38
|
+
unitLabel: `${unitVerb(input.unitType)} ${input.unitId}`,
|
|
39
|
+
nextAction: "Preparing the next phase. Review this handoff while the next session starts.",
|
|
40
|
+
commands: ["/gsd status for overview", "/gsd visualize to inspect", "/gsd notifications for history"],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
30
43
|
// ─── Unit Description Helpers ─────────────────────────────────────────────────
|
|
31
44
|
export function unitVerb(unitType) {
|
|
32
45
|
if (unitType.startsWith("hook/"))
|
|
@@ -464,7 +477,6 @@ export function _resetWidgetModeForTests() {
|
|
|
464
477
|
export function updateProgressWidget(ctx, unitType, unitId, state, accessors, tierBadge) {
|
|
465
478
|
if (!ctx.hasUI)
|
|
466
479
|
return;
|
|
467
|
-
ctx.ui.setWidget("gsd-outcome", undefined);
|
|
468
480
|
// Welcome header is a startup-only banner — permanently suppress it once
|
|
469
481
|
// auto-mode activates. The dashboard widget owns all status from here.
|
|
470
482
|
// Note: setHeader(undefined) restores the built-in header (logo +
|
|
@@ -927,3 +939,56 @@ function normalizeRollupText(value) {
|
|
|
927
939
|
return null;
|
|
928
940
|
return clean;
|
|
929
941
|
}
|
|
942
|
+
function isAssistantMessage(value) {
|
|
943
|
+
if (!value || typeof value !== "object")
|
|
944
|
+
return false;
|
|
945
|
+
const record = value;
|
|
946
|
+
if (record.role === "assistant")
|
|
947
|
+
return true;
|
|
948
|
+
const message = record.message;
|
|
949
|
+
if (message && typeof message === "object") {
|
|
950
|
+
return message.role === "assistant";
|
|
951
|
+
}
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
function extractLastAssistantSummary(messages) {
|
|
955
|
+
if (!messages || messages.length === 0)
|
|
956
|
+
return null;
|
|
957
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
958
|
+
if (!isAssistantMessage(messages[i]))
|
|
959
|
+
continue;
|
|
960
|
+
const text = extractMessageText(messages[i]);
|
|
961
|
+
const clean = normalizeRollupText(text);
|
|
962
|
+
if (clean)
|
|
963
|
+
return truncateToWidth(clean, 220, "…");
|
|
964
|
+
}
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
function extractMessageText(value) {
|
|
968
|
+
if (typeof value === "string")
|
|
969
|
+
return value;
|
|
970
|
+
if (!value || typeof value !== "object")
|
|
971
|
+
return null;
|
|
972
|
+
const record = value;
|
|
973
|
+
if (typeof record.content === "string")
|
|
974
|
+
return record.content;
|
|
975
|
+
const message = record.message;
|
|
976
|
+
if (message && typeof message === "object") {
|
|
977
|
+
return extractMessageText(message);
|
|
978
|
+
}
|
|
979
|
+
const content = record.content;
|
|
980
|
+
if (Array.isArray(content)) {
|
|
981
|
+
const parts = content
|
|
982
|
+
.map((part) => {
|
|
983
|
+
if (typeof part === "string")
|
|
984
|
+
return part;
|
|
985
|
+
if (!part || typeof part !== "object")
|
|
986
|
+
return "";
|
|
987
|
+
const partRecord = part;
|
|
988
|
+
return typeof partRecord.text === "string" ? partRecord.text : "";
|
|
989
|
+
})
|
|
990
|
+
.filter(Boolean);
|
|
991
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
992
|
+
}
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
@@ -224,6 +224,7 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
|
|
|
224
224
|
unitType,
|
|
225
225
|
authMode: ctx.model?.provider ? ctx.modelRegistry.getProviderAuthMode(ctx.model.provider) : undefined,
|
|
226
226
|
baseUrl: ctx.model?.baseUrl,
|
|
227
|
+
activeTools: typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [],
|
|
227
228
|
});
|
|
228
229
|
if (compatibilityError) {
|
|
229
230
|
ctx.ui.notify(compatibilityError, "error");
|
|
@@ -299,9 +299,7 @@ export const DISPATCH_RULES = [
|
|
|
299
299
|
const attempts = incrementUatCount(basePath, mid, sliceId);
|
|
300
300
|
if (attempts > MAX_UAT_ATTEMPTS) {
|
|
301
301
|
return {
|
|
302
|
-
action: "
|
|
303
|
-
reason: `run-uat for ${mid}/${sliceId} has been dispatched ${attempts - 1} times without producing a verdict. Verification commands may be broken — fix the UAT spec or manually write an ASSESSMENT verdict.`,
|
|
304
|
-
level: "warning",
|
|
302
|
+
action: "skip",
|
|
305
303
|
};
|
|
306
304
|
}
|
|
307
305
|
const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT");
|
|
@@ -1098,16 +1096,16 @@ export const DISPATCH_RULES = [
|
|
|
1098
1096
|
return { action: "skip" };
|
|
1099
1097
|
}
|
|
1100
1098
|
}
|
|
1101
|
-
// Safety guard (#2675, #5747): block completion when VALIDATION
|
|
1102
|
-
// verdict is
|
|
1103
|
-
// terminal, but completing-milestone should NOT proceed —
|
|
1104
|
-
// or human attention is needed.
|
|
1099
|
+
// Safety guard (#2675, #5747, #5920): block completion when VALIDATION
|
|
1100
|
+
// verdict is anything other than pass. The state machine treats these
|
|
1101
|
+
// verdicts as terminal, but completing-milestone should NOT proceed —
|
|
1102
|
+
// remediation or human attention is needed.
|
|
1105
1103
|
const validationFile = resolveMilestoneFile(basePath, mid, "VALIDATION");
|
|
1106
1104
|
if (validationFile) {
|
|
1107
1105
|
const validationContent = await loadFile(validationFile);
|
|
1108
1106
|
if (validationContent) {
|
|
1109
1107
|
const verdict = extractVerdict(validationContent);
|
|
1110
|
-
if (verdict
|
|
1108
|
+
if (verdict !== "pass") {
|
|
1111
1109
|
return {
|
|
1112
1110
|
action: "stop",
|
|
1113
1111
|
reason: `Cannot complete milestone ${mid}: VALIDATION verdict is "${verdict}". Address the validation findings and re-run validation, or update the verdict manually.`,
|
|
@@ -1125,16 +1123,12 @@ export const DISPATCH_RULES = [
|
|
|
1125
1123
|
level: "error",
|
|
1126
1124
|
};
|
|
1127
1125
|
}
|
|
1128
|
-
// Safety
|
|
1129
|
-
// artifacts
|
|
1130
|
-
//
|
|
1126
|
+
// Safety signal (#1703, #5097): detect milestones with only .gsd/
|
|
1127
|
+
// artifacts. This no longer hard-blocks completion because some
|
|
1128
|
+
// milestones are intentionally planning/documentation-only.
|
|
1131
1129
|
const artifactCheck = hasImplementationArtifacts(basePath, mid);
|
|
1132
1130
|
if (artifactCheck === "absent") {
|
|
1133
|
-
|
|
1134
|
-
action: "stop",
|
|
1135
|
-
reason: `Cannot complete milestone ${mid}: no implementation files found outside .gsd/. The milestone has only plan files — actual code changes are required.`,
|
|
1136
|
-
level: "error",
|
|
1137
|
-
};
|
|
1131
|
+
logWarning("dispatch", `Milestone ${mid} has no implementation files outside .gsd/ — continuing complete-milestone dispatch (planning-only/documentation-only milestone).`);
|
|
1138
1132
|
}
|
|
1139
1133
|
if (artifactCheck === "unknown") {
|
|
1140
1134
|
logWarning("dispatch", `Implementation artifact check inconclusive for ${mid} — proceeding (git context unavailable)`);
|
|
@@ -162,9 +162,16 @@ export function hasImplementationArtifacts(basePath, milestoneId) {
|
|
|
162
162
|
// Strategy: check `git diff --name-only` against the merge-base with the
|
|
163
163
|
// main branch. This captures ALL files changed during the milestone's
|
|
164
164
|
// lifetime while running on a milestone branch.
|
|
165
|
-
const
|
|
166
|
-
? readIntegrationBranch(basePath, milestoneId)
|
|
167
|
-
:
|
|
165
|
+
const recordedIntegrationBranch = milestoneId
|
|
166
|
+
? readIntegrationBranch(basePath, milestoneId)
|
|
167
|
+
: null;
|
|
168
|
+
let integrationBranch;
|
|
169
|
+
if (recordedIntegrationBranch?.startsWith("milestone/")) {
|
|
170
|
+
integrationBranch = detectMainBranch(basePath);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
integrationBranch = recordedIntegrationBranch ?? detectMainBranch(basePath);
|
|
174
|
+
}
|
|
168
175
|
const currentBranch = getCurrentBranch(basePath);
|
|
169
176
|
const branchDiff = getChangedFilesSinceBranch(basePath, integrationBranch);
|
|
170
177
|
if (!branchDiff.ok)
|
|
@@ -496,29 +503,49 @@ function commitMatchesMilestone(basePath, message, milestoneId, files) {
|
|
|
496
503
|
// rather than Mxx/Sxx/Tyy. Bind those commits back to the milestone when
|
|
497
504
|
// either the commit touched this milestone's artifacts, or — for projects
|
|
498
505
|
// where .gsd/ is gitignored/external (#5033) — the message explicitly
|
|
499
|
-
// names the milestone
|
|
506
|
+
// names the milestone, local GSD state proves the task belongs here, or the
|
|
507
|
+
// commit is implementation-bearing evidence itself (#5100).
|
|
500
508
|
if (/^GSD-Task:\s*S[^/\s]+\/T\S+/m.test(message)) {
|
|
501
509
|
if (files.some((file) => isMilestoneArtifactPath(file, milestoneId)))
|
|
502
510
|
return true;
|
|
503
511
|
if (commitMessageMentionsMilestone(message, milestoneId))
|
|
504
512
|
return true;
|
|
505
|
-
|
|
513
|
+
const taskTrailerOwnership = getTaskOwnershipStatus(basePath, message, milestoneId);
|
|
514
|
+
if (taskTrailerOwnership === true)
|
|
515
|
+
return true;
|
|
516
|
+
if (taskTrailerOwnership === false)
|
|
517
|
+
return false;
|
|
518
|
+
// taskTrailerOwnership === null: unknown ownership. Apply fallback only
|
|
519
|
+
// in this case to avoid cross-milestone attribution.
|
|
520
|
+
if (MILESTONE_ID_RE.test(milestoneId) && classifyImplementationFiles(files) === "present")
|
|
506
521
|
return true;
|
|
507
522
|
}
|
|
508
523
|
return false;
|
|
509
524
|
}
|
|
510
|
-
|
|
525
|
+
/**
|
|
526
|
+
* Tri-state task ownership probe.
|
|
527
|
+
* true => DB or local files confirm this milestone owns the task.
|
|
528
|
+
* false => DB is available and this milestone is registered, but task is absent.
|
|
529
|
+
* null => ownership unknown (milestone not in DB yet, or no DB + no local files).
|
|
530
|
+
*/
|
|
531
|
+
function getTaskOwnershipStatus(basePath, message, milestoneId) {
|
|
511
532
|
const match = message.match(/^GSD-Task:\s*(S[^/\s]+)\/(T[^\s]+)/m);
|
|
512
533
|
if (!match)
|
|
513
|
-
return
|
|
534
|
+
return null;
|
|
514
535
|
const [, sliceId, taskId] = match;
|
|
515
|
-
if (
|
|
516
|
-
|
|
536
|
+
if (isDbAvailable()) {
|
|
537
|
+
if (!getMilestone(milestoneId))
|
|
538
|
+
return null;
|
|
539
|
+
return getTask(milestoneId, sliceId, taskId) ? true : false;
|
|
540
|
+
}
|
|
541
|
+
// DB unavailable: fallback to local task-file presence.
|
|
517
542
|
const tasksDir = resolveTasksDir(basePath, milestoneId, sliceId);
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
543
|
+
if (tasksDir
|
|
544
|
+
&& (existsSync(join(tasksDir, `${taskId}-PLAN.md`))
|
|
545
|
+
|| existsSync(join(tasksDir, `${taskId}-SUMMARY.md`)))) {
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
return null;
|
|
522
549
|
}
|
|
523
550
|
function commitMessageMentionsMilestone(message, milestoneId) {
|
|
524
551
|
if (!MILESTONE_ID_RE.test(milestoneId))
|
|
@@ -21,10 +21,10 @@ import { invalidateAllCaches } from "./cache.js";
|
|
|
21
21
|
import { writeLock, clearLock } from "./crash-recovery.js";
|
|
22
22
|
import { acquireSessionLock, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
|
|
23
23
|
import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
|
|
24
|
-
import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, nativeGetCurrentBranch, nativeDetectMainBranch,
|
|
24
|
+
import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, nativeGetCurrentBranch, nativeDetectMainBranch, nativeBranchList, nativeBranchExists, nativeBranchListMerged, nativeBranchDelete, nativeWorktreeRemove, nativeCommitCountBetween, } from "./native-git-bridge.js";
|
|
25
25
|
import { GitServiceImpl } from "./git-service.js";
|
|
26
26
|
import { captureIntegrationBranch, detectWorktreeName, setActiveMilestoneId, } from "./worktree.js";
|
|
27
|
-
import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
27
|
+
import { getAutoWorktreePath, checkoutBranchWithStashGuard } from "./auto-worktree.js";
|
|
28
28
|
import { readResourceVersion, cleanStaleRuntimeUnits } from "./auto-worktree.js";
|
|
29
29
|
import { worktreePath as getWorktreeDir, isInsideWorktreesDir } from "./worktree-manager.js";
|
|
30
30
|
import { emitWorktreeOrphaned } from "./worktree-telemetry.js";
|
|
@@ -901,7 +901,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
901
901
|
const integrationBranch = nativeDetectMainBranch(base);
|
|
902
902
|
const branchToCheckout = resolveIsolationNoneBranchCheckout(currentBranch, integrationBranch, isolationMode, isRepo);
|
|
903
903
|
if (branchToCheckout) {
|
|
904
|
-
|
|
904
|
+
checkoutBranchWithStashGuard(base, branchToCheckout, "isolation-none-recovery");
|
|
905
905
|
logWarning("bootstrap", `Returned to "${branchToCheckout}" — HEAD was on stale milestone branch "${currentBranch}" (isolation: none does not use milestone branches).`);
|
|
906
906
|
}
|
|
907
907
|
}
|
|
@@ -65,12 +65,25 @@ async function runValidateMilestonePostCheck(vctx, pauseAuto) {
|
|
|
65
65
|
const { milestone: mid } = parseUnitId(s.currentUnit.id);
|
|
66
66
|
if (!mid)
|
|
67
67
|
return "continue";
|
|
68
|
+
const setToolFailureRetry = (message) => {
|
|
69
|
+
const retryKey = verificationRetryKey(s.currentUnit.type, s.currentUnit.id);
|
|
70
|
+
const attempt = (s.verificationRetryCount.get(retryKey) ?? 0) + 1;
|
|
71
|
+
s.verificationRetryCount.set(retryKey, attempt);
|
|
72
|
+
s.pendingVerificationRetry = {
|
|
73
|
+
unitId: s.currentUnit.id,
|
|
74
|
+
failureContext: message,
|
|
75
|
+
attempt,
|
|
76
|
+
};
|
|
77
|
+
return "retry";
|
|
78
|
+
};
|
|
68
79
|
const validationFile = resolveMilestoneFile(s.basePath, mid, "VALIDATION");
|
|
69
|
-
if (!validationFile)
|
|
70
|
-
return "
|
|
80
|
+
if (!validationFile) {
|
|
81
|
+
return setToolFailureRetry("You must call gsd_validate_milestone to persist the validation results. No VALIDATION.md was created.");
|
|
82
|
+
}
|
|
71
83
|
const validationContent = await loadFile(validationFile);
|
|
72
|
-
if (!validationContent)
|
|
73
|
-
return "
|
|
84
|
+
if (!validationContent) {
|
|
85
|
+
return setToolFailureRetry("You must call gsd_validate_milestone to persist the validation results. VALIDATION.md exists but is empty.");
|
|
86
|
+
}
|
|
74
87
|
const verdict = extractVerdict(validationContent);
|
|
75
88
|
if (verdict !== "needs-remediation") {
|
|
76
89
|
await persistMilestoneValidationGate("pass", "none", `milestone validation verdict is ${verdict}; no remediation loop risk`, "", mid);
|
|
@@ -870,7 +870,63 @@ export function enterBranchModeForMilestone(basePath, milestoneId) {
|
|
|
870
870
|
reused: true,
|
|
871
871
|
});
|
|
872
872
|
}
|
|
873
|
-
|
|
873
|
+
checkoutBranchWithStashGuard(basePath, branch, `enter-branch-mode:${milestoneId}`);
|
|
874
|
+
}
|
|
875
|
+
export function checkoutBranchWithStashGuard(basePath, branch, reason) {
|
|
876
|
+
let stashMarker = null;
|
|
877
|
+
let stashed = false;
|
|
878
|
+
const status = nativeWorkingTreeStatus(basePath).trim();
|
|
879
|
+
if (status.length > 0) {
|
|
880
|
+
stashMarker = `gsd-checkout-stash:${reason}:${process.pid}:${Date.now()}:${process.hrtime.bigint().toString(36)}`;
|
|
881
|
+
const stashListBefore = execFileSync("git", ["stash", "list"], {
|
|
882
|
+
cwd: basePath,
|
|
883
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
884
|
+
encoding: "utf-8",
|
|
885
|
+
});
|
|
886
|
+
execFileSync("git", ["stash", "push", "--include-untracked", "-m", `gsd: checkout stash [${stashMarker}]`], {
|
|
887
|
+
cwd: basePath,
|
|
888
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
889
|
+
encoding: "utf-8",
|
|
890
|
+
});
|
|
891
|
+
const stashListAfter = execFileSync("git", ["stash", "list"], {
|
|
892
|
+
cwd: basePath,
|
|
893
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
894
|
+
encoding: "utf-8",
|
|
895
|
+
});
|
|
896
|
+
stashed = stashListAfter !== stashListBefore;
|
|
897
|
+
}
|
|
898
|
+
// Checkout and stash-restore are split so we can distinguish two failure
|
|
899
|
+
// modes: (a) checkout failed → HEAD did not move, restore stash and rethrow;
|
|
900
|
+
// (b) checkout succeeded but stash pop failed → HEAD moved to `branch` but
|
|
901
|
+
// the working-tree changes remain in the stash list. We surface a distinct
|
|
902
|
+
// error in case (b) so callers don't assume the branch switch was rolled back.
|
|
903
|
+
try {
|
|
904
|
+
nativeCheckoutBranch(basePath, branch);
|
|
905
|
+
}
|
|
906
|
+
catch (checkoutErr) {
|
|
907
|
+
if (stashed) {
|
|
908
|
+
try {
|
|
909
|
+
popStashByRef(basePath, stashMarker);
|
|
910
|
+
}
|
|
911
|
+
catch (restoreErr) {
|
|
912
|
+
logWarning("worktree", `git stash pop failed during checkout restore: ${restoreErr instanceof Error ? restoreErr.message : String(restoreErr)}`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
throw checkoutErr;
|
|
916
|
+
}
|
|
917
|
+
if (stashed) {
|
|
918
|
+
try {
|
|
919
|
+
popStashByRef(basePath, stashMarker);
|
|
920
|
+
}
|
|
921
|
+
catch (popErr) {
|
|
922
|
+
const msg = popErr instanceof Error ? popErr.message : String(popErr);
|
|
923
|
+
const wrapped = new Error(`checkout to '${branch}' succeeded but stash restore failed; working tree changes remain in the stash list. Original error: ${msg}`);
|
|
924
|
+
const ref = popErr?.stashRef;
|
|
925
|
+
if (ref)
|
|
926
|
+
wrapped.stashRef = ref;
|
|
927
|
+
throw wrapped;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
874
930
|
}
|
|
875
931
|
// ─── Public API ────────────────────────────────────────────────────────────
|
|
876
932
|
/**
|
|
@@ -1727,14 +1783,6 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
|
|
|
1727
1783
|
// report the dirty tree if it fails.
|
|
1728
1784
|
logWarning("worktree", `git stash failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1729
1785
|
}
|
|
1730
|
-
if (needsDbCycle && dbPathToReopen) {
|
|
1731
|
-
try {
|
|
1732
|
-
openDatabase(dbPathToReopen);
|
|
1733
|
-
}
|
|
1734
|
-
catch (err) {
|
|
1735
|
-
logWarning("worktree", `post-stash db reopen failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
1786
|
// 7b. Clean up stale merge state before attempting squash merge (#2912).
|
|
1739
1787
|
// A leftover MERGE_HEAD (from a previous failed merge, libgit2 native path,
|
|
1740
1788
|
// or interrupted operation) causes `git merge --squash` to refuse with
|
|
@@ -1743,6 +1791,14 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
|
|
|
1743
1791
|
removeMergeStateFiles(originalBasePath_, "pre-merge");
|
|
1744
1792
|
// 8. Squash merge — auto-resolve .gsd/ state file conflicts (#530)
|
|
1745
1793
|
const mergeResult = nativeMergeSquash(originalBasePath_, milestoneBranch);
|
|
1794
|
+
if (needsDbCycle && dbPathToReopen) {
|
|
1795
|
+
try {
|
|
1796
|
+
openDatabase(dbPathToReopen);
|
|
1797
|
+
}
|
|
1798
|
+
catch (err) {
|
|
1799
|
+
logWarning("worktree", `post-merge db reopen failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1746
1802
|
if (!mergeResult.success) {
|
|
1747
1803
|
// Dirty working tree — the merge was rejected before it started (e.g.
|
|
1748
1804
|
// untracked .gsd/ files left by syncStateToProjectRoot). Preserve the
|
|
@@ -127,6 +127,12 @@ import { normalizeRealPath } from "./paths.js";
|
|
|
127
127
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
128
128
|
/** Throttle STATE.md rebuilds — at most once per 30 seconds */
|
|
129
129
|
const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
|
|
130
|
+
export function formatAutoStopNotification(prefix, totals, unitCount) {
|
|
131
|
+
return [
|
|
132
|
+
`${prefix}.`,
|
|
133
|
+
`Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${unitCount} units`,
|
|
134
|
+
].join("\n");
|
|
135
|
+
}
|
|
130
136
|
/**
|
|
131
137
|
* Phase B — register this auto-mode process in the workers table so other
|
|
132
138
|
* workers and janitors can detect liveness via heartbeat. Best-effort: if
|
|
@@ -1030,7 +1036,7 @@ export async function stopAuto(ctx, pi, reason, options = {}) {
|
|
|
1030
1036
|
: `Auto-mode stopped${reasonSuffix}`;
|
|
1031
1037
|
if (ledger && ledger.units.length > 0) {
|
|
1032
1038
|
const totals = getProjectTotals(ledger.units);
|
|
1033
|
-
ctx?.ui.notify(
|
|
1039
|
+
ctx?.ui.notify(formatAutoStopNotification(notificationPrefix, totals, ledger.units.length), "info");
|
|
1034
1040
|
}
|
|
1035
1041
|
else {
|
|
1036
1042
|
ctx?.ui.notify(`${notificationPrefix}.`, "info");
|
|
@@ -1319,7 +1325,6 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
|
|
|
1319
1325
|
restoreProjectRootEnv();
|
|
1320
1326
|
restoreMilestoneLockEnv();
|
|
1321
1327
|
s.pendingVerificationRetry = null;
|
|
1322
|
-
s.verificationRetryCount.clear();
|
|
1323
1328
|
ctx?.ui.setStatus("gsd-auto", "paused");
|
|
1324
1329
|
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
1325
1330
|
const resumeCmd = s.stepMode ? "/gsd next" : "/gsd auto";
|