@opengsd/gsd-pi 1.9.0-dev.5c1a3ff2 → 1.9.0-dev.d2070876
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/headless.js +2 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/loop.js +24 -4
- package/dist/resources/extensions/gsd/auto/run-unit.js +19 -1
- package/dist/resources/extensions/gsd/auto/unit-phase.js +37 -5
- package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +12 -4
- package/dist/resources/extensions/gsd/auto-runtime-state.js +5 -5
- package/dist/resources/extensions/gsd/auto-start.js +29 -9
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +40 -0
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +7 -3
- package/dist/resources/extensions/gsd/auto.js +15 -5
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +2 -2
- package/dist/resources/extensions/gsd/commands-verdict.js +10 -9
- package/dist/resources/extensions/gsd/compat/compat-marker.js +80 -5
- package/dist/resources/extensions/gsd/db/engine.js +5 -2
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +21 -2
- package/dist/resources/extensions/gsd/error-classifier.js +12 -2
- package/dist/resources/extensions/gsd/exec-sandbox.js +1 -0
- package/dist/resources/extensions/gsd/flat-phase-migration.js +12 -0
- package/dist/resources/extensions/gsd/gsd-db.js +75 -31
- package/dist/resources/extensions/gsd/guided-flow.js +2 -2
- package/dist/resources/extensions/gsd/markdown-renderer.js +3 -9
- package/dist/resources/extensions/gsd/paths.js +17 -7
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
- package/dist/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.js +98 -2
- package/dist/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.js +4 -1
- package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +5 -2
- package/dist/resources/extensions/gsd/stop-notice.js +9 -0
- package/dist/resources/extensions/gsd/worktree-state-projection.js +20 -18
- package/dist/resources/extensions/shared/interview-ui.js +43 -6
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
- package/dist/web/standalone/.next/server/chunks/1991.js +2 -2
- package/dist/web/standalone/.next/server/chunks/5124.js +1 -1
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- 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 +2 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/gsd-cloud/README.md +9 -3
- package/packages/gsd-cloud/dist/background-cli.test.d.ts +2 -0
- package/packages/gsd-cloud/dist/background-cli.test.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/background-cli.test.js +248 -0
- package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -0
- package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cli.js +121 -15
- package/packages/gsd-cloud/dist/cli.js.map +1 -1
- package/packages/gsd-cloud/dist/cloud-config.d.ts +1 -1
- package/packages/gsd-cloud/dist/cloud-config.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cloud-config.js +7 -1
- package/packages/gsd-cloud/dist/cloud-config.js.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.d.ts +1 -3
- package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.js +9 -7
- package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
- package/packages/gsd-cloud/dist/runtime-process.d.ts +27 -0
- package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-process.js +272 -0
- package/packages/gsd-cloud/dist/runtime-process.js.map +1 -0
- package/packages/gsd-cloud/dist/runtime-process.test.d.ts +2 -0
- package/packages/gsd-cloud/dist/runtime-process.test.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-process.test.js +97 -0
- package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -0
- package/packages/gsd-cloud/package.json +2 -2
- package/packages/mcp-server/dist/cli-runner.d.ts +3 -0
- package/packages/mcp-server/dist/cli-runner.d.ts.map +1 -1
- package/packages/mcp-server/dist/cli-runner.js +119 -33
- package/packages/mcp-server/dist/cli-runner.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +17 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +17 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +103 -0
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/skills.d.ts +5 -0
- package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.js +54 -0
- package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/loop.ts +48 -4
- package/src/resources/extensions/gsd/auto/run-unit.ts +20 -1
- package/src/resources/extensions/gsd/auto/unit-phase.ts +41 -5
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +14 -4
- package/src/resources/extensions/gsd/auto-runtime-state.ts +12 -7
- package/src/resources/extensions/gsd/auto-start.ts +31 -8
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +56 -0
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +10 -3
- package/src/resources/extensions/gsd/auto.ts +20 -7
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +2 -2
- package/src/resources/extensions/gsd/commands-verdict.ts +16 -9
- package/src/resources/extensions/gsd/compat/compat-marker.ts +99 -5
- package/src/resources/extensions/gsd/db/engine.ts +5 -2
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +27 -2
- package/src/resources/extensions/gsd/error-classifier.ts +13 -2
- package/src/resources/extensions/gsd/exec-sandbox.ts +1 -0
- package/src/resources/extensions/gsd/flat-phase-migration.ts +14 -0
- package/src/resources/extensions/gsd/gsd-db.ts +85 -33
- package/src/resources/extensions/gsd/guided-flow.ts +2 -2
- package/src/resources/extensions/gsd/markdown-renderer.ts +3 -9
- package/src/resources/extensions/gsd/paths.ts +17 -7
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +5 -5
- package/src/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.ts +132 -2
- package/src/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.ts +4 -1
- package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +4 -2
- package/src/resources/extensions/gsd/stop-notice.ts +11 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +264 -0
- package/src/resources/extensions/gsd/tests/auto-start-index-lock.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +74 -1
- package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +10 -0
- package/src/resources/extensions/gsd/tests/compat-marker.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/db-engine-cjs-compat.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +83 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +33 -1
- package/src/resources/extensions/gsd/tests/external-markdown-edit.test.ts +38 -1
- package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +8 -9
- package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +20 -1
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +13 -0
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +21 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +121 -1
- package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +105 -0
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +52 -0
- package/src/resources/extensions/gsd/tests/stop-notice.test.ts +21 -1
- package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +60 -0
- package/src/resources/extensions/gsd/worktree-state-projection.ts +21 -24
- package/src/resources/extensions/shared/interview-ui.ts +47 -8
- package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +29 -0
- /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → sggX-h41OjePo-FjdArP5}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{6YWV7VAPg-jqjfPswrKZb → sggX-h41OjePo-FjdArP5}/_ssgManifest.js +0 -0
|
@@ -88,6 +88,7 @@ import { classifyMilestoneSummaryContent } from "./milestone-summary-classifier.
|
|
|
88
88
|
import { extractVerdict } from "./verdict-parser.js";
|
|
89
89
|
import { auditOrphanedPreflightStashes } from "./orphan-stash-audit.js";
|
|
90
90
|
import { parseProject } from "./schemas/parsers.js";
|
|
91
|
+
import { LAYOUT_SEGMENTS } from "./layout-policy.js";
|
|
91
92
|
|
|
92
93
|
import {
|
|
93
94
|
debugLog,
|
|
@@ -152,6 +153,33 @@ export function hasGitIndexLockForTest(basePath: string): boolean {
|
|
|
152
153
|
return existsSync(join(basePath, ".git", "index.lock"));
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
function isEmptyDirectory(dirPath: string): boolean {
|
|
157
|
+
try {
|
|
158
|
+
return existsSync(dirPath) && readdirSync(dirPath).length === 0;
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function reconcileFlatPhaseBootstrapLayout(basePath: string): boolean {
|
|
165
|
+
const gsdDir = join(basePath, ".gsd");
|
|
166
|
+
const phasesPath = join(gsdDir, LAYOUT_SEGMENTS.level1);
|
|
167
|
+
const milestonesPath = join(gsdDir, "milestones");
|
|
168
|
+
|
|
169
|
+
if (isEmptyDirectory(milestonesPath)) {
|
|
170
|
+
if (!existsSync(phasesPath)) mkdirSync(phasesPath, { recursive: true });
|
|
171
|
+
rmSync(milestonesPath, { recursive: true, force: true });
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!existsSync(phasesPath) && !existsSync(milestonesPath)) {
|
|
176
|
+
mkdirSync(phasesPath, { recursive: true });
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
155
183
|
export function _shouldAbortBootstrapForUnavailableDbForTest(
|
|
156
184
|
gsdDbPath: string,
|
|
157
185
|
dbAvailable: boolean,
|
|
@@ -1152,19 +1180,14 @@ export async function bootstrapAutoSession(
|
|
|
1152
1180
|
ensureGitignore(base, { manageGitignore });
|
|
1153
1181
|
if (manageGitignore !== false) untrackRuntimeFiles(base);
|
|
1154
1182
|
|
|
1155
|
-
// Bootstrap
|
|
1156
|
-
|
|
1157
|
-
// so checking .gsd/ existence would be dead code (#2942).
|
|
1158
|
-
const gsdDir = join(base, ".gsd");
|
|
1159
|
-
const milestonesPath = join(gsdDir, "milestones");
|
|
1160
|
-
if (!existsSync(milestonesPath)) {
|
|
1161
|
-
mkdirSync(milestonesPath, { recursive: true });
|
|
1183
|
+
// Bootstrap the flat-phase projection root and clean empty legacy scaffolds.
|
|
1184
|
+
if (reconcileFlatPhaseBootstrapLayout(base)) {
|
|
1162
1185
|
try {
|
|
1163
1186
|
nativeAddAll(base);
|
|
1164
1187
|
nativeCommit(base, "chore: init gsd");
|
|
1165
1188
|
} catch (err) {
|
|
1166
1189
|
/* nothing to commit */
|
|
1167
|
-
logWarning("engine", `
|
|
1190
|
+
logWarning("engine", `layout bootstrap commit failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1168
1191
|
}
|
|
1169
1192
|
}
|
|
1170
1193
|
|
|
@@ -132,6 +132,8 @@ export function clearInFlightTools(): void {
|
|
|
132
132
|
*/
|
|
133
133
|
const TOOL_INVOCATION_ERROR_RE = /Validation failed for tool|Input validation error|Invalid arguments for tool|MCP error -32602|No such tool available|Expected ',' or '\}'(?: after property value)?(?: in JSON)?|Unexpected end of JSON|Unexpected token.*in JSON|does not provide an export named|Named export .* not found|Cannot find module|ERR_MODULE_NOT_FOUND|ERR_MODULE_NOT_EXPORTED|ERR_PACKAGE_PATH_NOT_EXPORTED/i;
|
|
134
134
|
const DETERMINISTIC_POLICY_ERROR_RE = /(?:^|\b)(?:HARD BLOCK:|Blocked: \/gsd queue is a planning tool|Direct writes to \.gsd\/STATE\.md and \.gsd\/gsd\.db are blocked|This is a mechanical gate)/i;
|
|
135
|
+
const SCHEDULE_WAKEUP_TOOL_NAME = "ScheduleWakeup";
|
|
136
|
+
const SCHEDULE_WAKEUP_CONTINUATION_ERROR = "`prompt` is required when `stop` is not true";
|
|
135
137
|
|
|
136
138
|
/**
|
|
137
139
|
* Matches the runtime's "tool not registered" error. Unlike the deterministic
|
|
@@ -151,6 +153,60 @@ export function isToolInvocationError(errorMsg: string): boolean {
|
|
|
151
153
|
return TOOL_INVOCATION_ERROR_RE.test(errorMsg) || isDeterministicPolicyError(errorMsg);
|
|
152
154
|
}
|
|
153
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Claude Code validates its native ScheduleWakeup tool outside GSD's schema.
|
|
158
|
+
* Keep this exact and tool-scoped so similarly worded business validation from
|
|
159
|
+
* unrelated tools does not pause auto-mode.
|
|
160
|
+
*/
|
|
161
|
+
export function isScheduleWakeupContinuationError(toolName: string, errorMsg: string): boolean {
|
|
162
|
+
return stripMcpToolPrefix(toolName) === SCHEDULE_WAKEUP_TOOL_NAME
|
|
163
|
+
&& errorMsg.trim() === SCHEDULE_WAKEUP_CONTINUATION_ERROR;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function shouldRecordToolInvocationError(toolName: string, errorMsg: string): boolean {
|
|
167
|
+
return isScheduleWakeupContinuationError(toolName, errorMsg)
|
|
168
|
+
|| isToolInvocationError(errorMsg)
|
|
169
|
+
|| isQueuedUserMessageSkip(errorMsg);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isRecordedScheduleWakeupContinuationError(recordedError: string | null): boolean {
|
|
173
|
+
if (!recordedError) return false;
|
|
174
|
+
|
|
175
|
+
const separator = recordedError.indexOf(": ");
|
|
176
|
+
return separator >= 0 && isScheduleWakeupContinuationError(
|
|
177
|
+
recordedError.slice(0, separator),
|
|
178
|
+
recordedError.slice(separator + 2),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function updateToolInvocationError(
|
|
183
|
+
recordedError: string | null,
|
|
184
|
+
toolName: string,
|
|
185
|
+
errorMsg: string,
|
|
186
|
+
): string | null {
|
|
187
|
+
if (!shouldRecordToolInvocationError(toolName, errorMsg)) return recordedError;
|
|
188
|
+
if (
|
|
189
|
+
isRecordedScheduleWakeupContinuationError(recordedError)
|
|
190
|
+
&& !isScheduleWakeupContinuationError(toolName, errorMsg)
|
|
191
|
+
) return recordedError;
|
|
192
|
+
|
|
193
|
+
return `${toolName}: ${errorMsg}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* A failed continuation must survive successful diagnostic/read calls in the
|
|
198
|
+
* same turn. Only a later successful ScheduleWakeup proves the continuation
|
|
199
|
+
* was armed; all other invocation errors retain their existing clear-on-success
|
|
200
|
+
* behavior.
|
|
201
|
+
*/
|
|
202
|
+
export function shouldClearToolInvocationErrorAfterSuccess(
|
|
203
|
+
recordedError: string | null,
|
|
204
|
+
successfulToolName?: string,
|
|
205
|
+
): boolean {
|
|
206
|
+
return !isRecordedScheduleWakeupContinuationError(recordedError)
|
|
207
|
+
|| stripMcpToolPrefix(successfulToolName ?? "") === SCHEDULE_WAKEUP_TOOL_NAME;
|
|
208
|
+
}
|
|
209
|
+
|
|
154
210
|
/**
|
|
155
211
|
* Returns true if the error message indicates the called tool was not on the
|
|
156
212
|
* session's tool surface (MCP startup race — see TOOL_UNAVAILABLE_ERROR_RE).
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
_isSamePath as isSamePath,
|
|
17
17
|
_shouldReconcileWorktreeDb,
|
|
18
18
|
} from "./auto-worktree-cleanup.js";
|
|
19
|
-
import { resolveGsdPathContract } from "./paths.js";
|
|
19
|
+
import { dirIsContentBearingLegacyMilestone, resolveGsdPathContract } from "./paths.js";
|
|
20
20
|
import type { MilestoneScope } from "./workspace.js";
|
|
21
21
|
import { WorktreeStateProjection } from "./worktree-state-projection.js";
|
|
22
22
|
import { logWarning } from "./workflow-logger.js";
|
|
@@ -211,10 +211,17 @@ function syncMilestoneLayout(
|
|
|
211
211
|
if (!existsSync(mainMilestonesDir)) return;
|
|
212
212
|
|
|
213
213
|
try {
|
|
214
|
-
mkdirSync(wtMilestonesDir, { recursive: true });
|
|
215
214
|
const mainMilestones = readdirSync(mainMilestonesDir, { withFileTypes: true })
|
|
216
215
|
.filter((entry) => entry.isDirectory())
|
|
217
|
-
.map((entry) => entry.name)
|
|
216
|
+
.map((entry) => entry.name)
|
|
217
|
+
.filter((name) =>
|
|
218
|
+
layoutSegment !== "milestones" ||
|
|
219
|
+
dirIsContentBearingLegacyMilestone(join(mainMilestonesDir, name)),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
if (mainMilestones.length === 0) return;
|
|
223
|
+
|
|
224
|
+
mkdirSync(wtMilestonesDir, { recursive: true });
|
|
218
225
|
|
|
219
226
|
for (const milestoneId of mainMilestones) {
|
|
220
227
|
syncMilestoneDirectory(
|
|
@@ -110,9 +110,7 @@ import {
|
|
|
110
110
|
getOldestInFlightToolStart,
|
|
111
111
|
hasInteractiveToolInFlight,
|
|
112
112
|
clearInFlightTools,
|
|
113
|
-
|
|
114
|
-
isQueuedUserMessageSkip,
|
|
115
|
-
isDeterministicPolicyError,
|
|
113
|
+
updateToolInvocationError,
|
|
116
114
|
} from "./auto-tool-tracking.js";
|
|
117
115
|
import { closeoutUnit } from "./auto-unit-closeout.js";
|
|
118
116
|
import { recoverTimedOutUnit } from "./auto-timeout-recovery.js";
|
|
@@ -1001,14 +999,17 @@ export function markToolEnd(toolCallId: string): void {
|
|
|
1001
999
|
* Called from tool_execution_end when a GSD tool fails with isError.
|
|
1002
1000
|
* Stores the error if it matches:
|
|
1003
1001
|
* - tool-invocation-error pattern (malformed/truncated JSON)
|
|
1002
|
+
* - failed ScheduleWakeup continuation validation (#1379)
|
|
1004
1003
|
* - queued-user-message skip pattern
|
|
1005
1004
|
* - deterministic policy rejection (#4973, e.g. context_write_blocked)
|
|
1006
1005
|
*/
|
|
1007
1006
|
export function recordToolInvocationError(toolName: string, errorMsg: string): void {
|
|
1008
1007
|
if (!s.active) return;
|
|
1009
|
-
|
|
1010
|
-
s.lastToolInvocationError
|
|
1011
|
-
|
|
1008
|
+
s.lastToolInvocationError = updateToolInvocationError(
|
|
1009
|
+
s.lastToolInvocationError,
|
|
1010
|
+
toolName,
|
|
1011
|
+
errorMsg,
|
|
1012
|
+
);
|
|
1012
1013
|
}
|
|
1013
1014
|
|
|
1014
1015
|
export function getOldestInFlightToolAgeMs(): number {
|
|
@@ -3162,7 +3163,19 @@ export async function dispatchHookUnit(
|
|
|
3162
3163
|
workspaceRoot: s.basePath,
|
|
3163
3164
|
});
|
|
3164
3165
|
|
|
3165
|
-
const
|
|
3166
|
+
const cmdCtx = s.cmdCtx;
|
|
3167
|
+
if (typeof cmdCtx?.newSession !== "function") {
|
|
3168
|
+
const message = "Auto-mode has no command context for dispatch.";
|
|
3169
|
+
ctx.ui.notify(message, "error");
|
|
3170
|
+
const unitToCommit = previousCurrentUnit ?? s.currentUnit;
|
|
3171
|
+
if (unitToCommit) {
|
|
3172
|
+
await autoCommitUnit(s.basePath, unitToCommit.type, unitToCommit.id, ctx);
|
|
3173
|
+
}
|
|
3174
|
+
await stopAuto(ctx, pi, message, { preserveWorktree: true });
|
|
3175
|
+
return false;
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
const result = await cmdCtx.newSession({ workspaceRoot: s.basePath });
|
|
3166
3179
|
if (result.cancelled) {
|
|
3167
3180
|
await stopAuto(ctx, pi);
|
|
3168
3181
|
return false;
|
|
@@ -1769,7 +1769,7 @@ export function registerHooks(
|
|
|
1769
1769
|
recordToolInvocationError(event.toolName, errorText);
|
|
1770
1770
|
recordRetryableHarnessToolError(toolName, resultPayload, errorText);
|
|
1771
1771
|
} else if (isAutoActive()) {
|
|
1772
|
-
clearToolInvocationError();
|
|
1772
|
+
clearToolInvocationError(event.toolName);
|
|
1773
1773
|
}
|
|
1774
1774
|
// Interactive Closeout adapter (ADR-032): auto-mode owns closeout for its
|
|
1775
1775
|
// own units; interactive completions get the durable git subset (commit +
|
|
@@ -1980,7 +1980,7 @@ export function registerHooks(
|
|
|
1980
1980
|
recordToolInvocationError(event.toolName, errorText);
|
|
1981
1981
|
recordRetryableHarnessToolError(toolName, event.result, errorText);
|
|
1982
1982
|
} else if (isAutoActive()) {
|
|
1983
|
-
clearToolInvocationError();
|
|
1983
|
+
clearToolInvocationError(event.toolName);
|
|
1984
1984
|
}
|
|
1985
1985
|
// Safety harness: record tool execution results for evidence cross-referencing
|
|
1986
1986
|
if (isAutoActive()) {
|
|
@@ -11,6 +11,7 @@ import { executeValidateMilestone } from "./tools/workflow-tool-executors.js";
|
|
|
11
11
|
import { ensureDbOpen } from "./bootstrap/dynamic-tools.js";
|
|
12
12
|
import { getLatestAssessmentByScope } from "./gsd-db.js";
|
|
13
13
|
import { checkpointWorkflowDatabase } from "./db-workspace.js";
|
|
14
|
+
import { formatVerdictRecordedNotice, formatVerdictRejectedNotice } from "./stop-notice.js";
|
|
14
15
|
import {
|
|
15
16
|
VALIDATION_VERDICTS,
|
|
16
17
|
extractVerdict,
|
|
@@ -165,17 +166,17 @@ export async function handleVerdict(
|
|
|
165
166
|
basePath: string,
|
|
166
167
|
): Promise<void> {
|
|
167
168
|
if (!rawArgs.trim()) {
|
|
168
|
-
ctx.ui.notify(USAGE, "warning");
|
|
169
|
+
ctx.ui.notify(formatVerdictRejectedNotice(USAGE), "warning");
|
|
169
170
|
return;
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
const parsed = parseArgs(rawArgs);
|
|
173
174
|
if ("error" in parsed) {
|
|
174
|
-
ctx.ui.notify(`${parsed.error}\n${USAGE}
|
|
175
|
+
ctx.ui.notify(formatVerdictRejectedNotice(`${parsed.error}\n${USAGE}`), "warning");
|
|
175
176
|
return;
|
|
176
177
|
}
|
|
177
178
|
if (!parsed.verdict) {
|
|
178
|
-
ctx.ui.notify(USAGE, "warning");
|
|
179
|
+
ctx.ui.notify(formatVerdictRejectedNotice(USAGE), "warning");
|
|
179
180
|
return;
|
|
180
181
|
}
|
|
181
182
|
|
|
@@ -184,7 +185,7 @@ export async function handleVerdict(
|
|
|
184
185
|
const state = await deriveState(basePath);
|
|
185
186
|
if (!state.activeMilestone) {
|
|
186
187
|
ctx.ui.notify(
|
|
187
|
-
"No active milestone — pass --milestone Mxxx to target a specific milestone.",
|
|
188
|
+
formatVerdictRejectedNotice("No active milestone — pass --milestone Mxxx to target a specific milestone."),
|
|
188
189
|
"warning",
|
|
189
190
|
);
|
|
190
191
|
return;
|
|
@@ -195,7 +196,9 @@ export async function handleVerdict(
|
|
|
195
196
|
const existingValidation = await loadExistingValidation(basePath, milestoneId);
|
|
196
197
|
if (!existingValidation) {
|
|
197
198
|
ctx.ui.notify(
|
|
198
|
-
|
|
199
|
+
formatVerdictRejectedNotice(
|
|
200
|
+
`No milestone validation found for ${milestoneId}. Run /gsd auto to validate first, then retry /gsd verdict.`,
|
|
201
|
+
),
|
|
199
202
|
"warning",
|
|
200
203
|
);
|
|
201
204
|
return;
|
|
@@ -205,7 +208,7 @@ export async function handleVerdict(
|
|
|
205
208
|
|
|
206
209
|
if (parsed.verdict !== "pass" && !parsed.rationale) {
|
|
207
210
|
ctx.ui.notify(
|
|
208
|
-
`--rationale is required when overriding to ${parsed.verdict}
|
|
211
|
+
formatVerdictRejectedNotice(`--rationale is required when overriding to ${parsed.verdict}.`),
|
|
209
212
|
"warning",
|
|
210
213
|
);
|
|
211
214
|
return;
|
|
@@ -234,7 +237,7 @@ export async function handleVerdict(
|
|
|
234
237
|
if (result.isError) {
|
|
235
238
|
const msg =
|
|
236
239
|
result.content[0]?.type === "text" ? result.content[0].text : "Unknown error";
|
|
237
|
-
ctx.ui.notify(msg, "error");
|
|
240
|
+
ctx.ui.notify(formatVerdictRejectedNotice(msg), "error");
|
|
238
241
|
return;
|
|
239
242
|
}
|
|
240
243
|
|
|
@@ -244,12 +247,16 @@ export async function handleVerdict(
|
|
|
244
247
|
const effectiveVerdict = extractEffectiveVerdict(result.details, parsed.verdict);
|
|
245
248
|
if (effectiveVerdict !== parsed.verdict) {
|
|
246
249
|
ctx.ui.notify(
|
|
247
|
-
|
|
250
|
+
formatVerdictRecordedNotice(
|
|
251
|
+
`Milestone ${milestoneId} verdict requested: ${parsed.verdict}, effective: ${effectiveVerdict} (${existingValidation.source})`,
|
|
252
|
+
),
|
|
248
253
|
"warning",
|
|
249
254
|
);
|
|
250
255
|
} else {
|
|
251
256
|
ctx.ui.notify(
|
|
252
|
-
|
|
257
|
+
formatVerdictRecordedNotice(
|
|
258
|
+
`Milestone ${milestoneId} verdict: ${prevVerdict} -> ${effectiveVerdict} (${existingValidation.source})`,
|
|
259
|
+
),
|
|
253
260
|
"success",
|
|
254
261
|
);
|
|
255
262
|
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// (drift to import). gsd-core is oblivious to this file and ignores it.
|
|
7
7
|
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
|
-
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
-
import { dirname, join, isAbsolute, normalize } from "node:path";
|
|
9
|
+
import { existsSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { dirname, join, isAbsolute, normalize, relative, resolve } from "node:path";
|
|
11
11
|
|
|
12
12
|
/** Current marker schema version. Bump on breaking format changes + migrate. */
|
|
13
13
|
export const COMPAT_MARKER_SCHEMA = 2;
|
|
@@ -54,6 +54,13 @@ export interface CompatMarker {
|
|
|
54
54
|
piVersion: string;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export interface ReadCompatMarkerOptions {
|
|
58
|
+
/** Rewrite invalid-but-derivable marker keys back to safe root-relative keys. */
|
|
59
|
+
healInvalidKeys?: boolean;
|
|
60
|
+
/** Quarantine malformed/unsafe markers. Disable only for read-only previews. */
|
|
61
|
+
quarantineInvalid?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
/** Marker returned when no marker exists yet (fresh project, first gsd-pi run). */
|
|
58
65
|
export const EMPTY_MARKER: CompatMarker = {
|
|
59
66
|
schema: COMPAT_MARKER_SCHEMA,
|
|
@@ -81,6 +88,37 @@ export function computeProjectionSha(content: string): string {
|
|
|
81
88
|
return createHash("sha256").update(normalizeForHash(content)).digest("hex").slice(0, 16);
|
|
82
89
|
}
|
|
83
90
|
|
|
91
|
+
function normalizeRealPath(p: string): string {
|
|
92
|
+
try {
|
|
93
|
+
return realpathSync.native(p);
|
|
94
|
+
} catch {
|
|
95
|
+
return resolve(p);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function rootRelativeKey(rootPath: string, absPath: string): string | null {
|
|
100
|
+
const root = normalizeRealPath(rootPath);
|
|
101
|
+
const abs = normalizeRealPath(absPath);
|
|
102
|
+
const rel = relative(root, abs);
|
|
103
|
+
if (!rel || rel === ".." || rel.startsWith(`..${sepForRelative(rel)}`) || isAbsolute(rel)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return rel.replace(/\\/g, "/");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function sepForRelative(rel: string): string {
|
|
110
|
+
return rel.includes("\\") ? "\\" : "/";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function deriveCompatProjectionKey(absPath: string, roots: readonly string[]): string {
|
|
114
|
+
for (const root of roots) {
|
|
115
|
+
const key = rootRelativeKey(root, absPath);
|
|
116
|
+
if (key) return key;
|
|
117
|
+
}
|
|
118
|
+
const fallbackRoot = roots[roots.length - 1] ?? dirname(absPath);
|
|
119
|
+
return relative(fallbackRoot, absPath).replace(/\\/g, "/");
|
|
120
|
+
}
|
|
121
|
+
|
|
84
122
|
/**
|
|
85
123
|
* Read & validate the marker. A missing marker → EMPTY_MARKER (treat every
|
|
86
124
|
* projection as external on next reconcile). A malformed marker is quarantined
|
|
@@ -88,7 +126,12 @@ export function computeProjectionSha(content: string): string {
|
|
|
88
126
|
* EMPTY_MARKER. A schema-mismatch returns EMPTY_MARKER (forward-compat: refuse
|
|
89
127
|
* to act on a future format we don't understand).
|
|
90
128
|
*/
|
|
91
|
-
export function readCompatMarker(
|
|
129
|
+
export function readCompatMarker(
|
|
130
|
+
basePath: string,
|
|
131
|
+
options: ReadCompatMarkerOptions = {},
|
|
132
|
+
): CompatMarker {
|
|
133
|
+
const healInvalidKeys = options.healInvalidKeys ?? true;
|
|
134
|
+
const quarantineInvalid = options.quarantineInvalid ?? true;
|
|
92
135
|
const path = compatMarkerPath(basePath);
|
|
93
136
|
if (!existsSync(path)) return emptyMarker();
|
|
94
137
|
|
|
@@ -103,12 +146,16 @@ export function readCompatMarker(basePath: string): CompatMarker {
|
|
|
103
146
|
try {
|
|
104
147
|
parsed = JSON.parse(raw);
|
|
105
148
|
} catch {
|
|
106
|
-
quarantine(basePath, raw);
|
|
149
|
+
if (quarantineInvalid) quarantine(basePath, raw);
|
|
107
150
|
return emptyMarker();
|
|
108
151
|
}
|
|
109
152
|
|
|
153
|
+
if (healInvalidKeys && healMarkerProjectionKeys(basePath, parsed) && isValidMarker(parsed)) {
|
|
154
|
+
writeCompatMarker(basePath, parsed);
|
|
155
|
+
}
|
|
156
|
+
|
|
110
157
|
if (!isValidMarker(parsed)) {
|
|
111
|
-
quarantine(basePath, raw);
|
|
158
|
+
if (quarantineInvalid) quarantine(basePath, raw);
|
|
112
159
|
return emptyMarker();
|
|
113
160
|
}
|
|
114
161
|
// Promote older markers by defaulting absent fields. Schema 1 → 2 only adds
|
|
@@ -245,6 +292,53 @@ function isValidProjectionMap(x: unknown): boolean {
|
|
|
245
292
|
return true;
|
|
246
293
|
}
|
|
247
294
|
|
|
295
|
+
function healMarkerProjectionKeys(basePath: string, marker: unknown): marker is CompatMarker {
|
|
296
|
+
if (typeof marker !== "object" || marker === null) return false;
|
|
297
|
+
const m = marker as Record<string, unknown>;
|
|
298
|
+
let changed = false;
|
|
299
|
+
|
|
300
|
+
if (typeof m.projections === "object" && m.projections !== null) {
|
|
301
|
+
changed = healProjectionMapKeys(basePath, ".gsd", m.projections as Record<string, ProjectionEntry>) || changed;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const planning = m.planning;
|
|
305
|
+
if (typeof planning === "object" && planning !== null) {
|
|
306
|
+
const p = planning as Record<string, unknown>;
|
|
307
|
+
if (typeof p.projections === "object" && p.projections !== null) {
|
|
308
|
+
changed = healProjectionMapKeys(basePath, ".planning", p.projections as Record<string, ProjectionEntry>) || changed;
|
|
309
|
+
}
|
|
310
|
+
if (typeof p.passthrough === "object" && p.passthrough !== null) {
|
|
311
|
+
changed = healProjectionMapKeys(basePath, ".planning", p.passthrough as Record<string, ProjectionEntry>) || changed;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return changed;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function healProjectionMapKeys(
|
|
319
|
+
basePath: string,
|
|
320
|
+
rootName: ".gsd" | ".planning",
|
|
321
|
+
map: Record<string, ProjectionEntry>,
|
|
322
|
+
): boolean {
|
|
323
|
+
let changed = false;
|
|
324
|
+
const root = join(basePath, rootName);
|
|
325
|
+
|
|
326
|
+
for (const key of Object.keys(map)) {
|
|
327
|
+
if (isSafeProjectionKey(key)) continue;
|
|
328
|
+
if (key.includes("\0") || isAbsolute(key) || /^[A-Za-z]:/.test(key)) continue;
|
|
329
|
+
const healedKey = rootRelativeKey(root, resolve(root, key));
|
|
330
|
+
if (!healedKey || !isSafeProjectionKey(healedKey)) continue;
|
|
331
|
+
|
|
332
|
+
if (!map[healedKey]) {
|
|
333
|
+
map[healedKey] = map[key]!;
|
|
334
|
+
}
|
|
335
|
+
delete map[key];
|
|
336
|
+
changed = true;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return changed;
|
|
340
|
+
}
|
|
341
|
+
|
|
248
342
|
function isValidPlanningMarker(x: unknown): x is PlanningMarker {
|
|
249
343
|
if (typeof x !== "object" || x === null) return false;
|
|
250
344
|
const p = x as Record<string, unknown>;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// writer layer.
|
|
11
11
|
import { createRequire } from "node:module";
|
|
12
12
|
import { existsSync, copyFileSync, mkdirSync, realpathSync } from "node:fs";
|
|
13
|
-
import { dirname, join } from "node:path";
|
|
13
|
+
import { dirname, join, resolve } from "node:path";
|
|
14
14
|
import { GSDError, GSD_STALE_STATE } from "../errors.js";
|
|
15
15
|
import type { GsdWorkspace, MilestoneScope } from "../workspace.js";
|
|
16
16
|
import { logError, logWarning } from "../workflow-logger.js";
|
|
@@ -71,7 +71,10 @@ let _gsdRequire: ReturnType<typeof createRequire> | null | undefined;
|
|
|
71
71
|
function getGsdRequire(): ReturnType<typeof createRequire> | null {
|
|
72
72
|
if (_gsdRequire !== undefined) return _gsdRequire;
|
|
73
73
|
try {
|
|
74
|
-
|
|
74
|
+
// Next.js may emit this module into a CommonJS chunk. Avoid ESM-only module
|
|
75
|
+
// metadata syntax here; it is a hard parse error there.
|
|
76
|
+
const packageRoot = process.env.GSD_WEB_PACKAGE_ROOT || process.env.GSD_PKG_ROOT || process.cwd();
|
|
77
|
+
_gsdRequire = createRequire(resolve(packageRoot, "package.json"));
|
|
75
78
|
} catch {
|
|
76
79
|
_gsdRequire = null;
|
|
77
80
|
}
|
|
@@ -95,6 +95,25 @@ function reportCheckboxDbStatusDivergence(
|
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
function bareDuplicateMilestoneId(milestoneId: string): string | null {
|
|
99
|
+
const match = milestoneId.match(/^(M\d{3})-[a-z0-9]{6}$/i);
|
|
100
|
+
return match?.[1]?.toUpperCase() ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function checkboxDbStatusMilestoneIds(basePath: string, milestoneIds: string[]): string[] {
|
|
104
|
+
if (!hasFlatPhaseLayout(basePath)) return milestoneIds;
|
|
105
|
+
|
|
106
|
+
// Flat-phase directories are keyed by phase number. A restored
|
|
107
|
+
// M003-xxxxxx row alongside M003 is a stale duplicate for the same
|
|
108
|
+
// phases/03-* projection, so checking both can compare DB status against two
|
|
109
|
+
// competing PLAN files and report a persistent false divergence.
|
|
110
|
+
const allIds = new Set(milestoneIds.map((id) => id.toUpperCase()));
|
|
111
|
+
return milestoneIds.filter((milestoneId) => {
|
|
112
|
+
const bareId = bareDuplicateMilestoneId(milestoneId);
|
|
113
|
+
return !bareId || !allIds.has(bareId);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
98
117
|
function checkProjectionCheckboxDbStatus(basePath: string, milestoneIds: string[], issues: DoctorIssue[]): void {
|
|
99
118
|
for (const milestoneId of milestoneIds) {
|
|
100
119
|
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
@@ -434,7 +453,9 @@ function staleMilestonesArtifactRowFixable(basePath: string, row: ArtifactRow, a
|
|
|
434
453
|
|
|
435
454
|
function stalePhasesArtifactRowFixable(basePath: string, row: ArtifactRow, artifactRows: ArtifactRow[]): boolean {
|
|
436
455
|
const issuePath = artifactPathRelativeToGsd(row.path);
|
|
437
|
-
return issuePath.startsWith(`${LAYOUT_SEGMENTS.level1}/`) &&
|
|
456
|
+
return issuePath.startsWith(`${LAYOUT_SEGMENTS.level1}/`) &&
|
|
457
|
+
(hasPresentFlatPhaseReplacement(basePath, row, artifactRows) ||
|
|
458
|
+
hasPresentMilestonesReplacement(basePath, row, artifactRows));
|
|
438
459
|
}
|
|
439
460
|
|
|
440
461
|
function staleArtifactRowFixable(basePath: string, row: ArtifactRow, artifactRows: ArtifactRow[]): boolean {
|
|
@@ -807,7 +828,11 @@ export async function checkEngineHealth(
|
|
|
807
828
|
// block doctor.
|
|
808
829
|
try {
|
|
809
830
|
if (isDbAvailable()) {
|
|
810
|
-
checkProjectionCheckboxDbStatus(
|
|
831
|
+
checkProjectionCheckboxDbStatus(
|
|
832
|
+
basePath,
|
|
833
|
+
checkboxDbStatusMilestoneIds(basePath, getAllMilestones().map((milestone) => milestone.id)),
|
|
834
|
+
issues,
|
|
835
|
+
);
|
|
811
836
|
}
|
|
812
837
|
} catch {
|
|
813
838
|
// Non-fatal: checkbox-vs-DB divergence check must never block doctor
|
|
@@ -65,8 +65,12 @@ const NETWORK_RE = /network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|web
|
|
|
65
65
|
// transient errors so auto-mode can retry with reduced budget or fall back to a larger-context model.
|
|
66
66
|
// See: https://github.com/open-gsd/gsd-pi/issues/4528
|
|
67
67
|
const SERVER_RE = /internal(?: server)?[ _-]?error|server[ _-]?error|500|502|503|overloaded|server_error|api_error|service.?unavailable|context (?:window|length) exceed|context window exceed/i;
|
|
68
|
+
// Provider adapters throw this when an upstream stop reason reaches Pi before
|
|
69
|
+
// the adapter knows how to map it. Keep the match scoped so plain "unknown"
|
|
70
|
+
// runtime errors still pause for inspection instead of being retried forever.
|
|
71
|
+
const PROVIDER_STOP_REASON_RE = /unhandled stop reason:\s*(?:abort|unknown)\b/i;
|
|
68
72
|
// ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
|
|
69
|
-
const CONNECTION_RE = /terminated|connection.?(?:refused|error)|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
|
|
73
|
+
const CONNECTION_RE = /terminated|\babort\b|connection.?(?:refused|error)|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
|
|
70
74
|
// Catch-all for V8 JSON.parse errors: all modern variants end with "in JSON at position \d+".
|
|
71
75
|
// This eliminates the need to enumerate every error message variant individually.
|
|
72
76
|
const STREAM_RE = /in JSON at position \d+|Unexpected end of JSON|SyntaxError.*JSON/i;
|
|
@@ -103,7 +107,7 @@ const UNSUPPORTED_MODEL_SCOPE_RE = /\b(?:account|plan|tier|subscription)\b/i;
|
|
|
103
107
|
* 3. Network (ECONNRESET, ETIMEDOUT, socket hang up, fetch failed, dns)
|
|
104
108
|
* 4. Stream truncation (malformed JSON from mid-stream cut)
|
|
105
109
|
* 5. Server (500/502/503, overloaded, server_error)
|
|
106
|
-
* 6. Connection (terminated, ECONNREFUSED, EPIPE, other side closed)
|
|
110
|
+
* 6. Connection (terminated, abort/unknown stop reason, ECONNREFUSED, EPIPE, other side closed)
|
|
107
111
|
* 7. Model error (400 invalid argument / payload rejected for this model)
|
|
108
112
|
* 8. Unknown
|
|
109
113
|
*/
|
|
@@ -163,6 +167,13 @@ export function classifyError(errorMsg: string, retryAfterMs?: number): ErrorCla
|
|
|
163
167
|
return { kind: "rate-limit", retryAfterMs: delayMs };
|
|
164
168
|
}
|
|
165
169
|
|
|
170
|
+
// Provider stop-reason adapter gaps are usually request/stream failures from
|
|
171
|
+
// the current model. Treat the scoped adapter message as fallback-eligible,
|
|
172
|
+
// but keep generic unknown errors on the indefinite pause path.
|
|
173
|
+
if (PROVIDER_STOP_REASON_RE.test(errorMsg)) {
|
|
174
|
+
return { kind: "connection", retryAfterMs: retryAfterMs ?? 15_000 };
|
|
175
|
+
}
|
|
176
|
+
|
|
166
177
|
// 3. Network errors — same-model retry candidate
|
|
167
178
|
if (NETWORK_RE.test(errorMsg)) {
|
|
168
179
|
// Exclude if also matches permanent signals (already handled above for
|
|
@@ -155,6 +155,19 @@ function isInsideFlatProjectionBackup(backupDir: string, src: string): boolean {
|
|
|
155
155
|
return src === phaseBackupDir || src.startsWith(`${phaseBackupDir}/`) || src.startsWith(`${phaseBackupDir}\\`);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
function removeFlatProjectionBackup(backupDir: string): void {
|
|
159
|
+
const phaseBackupDir = join(backupDir, "__phases");
|
|
160
|
+
if (!existsSync(phaseBackupDir)) return;
|
|
161
|
+
try {
|
|
162
|
+
removePathWithRetries(phaseBackupDir);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
logWarning(
|
|
165
|
+
"migration",
|
|
166
|
+
`flat-phase migration succeeded but could not remove temporary ${LAYOUT_SEGMENTS.level1}/ backup: ${(err as Error).message}`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
158
171
|
function restoreFlatProjectionFromBackup(basePath: string, backupDir: string): void {
|
|
159
172
|
const phaseBackupDir = join(backupDir, "__phases");
|
|
160
173
|
if (!existsSync(phaseBackupDir)) return;
|
|
@@ -494,4 +507,5 @@ export async function migrateToFlatPhase(basePath: string): Promise<void> {
|
|
|
494
507
|
`flat-phase migration succeeded but could not remove ${LEGACY_MIGRATING_SEGMENT}: ${(err as Error).message}`,
|
|
495
508
|
);
|
|
496
509
|
}
|
|
510
|
+
removeFlatProjectionBackup(backupDir);
|
|
497
511
|
}
|