@opengsd/gsd-pi 1.4.0-dev.5fc22c6f → 1.4.0-dev.c18009cd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +58 -4
- package/dist/resources/extensions/gsd/artifact-verification.js +17 -5
- package/dist/resources/extensions/gsd/auto/closeout.js +45 -42
- package/dist/resources/extensions/gsd/auto/finalize.js +9 -0
- package/dist/resources/extensions/gsd/auto/orchestrator.js +1 -1
- package/dist/resources/extensions/gsd/auto/run-unit.js +5 -2
- package/dist/resources/extensions/gsd/auto/schedule-wakeup.js +3 -0
- package/dist/resources/extensions/gsd/auto.js +10 -3
- package/dist/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.js +58 -9
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +174 -16
- package/dist/resources/extensions/gsd/gsd-db.js +52 -0
- package/dist/resources/extensions/gsd/migrate-external.js +13 -4
- package/dist/resources/extensions/gsd/milestone-merge-transaction.js +3 -3
- package/dist/resources/extensions/gsd/repo-identity.js +16 -0
- package/dist/resources/extensions/gsd/root-write-leak-guard.js +4 -2
- package/dist/resources/extensions/gsd/unit-registry.js +27 -2
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +67 -5
- 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 +6 -6
- 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 +6 -6
- 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/integrations/hermes/CONTEXT.md +47 -0
- package/integrations/hermes/README.md +21 -0
- package/integrations/hermes/docs/issue-1162-grilling.md +197 -0
- package/integrations/hermes/docs/setup.md +4 -0
- package/integrations/hermes/open_gsd_hermes/commands.py +141 -0
- package/integrations/hermes/open_gsd_hermes/config.py +2 -0
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +594 -31
- package/integrations/hermes/open_gsd_hermes/notifications.py +10 -1
- package/integrations/hermes/plugin.yaml +4 -0
- package/integrations/hermes/tests/test_config.py +6 -0
- package/integrations/hermes/tests/test_gsd_client_cache.py +3 -0
- package/integrations/hermes/tests/test_gsd_client_execute.py +35 -0
- package/integrations/hermes/tests/test_gsd_client_process_cwd.py +22 -0
- package/integrations/hermes/tests/test_gsd_client_timeout.py +98 -0
- package/integrations/hermes/tests/test_milestone_client.py +447 -0
- package/integrations/hermes/tests/test_new_milestone_command.py +313 -0
- package/integrations/hermes/tests/test_supervisor_fsm.py +40 -0
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/dist/session-manager.js +1 -1
- package/packages/daemon/dist/session-manager.js.map +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.d.ts.map +1 -1
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.js +4 -1
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.js.map +1 -1
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/server.d.ts +18 -5
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +46 -5
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/session-manager.js +1 -1
- package/packages/mcp-server/dist/session-manager.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/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager-types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +3 -0
- package/packages/pi-coding-agent/dist/core/session-manager.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/claude-code-cli/stream-adapter.ts +64 -3
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +212 -39
- package/src/resources/extensions/gsd/artifact-verification.ts +16 -4
- package/src/resources/extensions/gsd/auto/closeout.ts +54 -56
- package/src/resources/extensions/gsd/auto/finalize.ts +8 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/orchestrator.ts +1 -1
- package/src/resources/extensions/gsd/auto/run-unit.ts +5 -2
- package/src/resources/extensions/gsd/auto/schedule-wakeup.ts +8 -0
- package/src/resources/extensions/gsd/auto.ts +9 -3
- package/src/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.ts +80 -12
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +230 -21
- package/src/resources/extensions/gsd/gsd-db.ts +72 -0
- package/src/resources/extensions/gsd/migrate-external.ts +14 -4
- package/src/resources/extensions/gsd/milestone-merge-transaction.ts +4 -4
- package/src/resources/extensions/gsd/repo-identity.ts +17 -0
- package/src/resources/extensions/gsd/root-write-leak-guard.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +131 -5
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +335 -0
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +152 -22
- package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +50 -6
- package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +31 -9
- package/src/resources/extensions/gsd/tests/schedule-wakeup-tool.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/unit-registry.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +2 -2
- package/src/resources/extensions/gsd/unit-registry.ts +34 -2
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +135 -11
- /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_ssgManifest.js +0 -0
|
@@ -35,6 +35,14 @@ export function consumeAutoWakeup(
|
|
|
35
35
|
return wakeup;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export function clearAutoWakeup(
|
|
39
|
+
basePath: string,
|
|
40
|
+
unitType: string,
|
|
41
|
+
unitId: string,
|
|
42
|
+
): void {
|
|
43
|
+
pendingWakeups.delete(wakeupKey(basePath, unitType, unitId));
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
export function _resetAutoWakeupsForTest(): void {
|
|
39
47
|
pendingWakeups.clear();
|
|
40
48
|
}
|
|
@@ -236,7 +236,7 @@ import {
|
|
|
236
236
|
getSlice,
|
|
237
237
|
getTask,
|
|
238
238
|
} from "./gsd-db.js";
|
|
239
|
-
import { closeWorkflowDatabase } from "./db-workspace.js";
|
|
239
|
+
import { checkpointWorkflowDatabase, closeWorkflowDatabase } from "./db-workspace.js";
|
|
240
240
|
import { markLatestActiveForWorkerCanceled } from "./db/unit-dispatches.js";
|
|
241
241
|
import { writeUnitRuntimeRecord } from "./unit-runtime.js";
|
|
242
242
|
import { countPendingCaptures } from "./captures.js";
|
|
@@ -1628,6 +1628,11 @@ export async function stopAuto(
|
|
|
1628
1628
|
const installTerminalCloseoutOutcome = completionStopRequested && preserveCloseoutTranscript;
|
|
1629
1629
|
const preserveCompletionSurface = completionStopRequested || preserveCloseoutTranscript;
|
|
1630
1630
|
s.completionStopInProgress = preserveCompletionSurface;
|
|
1631
|
+
try {
|
|
1632
|
+
checkpointWorkflowDatabase();
|
|
1633
|
+
} catch (e) {
|
|
1634
|
+
debugLog("stop-checkpoint-failed", { error: e instanceof Error ? e.message : String(e) });
|
|
1635
|
+
}
|
|
1631
1636
|
playNotificationBell("stop", loadedPreferences?.notifications);
|
|
1632
1637
|
|
|
1633
1638
|
// #4764 — telemetry: record the exit reason, isolation mode, whether an auto
|
|
@@ -2312,7 +2317,7 @@ export function buildWorktreeLifecycleDeps(): WorktreeLifecycleDeps {
|
|
|
2312
2317
|
// C4 (#5627) — GitServiceImpl constructor → gitServiceFactory
|
|
2313
2318
|
//
|
|
2314
2319
|
// Final WorktreeLifecycleDeps shape: 3 fields (gitServiceFactory,
|
|
2315
|
-
// worktreeProjection,
|
|
2320
|
+
// worktreeProjection, mergeMilestone transaction runner). Down from 18 at slice-7
|
|
2316
2321
|
// closure.
|
|
2317
2322
|
return {
|
|
2318
2323
|
gitServiceFactory: (basePath: string) => {
|
|
@@ -2321,7 +2326,7 @@ export function buildWorktreeLifecycleDeps(): WorktreeLifecycleDeps {
|
|
|
2321
2326
|
return new GitServiceImpl(basePath, gitConfig);
|
|
2322
2327
|
},
|
|
2323
2328
|
worktreeProjection: new WorktreeStateProjection(),
|
|
2324
|
-
|
|
2329
|
+
mergeMilestone: createMilestoneMergeTransaction(mergeMilestoneToMain),
|
|
2325
2330
|
};
|
|
2326
2331
|
}
|
|
2327
2332
|
|
|
@@ -2370,6 +2375,7 @@ function buildLoopDeps(pi: ExtensionAPI, ctx: ExtensionContext): LoopDeps {
|
|
|
2370
2375
|
stopAuto,
|
|
2371
2376
|
pauseAuto,
|
|
2372
2377
|
clearUnitTimeout,
|
|
2378
|
+
checkpointWorkflowDatabase,
|
|
2373
2379
|
updateProgressWidget,
|
|
2374
2380
|
...cmux,
|
|
2375
2381
|
handleLostSessionLock: (ctx: ExtensionContext | undefined, lockStatus: SessionLockStatus | undefined) => {
|
|
@@ -6,22 +6,85 @@ import type { ExtensionAPI } from "@gsd/pi-coding-agent";
|
|
|
6
6
|
|
|
7
7
|
import { getAutoRuntimeSnapshot } from "../auto-runtime-state.js";
|
|
8
8
|
import { scheduleAutoWakeup } from "../auto/schedule-wakeup.js";
|
|
9
|
+
import { logWarning } from "../workflow-logger.js";
|
|
9
10
|
import { resolveCtxCwd } from "./dynamic-tools.js";
|
|
10
11
|
|
|
11
12
|
const MAX_WAKEUP_DELAY_SECONDS = 24 * 60 * 60;
|
|
13
|
+
const INTERACTIVE_WAKEUP_CUSTOM_TYPE = "gsd-schedule-wakeup";
|
|
14
|
+
|
|
15
|
+
// One pending interactive wakeup per session, keyed by base path — the same
|
|
16
|
+
// scoping auto-mode uses for its wakeup map (see `wakeupKey`). Re-arming cancels
|
|
17
|
+
// only that session's prior timer, so repeated polling never stacks overlapping
|
|
18
|
+
// wakeups, and concurrent projects in one host process don't cancel each other.
|
|
19
|
+
const pendingInteractiveWakeups = new Map<string, ReturnType<typeof setTimeout>>();
|
|
20
|
+
|
|
21
|
+
function scheduleInteractiveWakeup(
|
|
22
|
+
pi: ExtensionAPI,
|
|
23
|
+
key: string,
|
|
24
|
+
delaySeconds: number,
|
|
25
|
+
prompt: string,
|
|
26
|
+
reason: string,
|
|
27
|
+
): void {
|
|
28
|
+
const existing = pendingInteractiveWakeups.get(key);
|
|
29
|
+
if (existing) clearTimeout(existing);
|
|
30
|
+
|
|
31
|
+
const handle = setTimeout(() => {
|
|
32
|
+
if (pendingInteractiveWakeups.get(key) === handle) {
|
|
33
|
+
pendingInteractiveWakeups.delete(key);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
void Promise.resolve(pi.sendMessage(
|
|
37
|
+
{
|
|
38
|
+
customType: INTERACTIVE_WAKEUP_CUSTOM_TYPE,
|
|
39
|
+
content: prompt,
|
|
40
|
+
display: true,
|
|
41
|
+
details: { delaySeconds, reason },
|
|
42
|
+
},
|
|
43
|
+
{ triggerTurn: true },
|
|
44
|
+
)).catch((error) => {
|
|
45
|
+
logWarning(
|
|
46
|
+
"bootstrap",
|
|
47
|
+
`ScheduleWakeup interactive dispatch failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logWarning(
|
|
52
|
+
"bootstrap",
|
|
53
|
+
`ScheduleWakeup interactive dispatch failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}, delaySeconds * 1000);
|
|
57
|
+
pendingInteractiveWakeups.set(key, handle);
|
|
58
|
+
if (
|
|
59
|
+
typeof handle === "object" &&
|
|
60
|
+
handle !== null &&
|
|
61
|
+
"unref" in handle &&
|
|
62
|
+
typeof handle.unref === "function"
|
|
63
|
+
) {
|
|
64
|
+
handle.unref();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function _resetInteractiveWakeupsForTest(): void {
|
|
69
|
+
for (const handle of pendingInteractiveWakeups.values()) {
|
|
70
|
+
clearTimeout(handle);
|
|
71
|
+
}
|
|
72
|
+
pendingInteractiveWakeups.clear();
|
|
73
|
+
}
|
|
12
74
|
|
|
13
75
|
export function registerScheduleWakeupTool(pi: ExtensionAPI): void {
|
|
14
76
|
pi.registerTool({
|
|
15
77
|
name: "ScheduleWakeup",
|
|
16
78
|
label: "Schedule Wakeup",
|
|
17
79
|
description:
|
|
18
|
-
"In GSD auto-mode,
|
|
19
|
-
"
|
|
20
|
-
promptSnippet: "Schedule a
|
|
80
|
+
"Schedule a delayed continuation turn. In GSD auto-mode, continue the current unit in the same session; " +
|
|
81
|
+
"outside auto-mode, start a new triggered turn with the supplied wakeup prompt.",
|
|
82
|
+
promptSnippet: "Schedule a wakeup prompt after a delay.",
|
|
21
83
|
promptGuidelines: [
|
|
22
84
|
"Use ScheduleWakeup at the end of an execute-task turn when waiting for a long external process.",
|
|
23
85
|
"Include a prompt that says exactly what external state to check next and what artifact to write when done.",
|
|
24
86
|
"Re-arm ScheduleWakeup on each polling turn if the external process is still running.",
|
|
87
|
+
"Outside auto-mode, use ScheduleWakeup when the user asks you to check back or poll later.",
|
|
25
88
|
],
|
|
26
89
|
parameters: Type.Object({
|
|
27
90
|
delaySeconds: Type.Number({
|
|
@@ -40,27 +103,32 @@ export function registerScheduleWakeupTool(pi: ExtensionAPI): void {
|
|
|
40
103
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
41
104
|
const dash = getAutoRuntimeSnapshot();
|
|
42
105
|
const currentUnit = dash.currentUnit;
|
|
43
|
-
const
|
|
106
|
+
const delaySeconds = Math.max(
|
|
107
|
+
1,
|
|
108
|
+
Math.min(MAX_WAKEUP_DELAY_SECONDS, Math.floor(params.delaySeconds)),
|
|
109
|
+
);
|
|
110
|
+
const reason = params.reason ?? "";
|
|
44
111
|
|
|
45
112
|
if (!dash.active || !currentUnit) {
|
|
113
|
+
const interactiveKey = dash.basePath || resolveCtxCwd(ctx);
|
|
114
|
+
scheduleInteractiveWakeup(pi, interactiveKey, delaySeconds, params.prompt, reason);
|
|
46
115
|
return {
|
|
47
|
-
content: [{
|
|
48
|
-
|
|
49
|
-
|
|
116
|
+
content: [{
|
|
117
|
+
type: "text",
|
|
118
|
+
text: `Wakeup scheduled for ${delaySeconds}s. GSD will start a new turn with the wakeup prompt.`,
|
|
119
|
+
}],
|
|
120
|
+
details: { operation: "schedule_wakeup", delaySeconds, mode: "interactive" },
|
|
50
121
|
};
|
|
51
122
|
}
|
|
52
123
|
|
|
53
|
-
const
|
|
54
|
-
1,
|
|
55
|
-
Math.min(MAX_WAKEUP_DELAY_SECONDS, Math.floor(params.delaySeconds)),
|
|
56
|
-
);
|
|
124
|
+
const basePath = dash.basePath || resolveCtxCwd(ctx);
|
|
57
125
|
scheduleAutoWakeup({
|
|
58
126
|
basePath,
|
|
59
127
|
unitType: currentUnit.type,
|
|
60
128
|
unitId: currentUnit.id,
|
|
61
129
|
delayMs: delaySeconds * 1000,
|
|
62
130
|
prompt: params.prompt,
|
|
63
|
-
reason
|
|
131
|
+
reason,
|
|
64
132
|
createdAt: Date.now(),
|
|
65
133
|
});
|
|
66
134
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { isAbsolute, join, relative } from "node:path";
|
|
2
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
3
3
|
|
|
4
4
|
import type { DoctorIssue } from "./doctor-types.js";
|
|
5
5
|
import {
|
|
6
|
+
deleteArtifactByPath,
|
|
6
7
|
getAllMilestones,
|
|
7
8
|
getMilestoneSlices,
|
|
8
9
|
getSliceTasks,
|
|
9
10
|
isDbAvailable,
|
|
10
11
|
isMemoriesFtsAvailable,
|
|
12
|
+
repairTaskCompletionFromSummary,
|
|
11
13
|
_getAdapter,
|
|
12
14
|
} from "./gsd-db.js";
|
|
13
15
|
import { MEMORIES_FTS_REBUILT_KEY } from "./db-memory-fts-schema.js";
|
|
@@ -20,9 +22,14 @@ import { readEvents } from "./workflow-events.js";
|
|
|
20
22
|
import { flushWorkflowProjections } from "./projection-flush.js";
|
|
21
23
|
import { parseRoadmapSlices } from "./roadmap-slices.js";
|
|
22
24
|
import { parsePlan } from "./parsers-legacy.js";
|
|
25
|
+
import { parseSummary } from "./files.js";
|
|
23
26
|
|
|
24
27
|
const USER_AUTHORED_ARTIFACT_TYPES = new Set(["CONTEXT", "RESEARCH"]);
|
|
25
28
|
|
|
29
|
+
function escapeRegExp(value: string): string {
|
|
30
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
function relativeFile(basePath: string, filePath: string): string {
|
|
27
34
|
return relative(basePath, filePath).split("\\").join("/");
|
|
28
35
|
}
|
|
@@ -44,6 +51,18 @@ function userContentMissingMessage(path: string, artifactType: string): string {
|
|
|
44
51
|
return `Artifact \`${path}\` is a user-authored ${type} file recorded in the database but missing from disk. Re-run \`${userContentRecoveryCommand(type)}\` in this milestone to regenerate it.`;
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
function artifactPathRelativeToGsd(artifactPath: string): string {
|
|
55
|
+
const parts = artifactPath.split(/[\\/]+/);
|
|
56
|
+
const gsdIndex = parts.lastIndexOf(".gsd");
|
|
57
|
+
if (gsdIndex < 0 || gsdIndex === parts.length - 1) return artifactPath;
|
|
58
|
+
return parts.slice(gsdIndex + 1).join("/");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isPathInside(basePath: string, candidatePath: string): boolean {
|
|
62
|
+
const rel = relative(basePath, candidatePath);
|
|
63
|
+
return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
function reportCheckboxDbStatusDivergence(
|
|
48
67
|
issues: DoctorIssue[],
|
|
49
68
|
basePath: string,
|
|
@@ -142,11 +161,137 @@ function isClearedByMilestoneShellProjectionFlush(
|
|
|
142
161
|
}
|
|
143
162
|
|
|
144
163
|
function artifactExistsOnDisk(basePath: string, artifactPath: string): boolean {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
164
|
+
return resolveArtifactDiskPath(basePath, artifactPath) !== null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function resolveArtifactDiskPath(basePath: string, artifactPath: string): string | null {
|
|
168
|
+
const relativeArtifactPath = artifactPathRelativeToGsd(artifactPath);
|
|
169
|
+
if (isAbsolute(relativeArtifactPath)) {
|
|
170
|
+
return existsSync(relativeArtifactPath) ? relativeArtifactPath : null;
|
|
171
|
+
}
|
|
172
|
+
for (const root of [gsdProjectionRoot(basePath), gsdRoot(basePath)]) {
|
|
173
|
+
const candidate = join(root, relativeArtifactPath);
|
|
174
|
+
if (isPathInside(root, candidate) && existsSync(candidate)) return candidate;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function taskExistsInPlan(basePath: string, milestoneId: string, sliceId: string, taskId: string): boolean {
|
|
180
|
+
const planPath = resolveSliceFile(basePath, milestoneId, sliceId, "PLAN");
|
|
181
|
+
if (!planPath || !existsSync(planPath)) return false;
|
|
182
|
+
try {
|
|
183
|
+
return parsePlan(readFileSync(planPath, "utf-8")).tasks.some((task) => task.id === taskId);
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isPassingVerificationResult(value: string): boolean {
|
|
190
|
+
const normalized = value.trim().toLowerCase();
|
|
191
|
+
if (!normalized) return false;
|
|
192
|
+
if (/\b(fail(?:ed|ing)?|error|blocked|mixed|untested)\b/.test(normalized)) return false;
|
|
193
|
+
if (
|
|
194
|
+
/\b(?:not|never|no|didn't|did\s+not|cannot|can't|won't|couldn't)\s+(?:\w+\s+){0,3}?(?:pass(?:ed|ing)?|success(?:ful)?|succeeded)\b/.test(
|
|
195
|
+
normalized,
|
|
196
|
+
)
|
|
197
|
+
) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return /\b(pass(?:ed|ing)?|success(?:ful)?|succeeded)\b/.test(normalized) || normalized === "all-pass";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function readTaskCompletionEvidenceFromSummary(
|
|
204
|
+
basePath: string,
|
|
205
|
+
row: {
|
|
206
|
+
path: string;
|
|
207
|
+
milestone_id: string;
|
|
208
|
+
slice_id: string | null;
|
|
209
|
+
task_id: string | null;
|
|
210
|
+
task_status: string | null;
|
|
211
|
+
task_count: number;
|
|
212
|
+
},
|
|
213
|
+
): {
|
|
214
|
+
completedAt: string;
|
|
215
|
+
verificationResult: string;
|
|
216
|
+
title: string;
|
|
217
|
+
oneLiner: string;
|
|
218
|
+
narrative: string;
|
|
219
|
+
duration: string;
|
|
220
|
+
blockerDiscovered: boolean;
|
|
221
|
+
deviations: string;
|
|
222
|
+
knownIssues: string;
|
|
223
|
+
keyFiles: string[];
|
|
224
|
+
keyDecisions: string[];
|
|
225
|
+
fullSummaryMd: string;
|
|
226
|
+
} | null {
|
|
227
|
+
if (!row.slice_id || !row.task_id) return null;
|
|
228
|
+
if (!row.task_status && Number(row.task_count) > 0 && !taskExistsInPlan(basePath, row.milestone_id, row.slice_id, row.task_id)) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const diskPath = resolveArtifactDiskPath(basePath, row.path);
|
|
232
|
+
if (!diskPath) return null;
|
|
233
|
+
try {
|
|
234
|
+
const fullSummaryMd = readFileSync(diskPath, "utf-8");
|
|
235
|
+
const summary = parseSummary(fullSummaryMd);
|
|
236
|
+
const fm = summary.frontmatter;
|
|
237
|
+
if (fm.id !== row.task_id || fm.parent !== row.slice_id || fm.milestone !== row.milestone_id) return null;
|
|
238
|
+
if (fm.blocker_discovered) return null;
|
|
239
|
+
if (!isPassingVerificationResult(fm.verification_result)) return null;
|
|
240
|
+
const completedAt = fm.completed_at.trim();
|
|
241
|
+
if (!completedAt || !Number.isFinite(Date.parse(completedAt))) return null;
|
|
242
|
+
return {
|
|
243
|
+
completedAt,
|
|
244
|
+
verificationResult: fm.verification_result.trim(),
|
|
245
|
+
title: summary.title.replace(new RegExp(`^${escapeRegExp(row.task_id)}:\\s*`), "").trim(),
|
|
246
|
+
oneLiner: summary.oneLiner,
|
|
247
|
+
narrative: summary.whatHappened,
|
|
248
|
+
duration: fm.duration,
|
|
249
|
+
blockerDiscovered: fm.blocker_discovered,
|
|
250
|
+
deviations: summary.deviations,
|
|
251
|
+
knownIssues: summary.knownLimitations,
|
|
252
|
+
keyFiles: fm.key_files.filter((file) => file !== "(none)"),
|
|
253
|
+
keyDecisions: fm.key_decisions.filter((decision) => decision !== "(none)"),
|
|
254
|
+
fullSummaryMd,
|
|
255
|
+
};
|
|
256
|
+
} catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function repairTaskArtifactDbStatusDivergence(
|
|
262
|
+
basePath: string,
|
|
263
|
+
row: {
|
|
264
|
+
path: string;
|
|
265
|
+
milestone_id: string;
|
|
266
|
+
slice_id: string | null;
|
|
267
|
+
task_id: string | null;
|
|
268
|
+
task_status: string | null;
|
|
269
|
+
task_count: number;
|
|
270
|
+
},
|
|
271
|
+
): boolean {
|
|
272
|
+
const evidence = readTaskCompletionEvidenceFromSummary(basePath, row);
|
|
273
|
+
if (!evidence || !row.slice_id || !row.task_id) return false;
|
|
274
|
+
repairTaskCompletionFromSummary({
|
|
275
|
+
milestoneId: row.milestone_id,
|
|
276
|
+
sliceId: row.slice_id,
|
|
277
|
+
taskId: row.task_id,
|
|
278
|
+
...evidence,
|
|
279
|
+
});
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function artifactDbStatusDivergenceFixable(
|
|
284
|
+
basePath: string,
|
|
285
|
+
row: {
|
|
286
|
+
path: string;
|
|
287
|
+
milestone_id: string;
|
|
288
|
+
slice_id: string | null;
|
|
289
|
+
task_id: string | null;
|
|
290
|
+
task_status: string | null;
|
|
291
|
+
task_count: number;
|
|
292
|
+
},
|
|
293
|
+
): boolean {
|
|
294
|
+
return readTaskCompletionEvidenceFromSummary(basePath, row) !== null;
|
|
150
295
|
}
|
|
151
296
|
|
|
152
297
|
function artifactUnitId(row: { milestone_id: string | null; slice_id: string | null; task_id: string | null }): string {
|
|
@@ -163,6 +308,51 @@ function artifactScope(row: { milestone_id: string | null; slice_id: string | nu
|
|
|
163
308
|
return "project";
|
|
164
309
|
}
|
|
165
310
|
|
|
311
|
+
type ArtifactRow = {
|
|
312
|
+
path: string;
|
|
313
|
+
artifact_type: string;
|
|
314
|
+
milestone_id: string | null;
|
|
315
|
+
slice_id: string | null;
|
|
316
|
+
task_id: string | null;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
function sameArtifactIdentity(left: ArtifactRow, right: ArtifactRow): boolean {
|
|
320
|
+
return left.artifact_type === right.artifact_type &&
|
|
321
|
+
left.milestone_id === right.milestone_id &&
|
|
322
|
+
left.slice_id === right.slice_id &&
|
|
323
|
+
left.task_id === right.task_id;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function isMilestonesArtifactPath(artifactPath: string): boolean {
|
|
327
|
+
return artifactPathRelativeToGsd(artifactPath).startsWith("milestones/");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function expectedMilestonesArtifactPath(row: ArtifactRow): string | null {
|
|
331
|
+
if (!row.milestone_id) return null;
|
|
332
|
+
const artifactType = normalizedArtifactType(row.artifact_type);
|
|
333
|
+
if (!artifactType) return null;
|
|
334
|
+
if (row.slice_id && row.task_id) {
|
|
335
|
+
return `milestones/${row.milestone_id}/slices/${row.slice_id}/tasks/${row.task_id}-${artifactType}.md`;
|
|
336
|
+
}
|
|
337
|
+
if (row.slice_id) {
|
|
338
|
+
return `milestones/${row.milestone_id}/slices/${row.slice_id}/${row.slice_id}-${artifactType}.md`;
|
|
339
|
+
}
|
|
340
|
+
return `milestones/${row.milestone_id}/${row.milestone_id}-${artifactType}.md`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function hasPresentMilestonesReplacement(basePath: string, row: ArtifactRow, artifactRows: ArtifactRow[]): boolean {
|
|
344
|
+
const expectedPath = expectedMilestonesArtifactPath(row);
|
|
345
|
+
if (expectedPath && artifactExistsOnDisk(basePath, expectedPath)) return true;
|
|
346
|
+
|
|
347
|
+
return artifactRows.some(
|
|
348
|
+
(other) =>
|
|
349
|
+
other.path !== row.path &&
|
|
350
|
+
isMilestonesArtifactPath(other.path) &&
|
|
351
|
+
sameArtifactIdentity(row, other) &&
|
|
352
|
+
artifactExistsOnDisk(basePath, other.path),
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
166
356
|
export async function checkEngineHealth(
|
|
167
357
|
basePath: string,
|
|
168
358
|
issues: DoctorIssue[],
|
|
@@ -386,27 +576,30 @@ export async function checkEngineHealth(
|
|
|
386
576
|
WHERE path != ''
|
|
387
577
|
ORDER BY path`,
|
|
388
578
|
)
|
|
389
|
-
.all() as
|
|
390
|
-
path: string;
|
|
391
|
-
artifact_type: string;
|
|
392
|
-
milestone_id: string | null;
|
|
393
|
-
slice_id: string | null;
|
|
394
|
-
task_id: string | null;
|
|
395
|
-
}>;
|
|
579
|
+
.all() as ArtifactRow[];
|
|
396
580
|
|
|
397
581
|
for (const row of artifactRows) {
|
|
398
582
|
if (artifactExistsOnDisk(basePath, row.path)) continue;
|
|
399
583
|
const unitId = artifactUnitId(row);
|
|
584
|
+
const issuePath = artifactPathRelativeToGsd(row.path);
|
|
585
|
+
if (options?.repair && issuePath.startsWith("phases/") && hasPresentMilestonesReplacement(basePath, row, artifactRows)) {
|
|
586
|
+
// Route the write through the Single Writer owner (gsd-db.ts) instead
|
|
587
|
+
// of issuing raw DELETE SQL here — doctor is a read-only consumer and
|
|
588
|
+
// the single-writer invariant forbids write SQL outside the allowlist.
|
|
589
|
+
deleteArtifactByPath(row.path);
|
|
590
|
+
fixesApplied.push(`pruned stale flat-phase artifact row ${row.path}`);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
400
593
|
if (isUserAuthoredArtifactType(row.artifact_type)) {
|
|
401
594
|
const artifactType = normalizedArtifactType(row.artifact_type);
|
|
402
|
-
missingUserContentArtifacts.push({ path:
|
|
595
|
+
missingUserContentArtifacts.push({ path: issuePath, artifactType });
|
|
403
596
|
issues.push({
|
|
404
597
|
severity: "warning",
|
|
405
598
|
code: "artifact_user_content_missing",
|
|
406
599
|
scope: artifactScope(row),
|
|
407
600
|
unitId,
|
|
408
|
-
message: userContentMissingMessage(
|
|
409
|
-
file:
|
|
601
|
+
message: userContentMissingMessage(issuePath, artifactType),
|
|
602
|
+
file: issuePath,
|
|
410
603
|
fixable: false,
|
|
411
604
|
});
|
|
412
605
|
continue;
|
|
@@ -416,9 +609,9 @@ export async function checkEngineHealth(
|
|
|
416
609
|
code: "artifact_file_missing",
|
|
417
610
|
scope: artifactScope(row),
|
|
418
611
|
unitId,
|
|
419
|
-
message: `Artifact ${
|
|
420
|
-
file:
|
|
421
|
-
fixable:
|
|
612
|
+
message: `Artifact ${issuePath} is recorded in the database as ${row.artifact_type || "UNKNOWN"} but no matching file exists on disk`,
|
|
613
|
+
file: issuePath,
|
|
614
|
+
fixable: issuePath.startsWith("phases/") && hasPresentMilestonesReplacement(basePath, row, artifactRows),
|
|
422
615
|
});
|
|
423
616
|
}
|
|
424
617
|
} catch {
|
|
@@ -476,13 +669,29 @@ export async function checkEngineHealth(
|
|
|
476
669
|
: row.milestone_id;
|
|
477
670
|
if (seen.has(unitId)) continue;
|
|
478
671
|
seen.add(unitId);
|
|
672
|
+
const fixable = artifactDbStatusDivergenceFixable(basePath, row);
|
|
673
|
+
let repairFailed = false;
|
|
674
|
+
if (options?.repair && fixable) {
|
|
675
|
+
try {
|
|
676
|
+
if (repairTaskArtifactDbStatusDivergence(basePath, row)) {
|
|
677
|
+
fixesApplied.push(`repaired task completion from SUMMARY artifact for ${unitId}`);
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
} catch {
|
|
681
|
+
repairFailed = true;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
479
684
|
issues.push({
|
|
480
685
|
severity: "error",
|
|
481
686
|
code: "artifact_db_status_divergence",
|
|
482
687
|
scope: row.task_id ? "task" : row.slice_id ? "slice" : "milestone",
|
|
483
688
|
unitId,
|
|
484
|
-
message:
|
|
485
|
-
|
|
689
|
+
message: repairFailed
|
|
690
|
+
? `Completion artifact ${row.path} exists while DB state for ${unitId} is still open or missing. Doctor found valid SUMMARY completion evidence but could not repair the database state.`
|
|
691
|
+
: fixable
|
|
692
|
+
? `Completion artifact ${row.path} exists while DB state for ${unitId} is still open or missing. Doctor can repair this from the SUMMARY completion evidence.`
|
|
693
|
+
: `Completion artifact ${row.path} exists while DB state for ${unitId} is still open or missing. Runtime will not import it silently; run explicit recovery/repair after review.`,
|
|
694
|
+
fixable,
|
|
486
695
|
});
|
|
487
696
|
}
|
|
488
697
|
} catch {
|
|
@@ -535,6 +535,78 @@ export function updateTaskStatus(milestoneId: string, sliceId: string, taskId: s
|
|
|
535
535
|
applyStatusTransition({ entity: "task", milestoneId, sliceId, taskId, status, completedAt });
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
+
export function repairTaskCompletionFromSummary(t: {
|
|
539
|
+
milestoneId: string;
|
|
540
|
+
sliceId: string;
|
|
541
|
+
taskId: string;
|
|
542
|
+
title?: string;
|
|
543
|
+
oneLiner?: string;
|
|
544
|
+
narrative?: string;
|
|
545
|
+
verificationResult: string;
|
|
546
|
+
duration?: string;
|
|
547
|
+
completedAt: string;
|
|
548
|
+
blockerDiscovered?: boolean;
|
|
549
|
+
deviations?: string;
|
|
550
|
+
knownIssues?: string;
|
|
551
|
+
keyFiles?: string[];
|
|
552
|
+
keyDecisions?: string[];
|
|
553
|
+
fullSummaryMd: string;
|
|
554
|
+
}): void {
|
|
555
|
+
const db = getDbOrNull();
|
|
556
|
+
if (!db) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
557
|
+
transaction(() => {
|
|
558
|
+
const seq = db.prepare(
|
|
559
|
+
`SELECT COALESCE(MAX(sequence), 0) + 1 AS next_sequence
|
|
560
|
+
FROM tasks
|
|
561
|
+
WHERE milestone_id = :mid AND slice_id = :sid`,
|
|
562
|
+
).get({ ":mid": t.milestoneId, ":sid": t.sliceId }) as { next_sequence?: number } | undefined;
|
|
563
|
+
db.prepare(
|
|
564
|
+
`INSERT INTO tasks (
|
|
565
|
+
milestone_id, slice_id, id, title, status, one_liner, narrative,
|
|
566
|
+
verification_result, duration, completed_at, blocker_discovered,
|
|
567
|
+
deviations, known_issues, key_files, key_decisions, full_summary_md,
|
|
568
|
+
sequence
|
|
569
|
+
) VALUES (
|
|
570
|
+
:milestone_id, :slice_id, :id, :title, 'complete', :one_liner, :narrative,
|
|
571
|
+
:verification_result, :duration, :completed_at, :blocker_discovered,
|
|
572
|
+
:deviations, :known_issues, :key_files, :key_decisions, :full_summary_md,
|
|
573
|
+
:sequence
|
|
574
|
+
)
|
|
575
|
+
ON CONFLICT(milestone_id, slice_id, id) DO UPDATE SET
|
|
576
|
+
title = CASE WHEN NULLIF(:title, '') IS NOT NULL THEN :title ELSE tasks.title END,
|
|
577
|
+
status = 'complete',
|
|
578
|
+
one_liner = CASE WHEN NULLIF(:one_liner, '') IS NOT NULL THEN :one_liner ELSE tasks.one_liner END,
|
|
579
|
+
narrative = CASE WHEN NULLIF(:narrative, '') IS NOT NULL THEN :narrative ELSE tasks.narrative END,
|
|
580
|
+
verification_result = :verification_result,
|
|
581
|
+
duration = CASE WHEN NULLIF(:duration, '') IS NOT NULL THEN :duration ELSE tasks.duration END,
|
|
582
|
+
completed_at = :completed_at,
|
|
583
|
+
blocker_discovered = :blocker_discovered,
|
|
584
|
+
deviations = CASE WHEN NULLIF(:deviations, '') IS NOT NULL THEN :deviations ELSE tasks.deviations END,
|
|
585
|
+
known_issues = CASE WHEN NULLIF(:known_issues, '') IS NOT NULL THEN :known_issues ELSE tasks.known_issues END,
|
|
586
|
+
key_files = :key_files,
|
|
587
|
+
key_decisions = :key_decisions,
|
|
588
|
+
full_summary_md = :full_summary_md`,
|
|
589
|
+
).run({
|
|
590
|
+
":milestone_id": t.milestoneId,
|
|
591
|
+
":slice_id": t.sliceId,
|
|
592
|
+
":id": t.taskId,
|
|
593
|
+
":title": t.title ?? "",
|
|
594
|
+
":one_liner": t.oneLiner ?? "",
|
|
595
|
+
":narrative": t.narrative ?? "",
|
|
596
|
+
":verification_result": t.verificationResult,
|
|
597
|
+
":duration": t.duration ?? "",
|
|
598
|
+
":completed_at": t.completedAt,
|
|
599
|
+
":blocker_discovered": t.blockerDiscovered ? 1 : 0,
|
|
600
|
+
":deviations": t.deviations ?? "",
|
|
601
|
+
":known_issues": t.knownIssues ?? "",
|
|
602
|
+
":key_files": JSON.stringify(t.keyFiles ?? []),
|
|
603
|
+
":key_decisions": JSON.stringify(t.keyDecisions ?? []),
|
|
604
|
+
":full_summary_md": t.fullSummaryMd,
|
|
605
|
+
":sequence": seq?.next_sequence ?? 1,
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
538
610
|
export function setTaskBlockerDiscovered(milestoneId: string, sliceId: string, taskId: string, discovered: boolean): void {
|
|
539
611
|
if (!getDbOrNull()!) return;
|
|
540
612
|
getDbOrNull()!.prepare(
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { execFileSync } from "node:child_process";
|
|
10
10
|
import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, cpSync, rmSync, statSync, symlinkSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
import { externalGsdRoot, isInsideWorktree } from "./repo-identity.js";
|
|
12
|
+
import { externalGsdRoot, externalStateAlreadyExistsForProject, isInsideWorktree } from "./repo-identity.js";
|
|
13
13
|
import { getErrorMessage } from "./error-utils.js";
|
|
14
14
|
import { hasGitTrackedGsdFiles } from "./gitignore.js";
|
|
15
15
|
import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
|
|
@@ -85,6 +85,16 @@ export function migrateToExternalState(basePath: string): MigrationResult {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// If external state already contains project data, this is not a legacy
|
|
89
|
+
// migration source. It is likely an already-migrated project whose symlink
|
|
90
|
+
// was replaced by a real directory, so copying would risk data loss.
|
|
91
|
+
if (externalStateAlreadyExistsForProject(basePath)) {
|
|
92
|
+
return {
|
|
93
|
+
migrated: false,
|
|
94
|
+
error: "External state already exists for this project; leaving local .gsd directory untouched to avoid overwriting authoritative state",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
88
98
|
const externalPath = externalGsdRoot(basePath);
|
|
89
99
|
const migratingPath = join(basePath, ".gsd.migrating");
|
|
90
100
|
|
|
@@ -94,12 +104,12 @@ export function migrateToExternalState(basePath: string): MigrationResult {
|
|
|
94
104
|
|
|
95
105
|
// Rename .gsd -> .gsd.migrating (atomic lock).
|
|
96
106
|
// On Windows, NTFS may reject rename with EPERM if file descriptors are
|
|
97
|
-
// open (VS Code watchers, antivirus on-access scan).
|
|
98
|
-
// copy+delete (#1292).
|
|
107
|
+
// open (VS Code watchers, antivirus on-access scan). WSL/DrvFs can report
|
|
108
|
+
// the same transient lock as EACCES. Fall back to copy+delete (#1292).
|
|
99
109
|
try {
|
|
100
110
|
renameSync(localGsd, migratingPath);
|
|
101
111
|
} catch (renameErr: any) {
|
|
102
|
-
if (renameErr?.code === "EPERM" || renameErr?.code === "EBUSY") {
|
|
112
|
+
if (renameErr?.code === "EPERM" || renameErr?.code === "EBUSY" || renameErr?.code === "EACCES") {
|
|
103
113
|
try {
|
|
104
114
|
cpSync(localGsd, migratingPath, { recursive: true, force: true });
|
|
105
115
|
rmSync(localGsd, { recursive: true, force: true });
|
|
@@ -21,14 +21,14 @@ export type MilestoneMergeTransactionRunner = (
|
|
|
21
21
|
) => MilestoneMergeTransactionResult;
|
|
22
22
|
|
|
23
23
|
export interface MilestoneMergeTransactionDeps {
|
|
24
|
-
|
|
24
|
+
mergeMilestone: MilestoneMergeTransactionRunner;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export function runMilestoneMergeTransaction(
|
|
28
28
|
deps: MilestoneMergeTransactionDeps,
|
|
29
29
|
input: MilestoneMergeTransactionInput,
|
|
30
30
|
): MilestoneMergeTransactionResult {
|
|
31
|
-
return deps.
|
|
31
|
+
return deps.mergeMilestone(
|
|
32
32
|
input.basePath,
|
|
33
33
|
input.milestoneId,
|
|
34
34
|
input.roadmapContent,
|
|
@@ -36,11 +36,11 @@ export function runMilestoneMergeTransaction(
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export function createMilestoneMergeTransaction(
|
|
39
|
-
|
|
39
|
+
mergeMilestone: MilestoneMergeTransactionRunner,
|
|
40
40
|
): MilestoneMergeTransactionRunner {
|
|
41
41
|
return function mergeMilestoneTransaction(basePath, milestoneId, roadmapContent) {
|
|
42
42
|
return runMilestoneMergeTransaction(
|
|
43
|
-
{
|
|
43
|
+
{ mergeMilestone },
|
|
44
44
|
{ basePath, milestoneId, roadmapContent },
|
|
45
45
|
);
|
|
46
46
|
};
|