@opengsd/gsd-pi 1.9.0-dev.e10b60d4 → 1.10.0
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 +10 -10
- 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-start.js +29 -9
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +7 -3
- package/dist/resources/extensions/gsd/auto.js +12 -1
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +21 -2
- 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/worktree-state-projection.js +20 -18
- 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 +8 -8
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- 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/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- 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 +3 -3
- 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/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/mcp-connections/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- 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 +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
- package/dist/web/standalone/.next/server/chunks/1991.js +2 -2
- package/dist/web/standalone/.next/server/chunks/2842.js +4 -4
- package/dist/web/standalone/.next/server/chunks/5124.js +1 -1
- package/dist/web/standalone/.next/server/chunks/8357.js +3 -3
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware.js +3 -3
- 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/dist/web/standalone/.next/static/chunks/app/_not-found/page-49f565245e1e4afe.js +1 -0
- package/dist/web/standalone/.next/static/chunks/app/{layout-4ae2d68984392bbf.js → layout-b35cbfff38aaf4cf.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-bc4c723c6a19c6f0.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{main-app-90d1d8d5e5d2dc6b.js → main-app-590a74400e35f685.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ec3eaffc9785ba48.js +1 -0
- package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
- package/integrations/hermes/pyproject.toml +1 -1
- package/package.json +6 -6
- 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/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-start.ts +31 -8
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +10 -3
- package/src/resources/extensions/gsd/auto.ts +13 -1
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +27 -2
- 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/tests/auto-loop.test.ts +156 -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/doctor-scope-db-unavailable.test.ts +83 -0
- package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +8 -9
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +13 -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/dist/web/standalone/.next/static/chunks/app/_not-found/page-a6fb1847f67f167c.js +0 -1
- package/dist/web/standalone/.next/static/chunks/app/page-72a856634ad14c10.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-1115d46ac61b9823.js +0 -1
- /package/dist/web/standalone/.next/static/{V9MAVyYVPboQUB9KTTAMi → EAhnhYMt5UVOiG9-iPDjc}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{V9MAVyYVPboQUB9KTTAMi → EAhnhYMt5UVOiG9-iPDjc}/_ssgManifest.js +0 -0
|
@@ -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
|
}
|
|
@@ -67,6 +67,13 @@ export type { SliceRow, TaskRow } from "./db-task-slice-rows.js";
|
|
|
67
67
|
|
|
68
68
|
import { TERMINAL_STATUS_SQL } from "./db/sql-constants.js";
|
|
69
69
|
import { applyStatusTransition } from "./db/writers/status.js";
|
|
70
|
+
import {
|
|
71
|
+
LAYOUT_SEGMENTS,
|
|
72
|
+
derivePhaseSlug,
|
|
73
|
+
milestoneIdToPhaseNum,
|
|
74
|
+
milestoneIdUniqueSuffix,
|
|
75
|
+
phaseDirName,
|
|
76
|
+
} from "./layout-policy.js";
|
|
70
77
|
|
|
71
78
|
export function insertDecision(d: Omit<Decision, "seq">): void {
|
|
72
79
|
if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
@@ -206,6 +213,47 @@ export function insertArtifact(a: {
|
|
|
206
213
|
});
|
|
207
214
|
}
|
|
208
215
|
|
|
216
|
+
function canonicalPhaseDirNameForDb(milestoneId: string, title: string): string {
|
|
217
|
+
const phaseNum = milestoneIdToPhaseNum(milestoneId);
|
|
218
|
+
const slug = derivePhaseSlug(title || milestoneId);
|
|
219
|
+
const suffix = milestoneIdUniqueSuffix(milestoneId);
|
|
220
|
+
return phaseDirName(phaseNum, suffix ? `${suffix}-${slug}` : slug);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function reconcileMilestonePhaseArtifactPaths(milestoneId: string, title: string): void {
|
|
224
|
+
const db = getDbOrNull()!;
|
|
225
|
+
const phaseNumPrefix = String(milestoneIdToPhaseNum(milestoneId)).padStart(2, "0");
|
|
226
|
+
const canonicalDir = canonicalPhaseDirNameForDb(milestoneId, title);
|
|
227
|
+
const staleRows = db.prepare(
|
|
228
|
+
`SELECT path
|
|
229
|
+
FROM artifacts
|
|
230
|
+
WHERE milestone_id = :milestone_id
|
|
231
|
+
AND path LIKE :phase_prefix`,
|
|
232
|
+
).all({
|
|
233
|
+
":milestone_id": milestoneId,
|
|
234
|
+
":phase_prefix": `${LAYOUT_SEGMENTS.level1}/${phaseNumPrefix}-%/%`,
|
|
235
|
+
}) as Array<{ path: string }>;
|
|
236
|
+
|
|
237
|
+
const updatePath = db.prepare("UPDATE artifacts SET path = :new_path WHERE path = :old_path");
|
|
238
|
+
const deletePath = db.prepare("DELETE FROM artifacts WHERE path = :path");
|
|
239
|
+
const existingPath = db.prepare("SELECT 1 AS present FROM artifacts WHERE path = :path");
|
|
240
|
+
|
|
241
|
+
for (const row of staleRows) {
|
|
242
|
+
const parts = row.path.split("/");
|
|
243
|
+
if (parts.length < 3) continue;
|
|
244
|
+
if (parts[0] !== LAYOUT_SEGMENTS.level1) continue;
|
|
245
|
+
if (parts[1] === canonicalDir) continue;
|
|
246
|
+
if (!parts[1]?.startsWith(`${phaseNumPrefix}-`)) continue;
|
|
247
|
+
|
|
248
|
+
const newPath = [LAYOUT_SEGMENTS.level1, canonicalDir, ...parts.slice(2)].join("/");
|
|
249
|
+
if (existingPath.get({ ":path": newPath })) {
|
|
250
|
+
deletePath.run({ ":path": row.path });
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
updatePath.run({ ":new_path": newPath, ":old_path": row.path });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
209
257
|
export interface MilestonePlanningRecord {
|
|
210
258
|
vision: string;
|
|
211
259
|
successCriteria: string[];
|
|
@@ -287,39 +335,43 @@ export function insertMilestone(m: {
|
|
|
287
335
|
|
|
288
336
|
export function upsertMilestonePlanning(milestoneId: string, planning: Partial<MilestonePlanningRecord> & { title?: string; status?: string; depends_on?: string[] }): void {
|
|
289
337
|
if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
338
|
+
transaction(() => {
|
|
339
|
+
getDbOrNull()!.prepare(
|
|
340
|
+
`UPDATE milestones SET
|
|
341
|
+
title = COALESCE(NULLIF(:title, ''), title),
|
|
342
|
+
status = COALESCE(NULLIF(:status, ''), status),
|
|
343
|
+
depends_on = COALESCE(:depends_on, depends_on),
|
|
344
|
+
vision = COALESCE(:vision, vision),
|
|
345
|
+
success_criteria = COALESCE(:success_criteria, success_criteria),
|
|
346
|
+
key_risks = COALESCE(:key_risks, key_risks),
|
|
347
|
+
proof_strategy = COALESCE(:proof_strategy, proof_strategy),
|
|
348
|
+
verification_contract = COALESCE(:verification_contract, verification_contract),
|
|
349
|
+
verification_integration = COALESCE(:verification_integration, verification_integration),
|
|
350
|
+
verification_operational = COALESCE(:verification_operational, verification_operational),
|
|
351
|
+
verification_uat = COALESCE(:verification_uat, verification_uat),
|
|
352
|
+
definition_of_done = COALESCE(:definition_of_done, definition_of_done),
|
|
353
|
+
requirement_coverage = COALESCE(:requirement_coverage, requirement_coverage),
|
|
354
|
+
boundary_map_markdown = COALESCE(:boundary_map_markdown, boundary_map_markdown)
|
|
355
|
+
WHERE id = :id`,
|
|
356
|
+
).run({
|
|
357
|
+
":id": milestoneId,
|
|
358
|
+
":title": planning.title ?? "",
|
|
359
|
+
":status": planning.status ?? "",
|
|
360
|
+
":depends_on": planning.depends_on ? JSON.stringify(planning.depends_on) : null,
|
|
361
|
+
":vision": planning.vision ?? null,
|
|
362
|
+
":success_criteria": planning.successCriteria ? JSON.stringify(planning.successCriteria) : null,
|
|
363
|
+
":key_risks": planning.keyRisks ? JSON.stringify(planning.keyRisks) : null,
|
|
364
|
+
":proof_strategy": planning.proofStrategy ? JSON.stringify(planning.proofStrategy) : null,
|
|
365
|
+
":verification_contract": planning.verificationContract ?? null,
|
|
366
|
+
":verification_integration": planning.verificationIntegration ?? null,
|
|
367
|
+
":verification_operational": planning.verificationOperational ?? null,
|
|
368
|
+
":verification_uat": planning.verificationUat ?? null,
|
|
369
|
+
":definition_of_done": planning.definitionOfDone ? JSON.stringify(planning.definitionOfDone) : null,
|
|
370
|
+
":requirement_coverage": planning.requirementCoverage ?? null,
|
|
371
|
+
":boundary_map_markdown": planning.boundaryMapMarkdown ?? null,
|
|
372
|
+
});
|
|
373
|
+
const finalTitle = planning.title?.trim();
|
|
374
|
+
if (finalTitle) reconcileMilestonePhaseArtifactPaths(milestoneId, finalTitle);
|
|
323
375
|
});
|
|
324
376
|
}
|
|
325
377
|
|
|
@@ -41,6 +41,7 @@ import { claimMilestoneLease } from "../db/milestone-leases.js";
|
|
|
41
41
|
import { recordDispatchClaim, markCanceled } from "../db/unit-dispatches.js";
|
|
42
42
|
import { setRuntimeKv, getRuntimeKv } from "../db/runtime-kv.js";
|
|
43
43
|
import { SourceObservationStore } from "../source-observations.js";
|
|
44
|
+
import { autoCommitCurrentBranch } from "../worktree.js";
|
|
44
45
|
|
|
45
46
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
46
47
|
|
|
@@ -832,6 +833,37 @@ test("runUnit returns cancelled when session creation fails", async () => {
|
|
|
832
833
|
assert.equal(pi.calls.length, 0);
|
|
833
834
|
});
|
|
834
835
|
|
|
836
|
+
test("runUnit returns cancelled when command context lacks newSession", async () => {
|
|
837
|
+
_resetPendingResolve();
|
|
838
|
+
|
|
839
|
+
const ctx = {
|
|
840
|
+
...makeMockCtx(),
|
|
841
|
+
ui: {
|
|
842
|
+
notify: () => {},
|
|
843
|
+
setStatus: () => {},
|
|
844
|
+
setWorkingMessage: () => {},
|
|
845
|
+
},
|
|
846
|
+
sessionManager: {
|
|
847
|
+
getEntries: () => [],
|
|
848
|
+
},
|
|
849
|
+
modelRegistry: {
|
|
850
|
+
getProviderAuthMode: () => undefined,
|
|
851
|
+
isProviderRequestReady: () => true,
|
|
852
|
+
},
|
|
853
|
+
} as any;
|
|
854
|
+
const pi = makeMockPi();
|
|
855
|
+
const s = makeMockSession();
|
|
856
|
+
s.cmdCtx = {} as any;
|
|
857
|
+
|
|
858
|
+
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
859
|
+
|
|
860
|
+
assert.equal(result.status, "cancelled");
|
|
861
|
+
assert.equal(result.errorContext?.category, "session-failed");
|
|
862
|
+
assert.equal(result.errorContext?.isTransient, false);
|
|
863
|
+
assert.match(result.errorContext?.message ?? "", /missing newSession/);
|
|
864
|
+
assert.equal(pi.calls.length, 0);
|
|
865
|
+
});
|
|
866
|
+
|
|
835
867
|
test("runUnit: TypeError from newSession is classified as structural (isTransient: false)", async () => {
|
|
836
868
|
// Regression for #572: a TypeError thrown from newSession (e.g. "something is
|
|
837
869
|
// not a function") indicates a programming error, not a transient provider
|
|
@@ -1582,6 +1614,130 @@ test("autoLoop exits when s.active is set to false", async (t) => {
|
|
|
1582
1614
|
);
|
|
1583
1615
|
});
|
|
1584
1616
|
|
|
1617
|
+
test("autoLoop stops before dispatch when command context lacks newSession", async () => {
|
|
1618
|
+
_resetPendingResolve();
|
|
1619
|
+
|
|
1620
|
+
const ctx = makeMockCtx();
|
|
1621
|
+
ctx.ui.setStatus = () => {};
|
|
1622
|
+
const pi = makeMockPi();
|
|
1623
|
+
const s = makeLoopSession({
|
|
1624
|
+
cmdCtx: {
|
|
1625
|
+
getContextUsage: () => ({ percent: 10, tokens: 1000, limit: 10000 }),
|
|
1626
|
+
},
|
|
1627
|
+
});
|
|
1628
|
+
let stopReason: string | undefined;
|
|
1629
|
+
let preserveWorktree: boolean | undefined;
|
|
1630
|
+
let deriveCalled = false;
|
|
1631
|
+
|
|
1632
|
+
try {
|
|
1633
|
+
const deps = makeMockDeps({
|
|
1634
|
+
stopAuto: async (_ctx, _pi, reason, options) => {
|
|
1635
|
+
stopReason = reason;
|
|
1636
|
+
preserveWorktree = options?.preserveWorktree;
|
|
1637
|
+
s.active = false;
|
|
1638
|
+
},
|
|
1639
|
+
deriveState: async () => {
|
|
1640
|
+
deriveCalled = true;
|
|
1641
|
+
throw new Error("deriveState should not run without command session support");
|
|
1642
|
+
},
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
await autoLoop(ctx, pi, s, deps);
|
|
1646
|
+
|
|
1647
|
+
assert.equal(stopReason, "Auto-mode has no command context for dispatch.");
|
|
1648
|
+
assert.equal(preserveWorktree, true);
|
|
1649
|
+
assert.equal(deriveCalled, false);
|
|
1650
|
+
assert.equal(pi.calls.length, 0);
|
|
1651
|
+
} finally {
|
|
1652
|
+
rmSync(s.basePath, { recursive: true, force: true });
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
test("autoLoop commits open unit work when command context lacks newSession", async () => {
|
|
1657
|
+
_resetPendingResolve();
|
|
1658
|
+
|
|
1659
|
+
const ctx = makeMockCtx();
|
|
1660
|
+
ctx.ui.setStatus = () => {};
|
|
1661
|
+
const pi = makeMockPi();
|
|
1662
|
+
const s = makeLoopSession({
|
|
1663
|
+
cmdCtx: {
|
|
1664
|
+
getContextUsage: () => ({ percent: 10, tokens: 1000, limit: 10000 }),
|
|
1665
|
+
},
|
|
1666
|
+
currentUnit: { type: "execute-task", id: "T01", startedAt: Date.now() },
|
|
1667
|
+
});
|
|
1668
|
+
const outputPath = join(s.basePath, "executor-output.txt");
|
|
1669
|
+
let autoCommitArgs: { unitType: string; unitId: string } | undefined;
|
|
1670
|
+
let preserveWorktree: boolean | undefined;
|
|
1671
|
+
|
|
1672
|
+
try {
|
|
1673
|
+
const deps = makeMockDeps({
|
|
1674
|
+
autoCommitUnit: async (basePath, unitType, unitId) => {
|
|
1675
|
+
autoCommitArgs = { unitType, unitId };
|
|
1676
|
+
return autoCommitCurrentBranch(basePath, unitType, unitId);
|
|
1677
|
+
},
|
|
1678
|
+
stopAuto: async (_ctx, _pi, _reason, options) => {
|
|
1679
|
+
preserveWorktree = options?.preserveWorktree;
|
|
1680
|
+
s.active = false;
|
|
1681
|
+
},
|
|
1682
|
+
deriveState: async () => {
|
|
1683
|
+
throw new Error("deriveState should not run without command session support");
|
|
1684
|
+
},
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
writeFileSync(outputPath, "open unit work before stop\n", "utf-8");
|
|
1688
|
+
|
|
1689
|
+
await autoLoop(ctx, pi, s, deps);
|
|
1690
|
+
|
|
1691
|
+
assert.deepEqual(autoCommitArgs, { unitType: "execute-task", unitId: "T01" });
|
|
1692
|
+
assert.equal(preserveWorktree, true);
|
|
1693
|
+
const committed = execSync("git show HEAD:executor-output.txt", {
|
|
1694
|
+
cwd: s.basePath,
|
|
1695
|
+
encoding: "utf-8",
|
|
1696
|
+
});
|
|
1697
|
+
assert.equal(committed, "open unit work before stop\n");
|
|
1698
|
+
assert.equal(pi.calls.length, 0);
|
|
1699
|
+
} finally {
|
|
1700
|
+
rmSync(s.basePath, { recursive: true, force: true });
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
test("autoLoop snapshots dirty work when unit dispatch crashes", async () => {
|
|
1705
|
+
_resetPendingResolve();
|
|
1706
|
+
|
|
1707
|
+
const ctx = makeMockCtx();
|
|
1708
|
+
ctx.ui.setStatus = () => {};
|
|
1709
|
+
const pi = makeMockPi();
|
|
1710
|
+
const s = makeLoopSession();
|
|
1711
|
+
const outputPath = join(s.basePath, "executor-output.txt");
|
|
1712
|
+
let autoCommitCalls = 0;
|
|
1713
|
+
|
|
1714
|
+
try {
|
|
1715
|
+
const deps = makeMockDeps({
|
|
1716
|
+
ensurePreconditions: () => {
|
|
1717
|
+
writeFileSync(outputPath, "saved before crash\n", "utf-8");
|
|
1718
|
+
throw new Error("dispatch crash after file write");
|
|
1719
|
+
},
|
|
1720
|
+
autoCommitUnit: async (basePath, unitType, unitId) => {
|
|
1721
|
+
autoCommitCalls++;
|
|
1722
|
+
const commitMsg = autoCommitCurrentBranch(basePath, unitType, unitId);
|
|
1723
|
+
s.active = false;
|
|
1724
|
+
return commitMsg;
|
|
1725
|
+
},
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
await autoLoop(ctx, pi, s, deps);
|
|
1729
|
+
|
|
1730
|
+
assert.equal(autoCommitCalls, 1);
|
|
1731
|
+
const committed = execSync("git show HEAD:executor-output.txt", {
|
|
1732
|
+
cwd: s.basePath,
|
|
1733
|
+
encoding: "utf-8",
|
|
1734
|
+
});
|
|
1735
|
+
assert.equal(committed, "saved before crash\n");
|
|
1736
|
+
} finally {
|
|
1737
|
+
rmSync(s.basePath, { recursive: true, force: true });
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
|
|
1585
1741
|
test("autoLoop pauses visibly when Auto Orchestration Module is not wired", async () => {
|
|
1586
1742
|
_resetPendingResolve();
|
|
1587
1743
|
|
|
@@ -4,7 +4,7 @@ import { existsSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
|
|
7
|
-
import { hasGitIndexLockForTest } from "../auto-start.ts";
|
|
7
|
+
import { hasGitIndexLockForTest, reconcileFlatPhaseBootstrapLayout } from "../auto-start.ts";
|
|
8
8
|
|
|
9
9
|
test("bootstrapAutoSession detects .git/index.lock without deleting it", (t) => {
|
|
10
10
|
const base = mkdtempSync(join(tmpdir(), "gsd-index-lock-"));
|
|
@@ -24,3 +24,23 @@ test("bootstrapAutoSession reports no index lock when the lock file is absent",
|
|
|
24
24
|
|
|
25
25
|
assert.equal(hasGitIndexLockForTest(base), false);
|
|
26
26
|
});
|
|
27
|
+
|
|
28
|
+
test("bootstrapAutoSession removes empty legacy milestones dir in flat-phase projects", (t) => {
|
|
29
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-flat-bootstrap-clean-"));
|
|
30
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
31
|
+
mkdirSync(join(base, ".gsd", "phases"), { recursive: true });
|
|
32
|
+
mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
|
|
33
|
+
|
|
34
|
+
assert.equal(reconcileFlatPhaseBootstrapLayout(base), true);
|
|
35
|
+
assert.equal(existsSync(join(base, ".gsd", "phases")), true);
|
|
36
|
+
assert.equal(existsSync(join(base, ".gsd", "milestones")), false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("bootstrapAutoSession creates phases instead of milestones for empty GSD state", (t) => {
|
|
40
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-flat-bootstrap-init-"));
|
|
41
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
42
|
+
|
|
43
|
+
assert.equal(reconcileFlatPhaseBootstrapLayout(base), true);
|
|
44
|
+
assert.equal(existsSync(join(base, ".gsd", "phases")), true);
|
|
45
|
+
assert.equal(existsSync(join(base, ".gsd", "milestones")), false);
|
|
46
|
+
});
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
import { describe, test } from "node:test";
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
|
-
import { mkdtempSync, mkdirSync, realpathSync, rmSync } from "node:fs";
|
|
6
|
+
import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
7
8
|
import { join } from "node:path";
|
|
8
9
|
import { tmpdir } from "node:os";
|
|
9
10
|
|
|
@@ -96,6 +97,78 @@ describe("hook dispatch session workspace root", () => {
|
|
|
96
97
|
assert.equal(dispatched, true);
|
|
97
98
|
assert.deepEqual(newSessionOptions, { workspaceRoot: basePath });
|
|
98
99
|
});
|
|
100
|
+
|
|
101
|
+
test("dispatchHookUnit commits in-flight work when command context lacks newSession", async (t) => {
|
|
102
|
+
const originalCwd = process.cwd();
|
|
103
|
+
const basePath = realpathSync(mkdtempSync(join(tmpdir(), "gsd-hook-nosnap-")));
|
|
104
|
+
mkdirSync(join(basePath, ".gsd"), { recursive: true });
|
|
105
|
+
execSync("git init --initial-branch=main", { cwd: basePath, stdio: "ignore" });
|
|
106
|
+
execSync("git config user.email test@test.com", { cwd: basePath, stdio: "ignore" });
|
|
107
|
+
execSync("git config user.name Test", { cwd: basePath, stdio: "ignore" });
|
|
108
|
+
execSync("git commit --allow-empty -m init", { cwd: basePath, stdio: "ignore" });
|
|
109
|
+
|
|
110
|
+
autoSession.reset();
|
|
111
|
+
autoSession.active = true;
|
|
112
|
+
autoSession.basePath = basePath;
|
|
113
|
+
autoSession.originalBasePath = basePath;
|
|
114
|
+
// Simulate an in-flight unit whose executor already wrote files, then a hook
|
|
115
|
+
// fires with a command context missing newSession.
|
|
116
|
+
autoSession.currentUnit = {
|
|
117
|
+
type: "execute-task",
|
|
118
|
+
id: "M001/S01/T01",
|
|
119
|
+
startedAt: Date.now(),
|
|
120
|
+
};
|
|
121
|
+
t.after(() => {
|
|
122
|
+
try {
|
|
123
|
+
process.chdir(originalCwd);
|
|
124
|
+
} catch {
|
|
125
|
+
// best effort cleanup after cwd-sensitive dispatch tests
|
|
126
|
+
}
|
|
127
|
+
autoSession.reset();
|
|
128
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const outputPath = join(basePath, "executor-output.txt");
|
|
132
|
+
writeFileSync(outputPath, "in-flight executor work\n", "utf-8");
|
|
133
|
+
|
|
134
|
+
// Command context intentionally lacks newSession — the guard must snapshot
|
|
135
|
+
// the dirty work before stopping instead of discarding it.
|
|
136
|
+
const ctx = {
|
|
137
|
+
ui: {
|
|
138
|
+
notify: () => {},
|
|
139
|
+
setStatus: () => {},
|
|
140
|
+
setWidget: () => {},
|
|
141
|
+
},
|
|
142
|
+
modelRegistry: {
|
|
143
|
+
getAvailable: () => [],
|
|
144
|
+
},
|
|
145
|
+
sessionManager: {
|
|
146
|
+
getSessionFile: () => join(basePath, "session.jsonl"),
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
const pi = {
|
|
150
|
+
sendMessage: () => {},
|
|
151
|
+
setModel: async () => true,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const dispatched = await dispatchHookUnit(
|
|
155
|
+
ctx as any,
|
|
156
|
+
pi as any,
|
|
157
|
+
"review",
|
|
158
|
+
"execute-task",
|
|
159
|
+
"M001/S01/T01",
|
|
160
|
+
"review the completed unit",
|
|
161
|
+
undefined,
|
|
162
|
+
basePath,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
assert.equal(dispatched, false);
|
|
166
|
+
const committed = execSync("git show HEAD:executor-output.txt", {
|
|
167
|
+
cwd: basePath,
|
|
168
|
+
encoding: "utf-8",
|
|
169
|
+
});
|
|
170
|
+
assert.equal(committed, "in-flight executor work\n");
|
|
171
|
+
});
|
|
99
172
|
});
|
|
100
173
|
|
|
101
174
|
describe("deep setup approval questions pause immediately", () => {
|
|
@@ -167,6 +167,45 @@ test("checkEngineHealth keeps PLAN checkbox divergence after stale projection fl
|
|
|
167
167
|
assert.match(readFileSync(plan.planPath, "utf-8"), /- \[ \] \*\*T01\*\*:/);
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
+
test("checkEngineHealth ignores stale suffixed flat-phase duplicate when bare milestone exists", async (t) => {
|
|
171
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-checkbox-flat-duplicate-"));
|
|
172
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
173
|
+
|
|
174
|
+
const gsdDir = join(base, ".gsd");
|
|
175
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
176
|
+
|
|
177
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
178
|
+
insertMilestone({ id: "M003", title: "M003-vaz73w: New milestone M003-vaz73w", status: "active" });
|
|
179
|
+
insertSlice({ id: "S01", milestoneId: "M003", title: "Slice", status: "pending", risk: "low", depends: [], sequence: 1 });
|
|
180
|
+
for (const taskId of ["T01", "T02", "T03"]) {
|
|
181
|
+
insertTask({ id: taskId, milestoneId: "M003", sliceId: "S01", title: taskId, status: "complete", sequence: Number(taskId.slice(1)) });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
insertMilestone({ id: "M003-vaz73w", title: "New milestone M003-vaz73w", status: "active" });
|
|
185
|
+
insertSlice({ id: "S01", milestoneId: "M003-vaz73w", title: "Slice", status: "pending", risk: "low", depends: [], sequence: 1 });
|
|
186
|
+
for (const taskId of ["T01", "T02", "T03"]) {
|
|
187
|
+
insertTask({ id: taskId, milestoneId: "M003-vaz73w", sliceId: "S01", title: taskId, status: "complete", sequence: Number(taskId.slice(1)) });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const canonicalPlan = await renderPlanFromDb(base, "M003", "S01");
|
|
191
|
+
const staleDir = join(gsdDir, "phases", "03-vaz73w-new-milestone-m003-vaz73w");
|
|
192
|
+
mkdirSync(staleDir, { recursive: true });
|
|
193
|
+
writeFileSync(
|
|
194
|
+
join(staleDir, "03-01-PLAN.md"),
|
|
195
|
+
canonicalPlan.content.replace("- [x] **T03**:", "- [ ] **T03**:"),
|
|
196
|
+
"utf-8",
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const issues: any[] = [];
|
|
200
|
+
await checkEngineHealth(base, issues, []);
|
|
201
|
+
|
|
202
|
+
assert.deepEqual(
|
|
203
|
+
issues.filter((issue) => issue.code === "checkbox_db_status_divergence").map((issue) => issue.unitId),
|
|
204
|
+
[],
|
|
205
|
+
"stale suffixed duplicate phase projection must not compete with the bare milestone projection",
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
170
209
|
test("checkEngineHealth reads task checkboxes from the <tasks> block, not a stray line above it", async (t) => {
|
|
171
210
|
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-task-section-"));
|
|
172
211
|
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
@@ -428,6 +467,50 @@ test("checkEngineHealth repair prunes stale phases artifact rows with present mi
|
|
|
428
467
|
assert.deepEqual(rows.map((row) => row.path), []);
|
|
429
468
|
});
|
|
430
469
|
|
|
470
|
+
test("checkEngineHealth repair prunes stale phases artifact rows with renamed flat-phase files", async (t) => {
|
|
471
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-prune-renamed-flat-phase-artifact-"));
|
|
472
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
473
|
+
|
|
474
|
+
const gsdDir = join(base, ".gsd");
|
|
475
|
+
const stalePath = "phases/01-new-milestone-m001/01-01-PLAN.md";
|
|
476
|
+
const replacementPath = "phases/01-lokably-brand-foundation-and-welcome-pag/01-01-PLAN.md";
|
|
477
|
+
mkdirSync(join(gsdDir, "phases", "01-lokably-brand-foundation-and-welcome-pag"), { recursive: true });
|
|
478
|
+
writeFileSync(join(gsdDir, replacementPath), "# Plan\n", "utf-8");
|
|
479
|
+
|
|
480
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
481
|
+
insertArtifact({
|
|
482
|
+
path: stalePath,
|
|
483
|
+
artifact_type: "PLAN",
|
|
484
|
+
milestone_id: "M001",
|
|
485
|
+
slice_id: "S01",
|
|
486
|
+
task_id: null,
|
|
487
|
+
full_content: "# stale plan\n",
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const beforeIssues: any[] = [];
|
|
491
|
+
await checkEngineHealth(base, beforeIssues, []);
|
|
492
|
+
|
|
493
|
+
const beforeIssue = beforeIssues.find((issue) => issue.code === "artifact_file_missing" && issue.file === stalePath);
|
|
494
|
+
assert.ok(beforeIssue, "stale renamed phases row should be reported when repair is off");
|
|
495
|
+
assert.equal(beforeIssue.fixable, true, "renamed phases rows with a flat replacement should be fixable");
|
|
496
|
+
|
|
497
|
+
const repairIssues: any[] = [];
|
|
498
|
+
const fixes: string[] = [];
|
|
499
|
+
await checkEngineHealth(base, repairIssues, fixes, { repair: true });
|
|
500
|
+
|
|
501
|
+
assert.equal(
|
|
502
|
+
repairIssues.some((issue) => issue.code === "artifact_file_missing" && issue.file === stalePath),
|
|
503
|
+
false,
|
|
504
|
+
"repair should not report stale rows it pruned",
|
|
505
|
+
);
|
|
506
|
+
assert.ok(fixes.includes(`pruned stale flat-phase artifact row ${stalePath}`));
|
|
507
|
+
|
|
508
|
+
const rows = _getAdapter()!
|
|
509
|
+
.prepare("SELECT path FROM artifacts ORDER BY path")
|
|
510
|
+
.all() as Array<{ path: string }>;
|
|
511
|
+
assert.deepEqual(rows.map((row) => row.path), []);
|
|
512
|
+
});
|
|
513
|
+
|
|
431
514
|
test("checkEngineHealth repair keeps phases rows whose own file is still present", async (t) => {
|
|
432
515
|
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-keep-present-phase-artifact-"));
|
|
433
516
|
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
@@ -122,7 +122,7 @@ test("migrateToFlatPhase creates a backup", async () => {
|
|
|
122
122
|
assert.ok(backups.length >= 1, "at least one migrate-* backup dir should exist");
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
test("migrateToFlatPhase
|
|
125
|
+
test("migrateToFlatPhase removes disposable phases snapshot after successful migration", async () => {
|
|
126
126
|
const base = makeTmp();
|
|
127
127
|
const reviewPath = join(base, ".gsd", "phases", "01-foundation", "PLAN-REVIEW.md");
|
|
128
128
|
mkdirSync(join(base, ".gsd", "phases", "01-foundation"), { recursive: true });
|
|
@@ -133,11 +133,10 @@ test("migrateToFlatPhase backs up existing phases projection before clearing it"
|
|
|
133
133
|
const backupRoot = join(base, ".gsd-backups");
|
|
134
134
|
const backups = readdirSync(backupRoot).filter(d => d.startsWith("migrate-"));
|
|
135
135
|
assert.equal(backups.length, 1, "one migrate backup should exist");
|
|
136
|
-
const backedUpReview = join(backupRoot, backups[0]!, "__phases", "01-foundation", "PLAN-REVIEW.md");
|
|
137
136
|
assert.equal(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"
|
|
137
|
+
existsSync(join(backupRoot, backups[0]!, "__phases")),
|
|
138
|
+
false,
|
|
139
|
+
"temporary phases/ recovery snapshot should be removed after success",
|
|
141
140
|
);
|
|
142
141
|
});
|
|
143
142
|
|
|
@@ -202,7 +201,7 @@ test("failed migration rolls back without leaking a .gsd-backups/migrate-* dir",
|
|
|
202
201
|
assert.equal(leaked.length, 0, "rollback must delete the migrate-* backup it created");
|
|
203
202
|
});
|
|
204
203
|
|
|
205
|
-
test("resumed migration
|
|
204
|
+
test("resumed migration removes disposable phases snapshot after success", async () => {
|
|
206
205
|
const base = makeTmp();
|
|
207
206
|
const milestonesPath = join(base, ".gsd", "milestones");
|
|
208
207
|
const migratingPath = join(base, ".gsd", "milestones.migrating");
|
|
@@ -218,9 +217,9 @@ test("resumed migration stores phases backup in retained migrate snapshot", asyn
|
|
|
218
217
|
const backups = readdirSync(backupRoot).filter((d) => d.startsWith("migrate-"));
|
|
219
218
|
assert.equal(backups.length, 1, "resumed migration should create one retained migrate backup");
|
|
220
219
|
assert.equal(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
"
|
|
220
|
+
existsSync(join(backupRoot, backups[0]!, "__phases")),
|
|
221
|
+
false,
|
|
222
|
+
"temporary phases/ recovery snapshot should be removed after resumed success",
|
|
224
223
|
);
|
|
225
224
|
assert.equal(
|
|
226
225
|
existsSync(join(migratingPath, "__phases")),
|
|
@@ -46,6 +46,9 @@ import {
|
|
|
46
46
|
tryCreateMemoriesFts,
|
|
47
47
|
_isLikelyWslDrvFsPathForTest,
|
|
48
48
|
_shouldAttemptVacuumRecoveryForTest,
|
|
49
|
+
insertArtifact,
|
|
50
|
+
getArtifact,
|
|
51
|
+
upsertMilestonePlanning,
|
|
49
52
|
} from '../gsd-db.ts';
|
|
50
53
|
import { MigrationBackupError } from '../db-migration-backup.ts';
|
|
51
54
|
import { _resetLogs, peekLogs, setStderrLoggingEnabled } from '../workflow-logger.ts';
|
|
@@ -357,6 +360,35 @@ describe('gsd-db', () => {
|
|
|
357
360
|
closeDatabase();
|
|
358
361
|
});
|
|
359
362
|
|
|
363
|
+
test("gsd-db: upsertMilestonePlanning rewrites placeholder phase artifact paths to final title slug", () => {
|
|
364
|
+
openDatabase(":memory:");
|
|
365
|
+
|
|
366
|
+
const stalePath = "phases/01-new-milestone-m001/01-ROADMAP.md";
|
|
367
|
+
const finalPath = "phases/01-lokably-brand-foundation-and-welcome-pag/01-ROADMAP.md";
|
|
368
|
+
insertMilestone({ id: "M001", title: "New milestone M001", status: "active" });
|
|
369
|
+
insertArtifact({
|
|
370
|
+
path: stalePath,
|
|
371
|
+
artifact_type: "ROADMAP",
|
|
372
|
+
milestone_id: "M001",
|
|
373
|
+
slice_id: null,
|
|
374
|
+
task_id: null,
|
|
375
|
+
full_content: "# Placeholder roadmap\n",
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
upsertMilestonePlanning("M001", {
|
|
379
|
+
title: "Lokably brand foundation and welcome page rebuild",
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
assert.equal(getArtifact(stalePath), null, "placeholder-derived artifact row should be removed");
|
|
383
|
+
assert.equal(
|
|
384
|
+
getArtifact(finalPath)?.full_content,
|
|
385
|
+
"# Placeholder roadmap\n",
|
|
386
|
+
"artifact row should move to the final title-derived phase path",
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
closeDatabase();
|
|
390
|
+
});
|
|
391
|
+
|
|
360
392
|
test('gsd-db: active_requirements view excludes superseded', () => {
|
|
361
393
|
openDatabase(':memory:');
|
|
362
394
|
|