gsd-pi 2.82.0-dev.3709f22a5 → 2.82.0-dev.3a3c6509d
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 +53 -29
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +1 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +12 -18
- package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
- 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 +0 -1
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +6 -1
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +1 -1
- 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/forensics.js +3 -3
- 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 +8 -5
- package/dist/resources/extensions/gsd/queue-reorder-ui.js +30 -13
- package/dist/resources/extensions/gsd/state.js +1 -1
- package/dist/resources/extensions/gsd/status-guards.js +7 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +17 -1
- package/dist/resources/extensions/gsd/worktree-manager.js +1 -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/core/chat-controller-ordering.test.js +44 -3
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +71 -97
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js +12 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +19 -8
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +53 -3
- package/packages/pi-coding-agent/src/core/sdk.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +75 -102
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-ordering.test.ts +14 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +23 -8
- 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 +60 -36
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +12 -18
- package/src/resources/extensions/gsd/auto-post-unit.ts +1 -1
- 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 +0 -1
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +9 -1
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -1
- 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/forensics.ts +3 -3
- 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 +8 -5
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +31 -13
- package/src/resources/extensions/gsd/state.ts +1 -1
- package/src/resources/extensions/gsd/status-guards.ts +8 -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-recovery.test.ts +76 -5
- package/src/resources/extensions/gsd/tests/checkout-branch-stash-guard.test.ts +87 -0
- 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/dispatch-complete-milestone-guard.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +11 -0
- 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/pipeline-variant-dispatch.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +18 -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/status-guards.test.ts +13 -1
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +17 -0
- 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/src/resources/extensions/gsd/worktree-manager.ts +1 -1
- /package/dist/web/standalone/.next/static/{kkGf3_VaPFkiDNV_D7Dtl → O6femb9LLl3nlgsDaYwS-}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{kkGf3_VaPFkiDNV_D7Dtl → O6femb9LLl3nlgsDaYwS-}/_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
|
+
b9c1b29c0681f84f
|
|
@@ -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)
|
|
@@ -1338,35 +1338,8 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
1338
1338
|
s.currentUnit.type === unitType &&
|
|
1339
1339
|
s.currentUnit.id === unitId);
|
|
1340
1340
|
const previousTier = s.currentUnitRouting?.tier;
|
|
1341
|
-
// Scope workflow-logger buffer to this unit so post-finalize drains are
|
|
1342
|
-
// per-unit. Without this, the module-level _buffer accumulates across every
|
|
1343
|
-
// unit in the same Node process (see workflow-logger.ts module header).
|
|
1344
|
-
_resetLogs();
|
|
1345
1341
|
const dispatchKey = `${unitType}/${unitId}`;
|
|
1346
|
-
|
|
1347
|
-
s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
1348
|
-
s.lastGitActionFailure = null;
|
|
1349
|
-
s.lastGitActionStatus = null;
|
|
1350
|
-
s.lastUnitAgentEndMessages = null;
|
|
1351
|
-
setCurrentPhase(unitType, {
|
|
1352
|
-
basePath: s.basePath,
|
|
1353
|
-
traceId: ic.flowId,
|
|
1354
|
-
turnId: `iter-${ic.iteration}`,
|
|
1355
|
-
causedBy: "unit-start",
|
|
1356
|
-
});
|
|
1357
|
-
s.lastToolInvocationError = null; // #2883: clear stale error from previous unit
|
|
1358
|
-
const unitStartSeq = ic.nextSeq();
|
|
1359
|
-
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } });
|
|
1360
|
-
deps.captureAvailableSkills();
|
|
1361
|
-
writeUnitRuntimeRecord(s.basePath, unitType, unitId, s.currentUnit.startedAt, {
|
|
1362
|
-
phase: "dispatched",
|
|
1363
|
-
wrapupWarningSent: false,
|
|
1364
|
-
timeoutAt: null,
|
|
1365
|
-
lastProgressAt: s.currentUnit.startedAt,
|
|
1366
|
-
progressCount: 0,
|
|
1367
|
-
lastProgressKind: "dispatch",
|
|
1368
|
-
recoveryAttempts: 0, // Reset so re-dispatched units get full recovery budget (#2322)
|
|
1369
|
-
});
|
|
1342
|
+
const nextDispatchCount = (s.unitDispatchCount.get(dispatchKey) ?? 0) + 1;
|
|
1370
1343
|
// Status bar (widget + preconditions deferred until after model selection — see #2899)
|
|
1371
1344
|
ctx.ui.setStatus("gsd-auto", "auto");
|
|
1372
1345
|
if (mid)
|
|
@@ -1420,7 +1393,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
1420
1393
|
finalPrompt = `${capped}\n\n---\n\n${finalPrompt}`;
|
|
1421
1394
|
s.pendingCrashRecovery = null;
|
|
1422
1395
|
}
|
|
1423
|
-
else if (
|
|
1396
|
+
else if (nextDispatchCount > 1) {
|
|
1424
1397
|
const diagnostic = deps.getDeepDiagnostic(s.basePath);
|
|
1425
1398
|
if (diagnostic) {
|
|
1426
1399
|
const cappedDiag = diagnostic.length > MAX_RECOVERY_CHARS
|
|
@@ -1459,6 +1432,11 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
1459
1432
|
logWarning("engine", "Prompt reorder failed", { error: msg });
|
|
1460
1433
|
}
|
|
1461
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();
|
|
1462
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);
|
|
1463
1441
|
s.currentUnitRouting =
|
|
1464
1442
|
modelResult.routing;
|
|
@@ -1502,12 +1480,58 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
1502
1480
|
? ctx.modelRegistry.getProviderAuthMode(ctx.model.provider)
|
|
1503
1481
|
: undefined,
|
|
1504
1482
|
baseUrl: s.currentUnitModel?.baseUrl ?? ctx.model?.baseUrl,
|
|
1483
|
+
activeTools: typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [],
|
|
1505
1484
|
});
|
|
1506
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
|
+
}
|
|
1507
1502
|
ctx.ui.notify(compatibilityError, "error");
|
|
1508
1503
|
await deps.stopAuto(ctx, pi, compatibilityError);
|
|
1509
1504
|
return { action: "break", reason: "workflow-capability" };
|
|
1510
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
|
+
});
|
|
1511
1535
|
// Progress widget + preconditions — deferred to after model selection so the
|
|
1512
1536
|
// widget's first render tick shows the correct model (#2899).
|
|
1513
1537
|
deps.updateProgressWidget(ctx, unitType, unitId, state);
|
|
@@ -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");
|
|
@@ -616,7 +614,7 @@ export const DISPATCH_RULES = [
|
|
|
616
614
|
},
|
|
617
615
|
},
|
|
618
616
|
{
|
|
619
|
-
name: "planning (require_slice_discussion) → pause for discussion
|
|
617
|
+
name: "planning (require_slice_discussion) → pause for discussion",
|
|
620
618
|
match: async ({ state, mid, basePath, prefs }) => {
|
|
621
619
|
if (state.phase !== "planning")
|
|
622
620
|
return null;
|
|
@@ -1023,7 +1021,7 @@ export const DISPATCH_RULES = [
|
|
|
1023
1021
|
mkdirSync(mDir, { recursive: true });
|
|
1024
1022
|
const validationPath = join(mDir, buildMilestoneFileName(mid, "VALIDATION"));
|
|
1025
1023
|
const skipSource = trivialVariant
|
|
1026
|
-
? "trivial-scope pipeline variant
|
|
1024
|
+
? "trivial-scope pipeline variant"
|
|
1027
1025
|
: "`skip_milestone_validation` preference";
|
|
1028
1026
|
const skipValidationReason = trivialVariant ? "trivial-scope" : "preference";
|
|
1029
1027
|
const content = [
|
|
@@ -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)`);
|
|
@@ -999,7 +999,7 @@ export async function postUnitPreVerification(pctx, opts) {
|
|
|
999
999
|
s.verificationRetryCount.delete(retryKey);
|
|
1000
1000
|
s.verificationRetryFailureHashes.delete(retryKey);
|
|
1001
1001
|
writeBlockerPlaceholder(s.currentUnit.type, s.currentUnit.id, s.basePath, reason);
|
|
1002
|
-
ctx.ui.notify(`${s.currentUnit.type} ${s.currentUnit.id} — deterministic policy rejection, wrote blocker placeholder (no retries)
|
|
1002
|
+
ctx.ui.notify(`${s.currentUnit.type} ${s.currentUnit.id} — deterministic policy rejection, wrote blocker placeholder (no retries)`, "warning");
|
|
1003
1003
|
// Fall through to "continue" — do NOT enter the retry or db-unavailable paths.
|
|
1004
1004
|
}
|
|
1005
1005
|
else if (!triggerArtifactVerified && diagnoseWorktreeIntegrityFailure(s.basePath)) {
|
|
@@ -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
|
|
@@ -1325,7 +1325,6 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
|
|
|
1325
1325
|
restoreProjectRootEnv();
|
|
1326
1326
|
restoreMilestoneLockEnv();
|
|
1327
1327
|
s.pendingVerificationRetry = null;
|
|
1328
|
-
s.verificationRetryCount.clear();
|
|
1329
1328
|
ctx?.ui.setStatus("gsd-auto", "paused");
|
|
1330
1329
|
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
1331
1330
|
const resumeCmd = s.stepMode ? "/gsd next" : "/gsd auto";
|
|
@@ -61,6 +61,11 @@ export function isUserInitiatedAbortMessage(message) {
|
|
|
61
61
|
return false;
|
|
62
62
|
return /\b(?:claude code process aborted by user|request aborted by user|process aborted by user)\b/i.test(message);
|
|
63
63
|
}
|
|
64
|
+
export function shouldDeferTransientErrorToCoreRetry(cls, rawErrorMsg) {
|
|
65
|
+
if (!isTransient(cls) || cls.kind === "rate-limit")
|
|
66
|
+
return false;
|
|
67
|
+
return !/retry failed after \d+ attempts:/i.test(rawErrorMsg);
|
|
68
|
+
}
|
|
64
69
|
function isBareClaudeCodeSessionSwitchAbortMarker(message) {
|
|
65
70
|
if (!message)
|
|
66
71
|
return false;
|
|
@@ -394,7 +399,7 @@ export async function handleAgentEnd(pi, event, ctx) {
|
|
|
394
399
|
// Core retries transient failures in-session after this handler.
|
|
395
400
|
// Keep that behavior for non-rate-limit classes to avoid pause/retry races,
|
|
396
401
|
// but let rate-limit continue into model fallback logic below (#4373).
|
|
397
|
-
if (
|
|
402
|
+
if (shouldDeferTransientErrorToCoreRetry(cls, rawErrorMsg)) {
|
|
398
403
|
return;
|
|
399
404
|
}
|
|
400
405
|
// Cap rate-limit backoff for CLI-style providers (openai-codex, google-gemini-cli)
|
|
@@ -848,7 +848,7 @@ export function registerDbTools(pi) {
|
|
|
848
848
|
name: "gsd_skip_slice",
|
|
849
849
|
label: "Skip Slice",
|
|
850
850
|
description: "Mark a slice as skipped so auto-mode advances past it without executing. " +
|
|
851
|
-
"Non-closed tasks within the slice are cascaded to skipped so milestone completion is not blocked by leftover pending tasks
|
|
851
|
+
"Non-closed tasks within the slice are cascaded to skipped so milestone completion is not blocked by leftover pending tasks. " +
|
|
852
852
|
"The slice data is preserved for reference. The state machine treats skipped slices like completed ones for dependency satisfaction.",
|
|
853
853
|
promptSnippet: "Skip a GSD slice (mark as skipped, auto-mode will advance past it)",
|
|
854
854
|
promptGuidelines: [
|
|
@@ -625,7 +625,7 @@ function matchesAllowedGlob(absPath, basePath, globs) {
|
|
|
625
625
|
function blockReason(unitType, mode, what) {
|
|
626
626
|
return [
|
|
627
627
|
`HARD BLOCK: unit "${unitType}" runs under tools-policy "${mode}" — ${what}.`,
|
|
628
|
-
`This is a mechanical gate enforced by manifest.tools
|
|
628
|
+
`This is a mechanical gate enforced by manifest.tools. You MUST NOT proceed,`,
|
|
629
629
|
`retry the same call, or rationalize past this block. If you need to write user source,`,
|
|
630
630
|
`the work belongs in execute-task, not in a planning unit.`,
|
|
631
631
|
].join(" ");
|
|
@@ -571,10 +571,15 @@ async function configureModels(ctx, prefs) {
|
|
|
571
571
|
];
|
|
572
572
|
const models = prefs.models ?? {};
|
|
573
573
|
const availableModels = ctx.modelRegistry.getAvailable();
|
|
574
|
-
|
|
574
|
+
const getAllWithDiscovered = ctx.modelRegistry.getAllWithDiscovered;
|
|
575
|
+
const availableProviders = new Set(availableModels.map((m) => m.provider));
|
|
576
|
+
const selectableModels = typeof getAllWithDiscovered === "function"
|
|
577
|
+
? getAllWithDiscovered().filter((m) => availableProviders.has(m.provider))
|
|
578
|
+
: availableModels;
|
|
579
|
+
if (selectableModels.length > 0) {
|
|
575
580
|
// Group models by provider, sorted alphabetically
|
|
576
581
|
const byProvider = new Map();
|
|
577
|
-
for (const m of
|
|
582
|
+
for (const m of selectableModels) {
|
|
578
583
|
let group = byProvider.get(m.provider);
|
|
579
584
|
if (!group) {
|
|
580
585
|
group = [];
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
import { emitJournalEvent, queryJournal, } from "./journal.js";
|
|
24
24
|
import { readFileSync, unlinkSync, existsSync } from "node:fs";
|
|
25
25
|
import { join } from "node:path";
|
|
26
|
-
import { findStaleWorkerForProject, getAllAutoWorkers, markWorkerCrashed, } from "./db/auto-workers.js";
|
|
26
|
+
import { findStaleWorkerForProject, getAllAutoWorkers, markWorkerCrashed, markWorkerStopping, } from "./db/auto-workers.js";
|
|
27
|
+
import { forceReleaseLeasesForWorker } from "./db/milestone-leases.js";
|
|
27
28
|
import { markLatestActiveForWorkerCanceled } from "./db/unit-dispatches.js";
|
|
28
29
|
import { getRuntimeKv, setRuntimeKv, deleteRuntimeKv } from "./db/runtime-kv.js";
|
|
29
30
|
import { _getAdapter, isDbAvailable } from "./gsd-db.js";
|
|
@@ -182,10 +183,21 @@ export function clearLock(basePath) {
|
|
|
182
183
|
return;
|
|
183
184
|
try {
|
|
184
185
|
const projectRoot = normalizeRealPath(basePath);
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
186
|
+
const staleWorker = findStaleWorkerForProject(projectRoot);
|
|
187
|
+
if (staleWorker) {
|
|
188
|
+
markWorkerCrashed(staleWorker.worker_id);
|
|
189
|
+
forceReleaseLeasesForWorker(staleWorker.worker_id);
|
|
190
|
+
deleteRuntimeKv("worker", staleWorker.worker_id, SESSION_FILE_KV_KEY);
|
|
187
191
|
return;
|
|
188
|
-
|
|
192
|
+
}
|
|
193
|
+
const worker = findActiveWorkerForCurrentProcess(projectRoot);
|
|
194
|
+
if (worker)
|
|
195
|
+
deleteRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY);
|
|
196
|
+
const stale = findStaleWorkerForProject(projectRoot);
|
|
197
|
+
if (stale) {
|
|
198
|
+
markWorkerStopping(stale.worker_id);
|
|
199
|
+
deleteRuntimeKv("worker", stale.worker_id, SESSION_FILE_KV_KEY);
|
|
200
|
+
}
|
|
189
201
|
}
|
|
190
202
|
catch {
|
|
191
203
|
// Best-effort.
|
|
@@ -193,6 +193,30 @@ export function releaseMilestoneLease(workerId, milestoneId, fencingToken) {
|
|
|
193
193
|
return changes === 1;
|
|
194
194
|
});
|
|
195
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Force-release all held leases for a worker.
|
|
198
|
+
*
|
|
199
|
+
* Used by crash recovery once PID liveness has confirmed the worker is dead.
|
|
200
|
+
* No fencing token is required because this path is cleanup-only for a
|
|
201
|
+
* non-running process.
|
|
202
|
+
*/
|
|
203
|
+
export function forceReleaseLeasesForWorker(workerId) {
|
|
204
|
+
if (!isDbAvailable())
|
|
205
|
+
return 0;
|
|
206
|
+
const db = _getAdapter();
|
|
207
|
+
let changes = 0;
|
|
208
|
+
transaction(() => {
|
|
209
|
+
const result = db.prepare(`UPDATE milestone_leases
|
|
210
|
+
SET status = 'released'
|
|
211
|
+
WHERE worker_id = :worker_id
|
|
212
|
+
AND status = 'held'`).run({ ":worker_id": workerId });
|
|
213
|
+
changes =
|
|
214
|
+
typeof result.changes === "number"
|
|
215
|
+
? result.changes
|
|
216
|
+
: 0;
|
|
217
|
+
});
|
|
218
|
+
return changes;
|
|
219
|
+
}
|
|
196
220
|
/**
|
|
197
221
|
* Read current lease row for diagnostics. Returns null if no row exists.
|
|
198
222
|
*/
|
|
@@ -677,7 +677,7 @@ export function detectWorktreeOrphans(summary, anomalies) {
|
|
|
677
677
|
type: "worktree-unmerged-exit",
|
|
678
678
|
severity: "warning",
|
|
679
679
|
summary: `${summary.exitsWithUnmergedWork} auto-exit(s) left milestone work unmerged`,
|
|
680
|
-
details: `Exit reasons: ${reasonBreakdown || "(none)"} · Producer-side signal for
|
|
680
|
+
details: `Exit reasons: ${reasonBreakdown || "(none)"} · Producer-side signal for orphaned worktrees. Inspect .gsd/journal/*.jsonl with eventType:"auto-exit" for per-exit detail.`,
|
|
681
681
|
});
|
|
682
682
|
}
|
|
683
683
|
}
|
|
@@ -884,7 +884,7 @@ function saveForensicReport(basePath, report, problemDescription) {
|
|
|
884
884
|
.map(([r, n]) => `${r}=${n}`).join(", ");
|
|
885
885
|
sections.push(` - Exit reasons: ${breakdown}`);
|
|
886
886
|
}
|
|
887
|
-
sections.push(`- Canonical-root redirects
|
|
887
|
+
sections.push(`- Canonical-root redirects: ${t.canonicalRedirects}`);
|
|
888
888
|
// #4765 slice-cadence counters
|
|
889
889
|
if (t.slicesMerged + t.sliceMergeConflicts + t.milestoneResquashes > 0) {
|
|
890
890
|
sections.push(`- Slices merged: ${t.slicesMerged} · Slice merge conflicts: ${t.sliceMergeConflicts}`);
|
|
@@ -1033,7 +1033,7 @@ function formatReportForPrompt(report) {
|
|
|
1033
1033
|
if (hasSignal) {
|
|
1034
1034
|
sections.push("### Worktree Telemetry");
|
|
1035
1035
|
sections.push(`- Created: ${t.worktreesCreated} · Merged: ${t.worktreesMerged} · Conflicts: ${t.mergeConflicts}`);
|
|
1036
|
-
sections.push(`- Orphans: ${t.orphansDetected} · Unmerged exits: ${t.exitsWithUnmergedWork} · Redirects
|
|
1036
|
+
sections.push(`- Orphans: ${t.orphansDetected} · Unmerged exits: ${t.exitsWithUnmergedWork} · Redirects: ${t.canonicalRedirects}`);
|
|
1037
1037
|
if (t.orphansDetected > 0) {
|
|
1038
1038
|
const breakdown = Object.entries(t.orphansByReason)
|
|
1039
1039
|
.map(([r, n]) => `${r}=${n}`).join(", ");
|