gsd-pi 2.48.0-dev.ced2eca → 2.49.0-dev.9e177e9
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-ui.js +12 -2
- package/dist/headless.js +29 -13
- package/dist/resources/extensions/gsd/auto/infra-errors.js +1 -0
- package/dist/resources/extensions/gsd/auto/phases.js +11 -11
- package/dist/resources/extensions/gsd/auto/resolve.js +2 -2
- package/dist/resources/extensions/gsd/auto/run-unit.js +2 -2
- package/dist/resources/extensions/gsd/auto/session.js +4 -0
- package/dist/resources/extensions/gsd/auto-artifact-paths.js +8 -10
- package/dist/resources/extensions/gsd/auto-dashboard.js +6 -3
- package/dist/resources/extensions/gsd/auto-dispatch.js +33 -21
- package/dist/resources/extensions/gsd/auto-post-unit.js +17 -24
- package/dist/resources/extensions/gsd/auto-prompts.js +102 -21
- package/dist/resources/extensions/gsd/auto-recovery.js +62 -184
- package/dist/resources/extensions/gsd/auto-start.js +4 -31
- package/dist/resources/extensions/gsd/auto-timers.js +2 -2
- package/dist/resources/extensions/gsd/auto-verification.js +4 -7
- package/dist/resources/extensions/gsd/auto-worktree.js +257 -113
- package/dist/resources/extensions/gsd/auto.js +7 -5
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +89 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -1
- package/dist/resources/extensions/gsd/branch-patterns.js +13 -0
- package/dist/resources/extensions/gsd/doctor-checks.js +5 -1234
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +168 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +28 -7
- package/dist/resources/extensions/gsd/doctor-git-checks.js +405 -0
- package/dist/resources/extensions/gsd/doctor-global-checks.js +74 -0
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +600 -0
- package/dist/resources/extensions/gsd/doctor.js +9 -1
- package/dist/resources/extensions/gsd/extension-manifest.json +1 -1
- package/dist/resources/extensions/gsd/git-service.js +9 -10
- package/dist/resources/extensions/gsd/gsd-db.js +124 -1
- package/dist/resources/extensions/gsd/guided-flow-queue.js +10 -11
- package/dist/resources/extensions/gsd/markdown-renderer.js +33 -5
- package/dist/resources/extensions/gsd/preferences-types.js +2 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +39 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +27 -8
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +9 -8
- package/dist/resources/extensions/gsd/prompts/execute-task.md +16 -13
- package/dist/resources/extensions/gsd/prompts/forensics.md +12 -5
- package/dist/resources/extensions/gsd/prompts/gate-evaluate.md +32 -0
- package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +8 -3
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +3 -0
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +1 -1
- package/dist/resources/extensions/gsd/repo-identity.js +29 -0
- package/dist/resources/extensions/gsd/roadmap-slices.js +2 -2
- package/dist/resources/extensions/gsd/session-forensics.js +6 -11
- package/dist/resources/extensions/gsd/session-lock.js +67 -56
- package/dist/resources/extensions/gsd/state.js +34 -7
- package/dist/resources/extensions/gsd/templates/milestone-summary.md +8 -0
- package/dist/resources/extensions/gsd/templates/plan.md +16 -0
- package/dist/resources/extensions/gsd/templates/roadmap.md +13 -0
- package/dist/resources/extensions/gsd/templates/slice-summary.md +9 -0
- package/dist/resources/extensions/gsd/templates/task-plan.md +24 -0
- package/dist/resources/extensions/gsd/tools/plan-slice.js +14 -1
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +3 -3
- package/dist/resources/extensions/gsd/verdict-parser.js +84 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +24 -0
- package/dist/resources/extensions/gsd/worktree.js +3 -2
- package/dist/resources/extensions/remote-questions/config.js +3 -5
- package/dist/resources/extensions/search-the-web/native-search.js +8 -3
- package/dist/resources/extensions/search-the-web/tool-search.js +19 -2
- package/dist/resources/skills/github-workflows/references/gh/SKILL.md +22 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +19 -19
- 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/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- 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 +19 -19
- package/dist/web/standalone/.next/server/chunks/229.js +2 -2
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/4024.7c75ac378de0f2b5.js +9 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-0a4cd455ec4197d2.js → webpack-2473ce2c3879fff4.js} +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +4 -1
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.ts +4 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js +39 -10
- package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
- package/packages/pi-ai/src/providers/openai-codex-responses.ts +39 -8
- package/packages/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/blob-store.js +8 -3
- package/packages/pi-coding-agent/dist/core/blob-store.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.js +9 -2
- package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -32
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/jsonl.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/jsonl.js +5 -0
- package/packages/pi-coding-agent/dist/modes/rpc/jsonl.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js +0 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/blob-store.ts +6 -3
- package/packages/pi-coding-agent/src/core/discovery-cache.ts +9 -2
- package/packages/pi-coding-agent/src/core/retry-handler.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +7 -32
- package/packages/pi-coding-agent/src/modes/rpc/jsonl.ts +6 -0
- package/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts +0 -2
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/infra-errors.ts +1 -0
- package/src/resources/extensions/gsd/auto/phases.ts +10 -11
- package/src/resources/extensions/gsd/auto/resolve.ts +3 -3
- package/src/resources/extensions/gsd/auto/run-unit.ts +2 -2
- package/src/resources/extensions/gsd/auto/session.ts +5 -0
- package/src/resources/extensions/gsd/auto/types.ts +13 -0
- package/src/resources/extensions/gsd/auto-artifact-paths.ts +19 -21
- package/src/resources/extensions/gsd/auto-dashboard.ts +5 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +39 -21
- package/src/resources/extensions/gsd/auto-loop.ts +1 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +18 -28
- package/src/resources/extensions/gsd/auto-prompts.ts +113 -19
- package/src/resources/extensions/gsd/auto-recovery.ts +65 -199
- package/src/resources/extensions/gsd/auto-start.ts +7 -27
- package/src/resources/extensions/gsd/auto-timers.ts +2 -2
- package/src/resources/extensions/gsd/auto-verification.ts +4 -7
- package/src/resources/extensions/gsd/auto-worktree.ts +305 -108
- package/src/resources/extensions/gsd/auto.ts +11 -10
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +93 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +8 -0
- package/src/resources/extensions/gsd/branch-patterns.ts +16 -0
- package/src/resources/extensions/gsd/doctor-checks.ts +5 -1291
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +182 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +30 -7
- package/src/resources/extensions/gsd/doctor-git-checks.ts +415 -0
- package/src/resources/extensions/gsd/doctor-global-checks.ts +84 -0
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +626 -0
- package/src/resources/extensions/gsd/doctor.ts +9 -1
- package/src/resources/extensions/gsd/extension-manifest.json +1 -1
- package/src/resources/extensions/gsd/git-service.ts +7 -15
- package/src/resources/extensions/gsd/gsd-db.ts +150 -2
- package/src/resources/extensions/gsd/guided-flow-queue.ts +11 -12
- package/src/resources/extensions/gsd/markdown-renderer.ts +37 -4
- package/src/resources/extensions/gsd/preferences-types.ts +5 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +37 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +27 -8
- package/src/resources/extensions/gsd/prompts/complete-slice.md +9 -8
- package/src/resources/extensions/gsd/prompts/execute-task.md +16 -13
- package/src/resources/extensions/gsd/prompts/forensics.md +12 -5
- package/src/resources/extensions/gsd/prompts/gate-evaluate.md +32 -0
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +8 -3
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +3 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +1 -1
- package/src/resources/extensions/gsd/repo-identity.ts +28 -0
- package/src/resources/extensions/gsd/roadmap-slices.ts +2 -2
- package/src/resources/extensions/gsd/session-forensics.ts +6 -11
- package/src/resources/extensions/gsd/session-lock.ts +92 -64
- package/src/resources/extensions/gsd/state.ts +38 -5
- package/src/resources/extensions/gsd/templates/milestone-summary.md +8 -0
- package/src/resources/extensions/gsd/templates/plan.md +16 -0
- package/src/resources/extensions/gsd/templates/roadmap.md +13 -0
- package/src/resources/extensions/gsd/templates/slice-summary.md +9 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +24 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +1 -81
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +9 -12
- package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +115 -1
- package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +65 -1
- package/src/resources/extensions/gsd/tests/doctor-git.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/gate-dispatch.test.ts +189 -0
- package/src/resources/extensions/gsd/tests/gate-storage.test.ts +156 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/infra-error.test.ts +12 -2
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/quality-gates.test.ts +347 -0
- package/src/resources/extensions/gsd/tests/queue-completed-milestone-perf.test.ts +155 -0
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +20 -16
- package/src/resources/extensions/gsd/tests/session-lock-transient-read.test.ts +223 -0
- package/src/resources/extensions/gsd/tests/skill-activation.test.ts +44 -4
- package/src/resources/extensions/gsd/tests/tool-naming.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +0 -16
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +67 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-sync-overwrite-loop.test.ts +204 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +16 -0
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +3 -3
- package/src/resources/extensions/gsd/types.ts +30 -0
- package/src/resources/extensions/gsd/verdict-parser.ts +95 -0
- package/src/resources/extensions/gsd/verification-gate.ts +0 -2
- package/src/resources/extensions/gsd/worktree-resolver.ts +31 -0
- package/src/resources/extensions/gsd/worktree.ts +3 -2
- package/src/resources/extensions/remote-questions/config.ts +3 -5
- package/src/resources/extensions/search-the-web/native-search.ts +8 -3
- package/src/resources/extensions/search-the-web/tool-search.ts +22 -2
- package/src/resources/skills/github-workflows/references/gh/SKILL.md +22 -1
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +0 -191
- package/dist/resources/extensions/gsd/resource-version.js +0 -97
- package/dist/web/standalone/.next/static/chunks/4024.11ca5c01938e5948.js +0 -9
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +0 -234
- package/src/resources/extensions/gsd/resource-version.ts +0 -101
- /package/dist/web/standalone/.next/static/{PTL5V00OW8q4-092tUQKx → vNN0h0emdEi8l_npi8poE}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{PTL5V00OW8q4-092tUQKx → vNN0h0emdEi8l_npi8poE}/_ssgManifest.js +0 -0
|
@@ -1,1234 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { isDbAvailable, _getAdapter, getMilestoneSlices } from "./gsd-db.js";
|
|
7
|
-
import { resolveMilestoneFile, milestonesDir, gsdRoot, resolveGsdRootFile } from "./paths.js";
|
|
8
|
-
import { deriveState, isMilestoneComplete } from "./state.js";
|
|
9
|
-
import { saveFile } from "./files.js";
|
|
10
|
-
import { listWorktrees, resolveGitDir, worktreesDir } from "./worktree-manager.js";
|
|
11
|
-
import { abortAndReset } from "./git-self-heal.js";
|
|
12
|
-
import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch, writeIntegrationBranch } from "./git-service.js";
|
|
13
|
-
import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeForEachRef, nativeUpdateRef } from "./native-git-bridge.js";
|
|
14
|
-
import { readCrashLock, isLockProcessAlive, clearLock } from "./crash-recovery.js";
|
|
15
|
-
import { ensureGitignore } from "./gitignore.js";
|
|
16
|
-
import { getAllWorktreeHealth } from "./worktree-health.js";
|
|
17
|
-
import { readAllSessionStatuses, isSessionStale, removeSessionStatus } from "./session-status-io.js";
|
|
18
|
-
import { recoverFailedMigration } from "./migrate-external.js";
|
|
19
|
-
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
20
|
-
import { readEvents } from "./workflow-events.js";
|
|
21
|
-
import { renderAllProjections } from "./workflow-projections.js";
|
|
22
|
-
export async function checkGitHealth(basePath, issues, fixesApplied, shouldFix, isolationMode = "none") {
|
|
23
|
-
// Degrade gracefully if not a git repo
|
|
24
|
-
if (!nativeIsRepo(basePath)) {
|
|
25
|
-
return; // Not a git repo — skip all git health checks
|
|
26
|
-
}
|
|
27
|
-
const gitDir = resolveGitDir(basePath);
|
|
28
|
-
// ── Orphaned auto-worktrees & Stale milestone branches ────────────────
|
|
29
|
-
// These checks only apply in worktree/branch modes — skip in none mode
|
|
30
|
-
// where no milestone worktrees or branches are created.
|
|
31
|
-
if (isolationMode !== "none") {
|
|
32
|
-
try {
|
|
33
|
-
const worktrees = listWorktrees(basePath);
|
|
34
|
-
const milestoneWorktrees = worktrees.filter(wt => wt.branch.startsWith("milestone/"));
|
|
35
|
-
// Load roadmap state once for cross-referencing
|
|
36
|
-
const state = await deriveState(basePath);
|
|
37
|
-
for (const wt of milestoneWorktrees) {
|
|
38
|
-
// Extract milestone ID from branch name "milestone/M001" → "M001"
|
|
39
|
-
const milestoneId = wt.branch.replace(/^milestone\//, "");
|
|
40
|
-
const milestoneEntry = state.registry.find(m => m.id === milestoneId);
|
|
41
|
-
// Check if milestone is complete via roadmap
|
|
42
|
-
let isComplete = false;
|
|
43
|
-
if (milestoneEntry) {
|
|
44
|
-
if (isDbAvailable()) {
|
|
45
|
-
const dbSlices = getMilestoneSlices(milestoneId);
|
|
46
|
-
isComplete = dbSlices.length > 0 && dbSlices.every(s => s.status === "complete");
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
50
|
-
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
|
|
51
|
-
if (roadmapContent) {
|
|
52
|
-
const roadmap = parseLegacyRoadmap(roadmapContent);
|
|
53
|
-
isComplete = isMilestoneComplete(roadmap);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
// When DB unavailable and no roadmap, isComplete stays false
|
|
57
|
-
}
|
|
58
|
-
if (isComplete) {
|
|
59
|
-
issues.push({
|
|
60
|
-
severity: "warning",
|
|
61
|
-
code: "orphaned_auto_worktree",
|
|
62
|
-
scope: "milestone",
|
|
63
|
-
unitId: milestoneId,
|
|
64
|
-
message: `Worktree for completed milestone ${milestoneId} still exists at ${wt.path}`,
|
|
65
|
-
fixable: true,
|
|
66
|
-
});
|
|
67
|
-
if (shouldFix("orphaned_auto_worktree")) {
|
|
68
|
-
// Never remove a worktree matching current working directory
|
|
69
|
-
const cwd = process.cwd();
|
|
70
|
-
if (wt.path === cwd || cwd.startsWith(wt.path + sep)) {
|
|
71
|
-
fixesApplied.push(`skipped removing worktree at ${wt.path} (is cwd)`);
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
try {
|
|
75
|
-
nativeWorktreeRemove(basePath, wt.path, true);
|
|
76
|
-
fixesApplied.push(`removed orphaned worktree ${wt.path}`);
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
fixesApplied.push(`failed to remove worktree ${wt.path}`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
// ── Stale milestone branches ─────────────────────────────────────────
|
|
86
|
-
try {
|
|
87
|
-
const branches = nativeBranchList(basePath, "milestone/*");
|
|
88
|
-
if (branches.length > 0) {
|
|
89
|
-
const worktreeBranches = new Set(milestoneWorktrees.map(wt => wt.branch));
|
|
90
|
-
for (const branch of branches) {
|
|
91
|
-
// Skip branches that have a worktree (handled above)
|
|
92
|
-
if (worktreeBranches.has(branch))
|
|
93
|
-
continue;
|
|
94
|
-
const milestoneId = branch.replace(/^milestone\//, "");
|
|
95
|
-
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
96
|
-
let branchMilestoneComplete = false;
|
|
97
|
-
if (isDbAvailable()) {
|
|
98
|
-
const dbSlices = getMilestoneSlices(milestoneId);
|
|
99
|
-
branchMilestoneComplete = dbSlices.length > 0 && dbSlices.every(s => s.status === "complete");
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
|
|
103
|
-
if (!roadmapContent)
|
|
104
|
-
continue;
|
|
105
|
-
const roadmap = parseLegacyRoadmap(roadmapContent);
|
|
106
|
-
branchMilestoneComplete = isMilestoneComplete(roadmap);
|
|
107
|
-
}
|
|
108
|
-
if (branchMilestoneComplete) {
|
|
109
|
-
issues.push({
|
|
110
|
-
severity: "info",
|
|
111
|
-
code: "stale_milestone_branch",
|
|
112
|
-
scope: "milestone",
|
|
113
|
-
unitId: milestoneId,
|
|
114
|
-
message: `Branch ${branch} exists for completed milestone ${milestoneId}`,
|
|
115
|
-
fixable: true,
|
|
116
|
-
});
|
|
117
|
-
if (shouldFix("stale_milestone_branch")) {
|
|
118
|
-
try {
|
|
119
|
-
nativeBranchDelete(basePath, branch, true);
|
|
120
|
-
fixesApplied.push(`deleted stale branch ${branch}`);
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
fixesApplied.push(`failed to delete branch ${branch}`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
// git branch list failed — skip stale branch check
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
// listWorktrees or deriveState failed — skip worktree/branch checks
|
|
136
|
-
}
|
|
137
|
-
} // end isolationMode !== "none"
|
|
138
|
-
// ── Corrupt merge state ────────────────────────────────────────────────
|
|
139
|
-
try {
|
|
140
|
-
const mergeStateFiles = ["MERGE_HEAD", "SQUASH_MSG"];
|
|
141
|
-
const mergeStateDirs = ["rebase-apply", "rebase-merge"];
|
|
142
|
-
const found = [];
|
|
143
|
-
for (const f of mergeStateFiles) {
|
|
144
|
-
if (existsSync(join(gitDir, f)))
|
|
145
|
-
found.push(f);
|
|
146
|
-
}
|
|
147
|
-
for (const d of mergeStateDirs) {
|
|
148
|
-
if (existsSync(join(gitDir, d)))
|
|
149
|
-
found.push(d);
|
|
150
|
-
}
|
|
151
|
-
if (found.length > 0) {
|
|
152
|
-
issues.push({
|
|
153
|
-
severity: "error",
|
|
154
|
-
code: "corrupt_merge_state",
|
|
155
|
-
scope: "project",
|
|
156
|
-
unitId: "project",
|
|
157
|
-
message: `Corrupt merge/rebase state detected: ${found.join(", ")}`,
|
|
158
|
-
fixable: true,
|
|
159
|
-
});
|
|
160
|
-
if (shouldFix("corrupt_merge_state")) {
|
|
161
|
-
const result = abortAndReset(basePath);
|
|
162
|
-
fixesApplied.push(`cleaned merge state: ${result.cleaned.join(", ")}`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
// Can't check .git dir — skip
|
|
168
|
-
}
|
|
169
|
-
// ── Tracked runtime files ──────────────────────────────────────────────
|
|
170
|
-
try {
|
|
171
|
-
const trackedPaths = [];
|
|
172
|
-
for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
|
|
173
|
-
try {
|
|
174
|
-
const files = nativeLsFiles(basePath, exclusion);
|
|
175
|
-
if (files.length > 0) {
|
|
176
|
-
trackedPaths.push(...files);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
// Individual ls-files can fail — continue
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
if (trackedPaths.length > 0) {
|
|
184
|
-
issues.push({
|
|
185
|
-
severity: "warning",
|
|
186
|
-
code: "tracked_runtime_files",
|
|
187
|
-
scope: "project",
|
|
188
|
-
unitId: "project",
|
|
189
|
-
message: `${trackedPaths.length} runtime file(s) are tracked by git: ${trackedPaths.slice(0, 5).join(", ")}${trackedPaths.length > 5 ? "..." : ""}`,
|
|
190
|
-
fixable: true,
|
|
191
|
-
});
|
|
192
|
-
if (shouldFix("tracked_runtime_files")) {
|
|
193
|
-
try {
|
|
194
|
-
for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
|
|
195
|
-
nativeRmCached(basePath, [exclusion]);
|
|
196
|
-
}
|
|
197
|
-
fixesApplied.push(`untracked ${trackedPaths.length} runtime file(s)`);
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
fixesApplied.push("failed to untrack runtime files");
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
// git ls-files failed — skip
|
|
207
|
-
}
|
|
208
|
-
// ── Legacy slice branches ──────────────────────────────────────────────
|
|
209
|
-
try {
|
|
210
|
-
const branchList = nativeBranchList(basePath, "gsd/*/*")
|
|
211
|
-
.filter((branch) => !branch.startsWith("gsd/quick/"));
|
|
212
|
-
if (branchList.length > 0) {
|
|
213
|
-
issues.push({
|
|
214
|
-
severity: "info",
|
|
215
|
-
code: "legacy_slice_branches",
|
|
216
|
-
scope: "project",
|
|
217
|
-
unitId: "project",
|
|
218
|
-
message: `${branchList.length} legacy slice branch(es) found: ${branchList.slice(0, 3).join(", ")}${branchList.length > 3 ? "..." : ""}. These are no longer used (branchless architecture).`,
|
|
219
|
-
fixable: true,
|
|
220
|
-
});
|
|
221
|
-
if (shouldFix("legacy_slice_branches")) {
|
|
222
|
-
let deleted = 0;
|
|
223
|
-
for (const branch of branchList) {
|
|
224
|
-
try {
|
|
225
|
-
nativeBranchDelete(basePath, branch, true);
|
|
226
|
-
deleted++;
|
|
227
|
-
}
|
|
228
|
-
catch { /* skip branches that can't be deleted */ }
|
|
229
|
-
}
|
|
230
|
-
if (deleted > 0) {
|
|
231
|
-
fixesApplied.push(`deleted ${deleted} legacy slice branch(es)`);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
// git branch list failed — skip
|
|
238
|
-
}
|
|
239
|
-
// ── Integration branch existence ──────────────────────────────────────
|
|
240
|
-
// For each active (non-complete) milestone, verify the stored integration
|
|
241
|
-
// branch still exists in git. A missing integration branch blocks merge-back
|
|
242
|
-
// and causes the next merge operation to fail silently.
|
|
243
|
-
try {
|
|
244
|
-
const state = await deriveState(basePath);
|
|
245
|
-
const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
|
|
246
|
-
for (const milestone of state.registry) {
|
|
247
|
-
if (milestone.status === "complete")
|
|
248
|
-
continue;
|
|
249
|
-
const resolution = resolveMilestoneIntegrationBranch(basePath, milestone.id, gitPrefs);
|
|
250
|
-
if (!resolution.recordedBranch)
|
|
251
|
-
continue; // No stored branch — skip (not yet set)
|
|
252
|
-
if (resolution.status === "fallback" && resolution.effectiveBranch) {
|
|
253
|
-
issues.push({
|
|
254
|
-
severity: "warning",
|
|
255
|
-
code: "integration_branch_missing",
|
|
256
|
-
scope: "milestone",
|
|
257
|
-
unitId: milestone.id,
|
|
258
|
-
message: resolution.reason,
|
|
259
|
-
fixable: true,
|
|
260
|
-
});
|
|
261
|
-
if (shouldFix("integration_branch_missing")) {
|
|
262
|
-
writeIntegrationBranch(basePath, milestone.id, resolution.effectiveBranch);
|
|
263
|
-
fixesApplied.push(`updated integration branch for ${milestone.id} to "${resolution.effectiveBranch}"`);
|
|
264
|
-
}
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
if (resolution.status === "missing") {
|
|
268
|
-
issues.push({
|
|
269
|
-
severity: "error",
|
|
270
|
-
code: "integration_branch_missing",
|
|
271
|
-
scope: "milestone",
|
|
272
|
-
unitId: milestone.id,
|
|
273
|
-
message: resolution.reason,
|
|
274
|
-
fixable: false,
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
catch {
|
|
280
|
-
// Non-fatal — integration branch check failed
|
|
281
|
-
}
|
|
282
|
-
// ── Orphaned worktree directories ────────────────────────────────────
|
|
283
|
-
// Worktree removal can fail after a branch delete, leaving a directory
|
|
284
|
-
// that is no longer registered with git. These orphaned dirs cause
|
|
285
|
-
// "already exists" errors when re-creating the same worktree name.
|
|
286
|
-
try {
|
|
287
|
-
const wtDir = worktreesDir(basePath);
|
|
288
|
-
if (existsSync(wtDir)) {
|
|
289
|
-
// Resolve symlinks and normalize separators so that symlinked .gsd
|
|
290
|
-
// paths (e.g. ~/.gsd/projects/<hash>/worktrees/…) match the paths
|
|
291
|
-
// returned by `git worktree list`.
|
|
292
|
-
const normalizePath = (p) => {
|
|
293
|
-
try {
|
|
294
|
-
p = realpathSync(p);
|
|
295
|
-
}
|
|
296
|
-
catch { /* path may not exist */ }
|
|
297
|
-
return p.replaceAll("\\", "/");
|
|
298
|
-
};
|
|
299
|
-
const registeredPaths = new Set(nativeWorktreeList(basePath).map(entry => normalizePath(entry.path)));
|
|
300
|
-
for (const entry of readdirSync(wtDir)) {
|
|
301
|
-
const fullPath = join(wtDir, entry);
|
|
302
|
-
try {
|
|
303
|
-
if (!statSync(fullPath).isDirectory())
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
catch {
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
const normalizedFullPath = normalizePath(fullPath);
|
|
310
|
-
if (!registeredPaths.has(normalizedFullPath)) {
|
|
311
|
-
issues.push({
|
|
312
|
-
severity: "warning",
|
|
313
|
-
code: "worktree_directory_orphaned",
|
|
314
|
-
scope: "project",
|
|
315
|
-
unitId: entry,
|
|
316
|
-
message: `Worktree directory ${fullPath} exists on disk but is not registered with git. Run "git worktree prune" or doctor --fix to remove it.`,
|
|
317
|
-
fixable: true,
|
|
318
|
-
});
|
|
319
|
-
if (shouldFix("worktree_directory_orphaned")) {
|
|
320
|
-
try {
|
|
321
|
-
rmSync(fullPath, { recursive: true, force: true });
|
|
322
|
-
fixesApplied.push(`removed orphaned worktree directory ${fullPath}`);
|
|
323
|
-
}
|
|
324
|
-
catch {
|
|
325
|
-
fixesApplied.push(`failed to remove orphaned worktree directory ${fullPath}`);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
catch {
|
|
333
|
-
// Non-fatal — orphaned worktree directory check failed
|
|
334
|
-
}
|
|
335
|
-
// ── Worktree lifecycle checks ──────────────────────────────────────────
|
|
336
|
-
// Check GSD-managed worktrees for: merged branches, stale work, dirty
|
|
337
|
-
// state, and unpushed commits. Only worktrees under .gsd/worktrees/.
|
|
338
|
-
try {
|
|
339
|
-
const healthStatuses = getAllWorktreeHealth(basePath);
|
|
340
|
-
const cwd = process.cwd();
|
|
341
|
-
for (const health of healthStatuses) {
|
|
342
|
-
const wt = health.worktree;
|
|
343
|
-
const isCwd = wt.path === cwd || cwd.startsWith(wt.path + sep);
|
|
344
|
-
// Branch fully merged into main — safe to remove
|
|
345
|
-
if (health.mergedIntoMain) {
|
|
346
|
-
issues.push({
|
|
347
|
-
severity: "info",
|
|
348
|
-
code: "worktree_branch_merged",
|
|
349
|
-
scope: "project",
|
|
350
|
-
unitId: wt.name,
|
|
351
|
-
message: `Worktree "${wt.name}" (branch ${wt.branch}) is fully merged into main${health.safeToRemove ? " — safe to remove" : ""}`,
|
|
352
|
-
fixable: health.safeToRemove,
|
|
353
|
-
});
|
|
354
|
-
if (health.safeToRemove && shouldFix("worktree_branch_merged") && !isCwd) {
|
|
355
|
-
try {
|
|
356
|
-
const { removeWorktree } = await import("./worktree-manager.js");
|
|
357
|
-
removeWorktree(basePath, wt.name, { deleteBranch: true, branch: wt.branch });
|
|
358
|
-
fixesApplied.push(`removed merged worktree "${wt.name}" and deleted branch ${wt.branch}`);
|
|
359
|
-
}
|
|
360
|
-
catch {
|
|
361
|
-
fixesApplied.push(`failed to remove merged worktree "${wt.name}"`);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
// If merged, skip the stale/dirty/unpushed checks — they're irrelevant
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
// Stale: no commits in N days, not merged
|
|
368
|
-
if (health.stale) {
|
|
369
|
-
const days = Math.floor(health.lastCommitAgeDays);
|
|
370
|
-
issues.push({
|
|
371
|
-
severity: "warning",
|
|
372
|
-
code: "worktree_stale",
|
|
373
|
-
scope: "project",
|
|
374
|
-
unitId: wt.name,
|
|
375
|
-
message: `Worktree "${wt.name}" has had no commits in ${days} day${days === 1 ? "" : "s"}`,
|
|
376
|
-
fixable: false,
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
// Dirty: uncommitted changes in a worktree (only flag on stale worktrees to avoid noise)
|
|
380
|
-
if (health.dirty && health.stale) {
|
|
381
|
-
issues.push({
|
|
382
|
-
severity: "warning",
|
|
383
|
-
code: "worktree_dirty",
|
|
384
|
-
scope: "project",
|
|
385
|
-
unitId: wt.name,
|
|
386
|
-
message: `Worktree "${wt.name}" has ${health.dirtyFileCount} uncommitted file${health.dirtyFileCount === 1 ? "" : "s"} and is stale`,
|
|
387
|
-
fixable: false,
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
// Unpushed: commits not on any remote (only flag on stale worktrees to avoid noise)
|
|
391
|
-
if (health.unpushedCommits > 0 && health.stale) {
|
|
392
|
-
issues.push({
|
|
393
|
-
severity: "warning",
|
|
394
|
-
code: "worktree_unpushed",
|
|
395
|
-
scope: "project",
|
|
396
|
-
unitId: wt.name,
|
|
397
|
-
message: `Worktree "${wt.name}" has ${health.unpushedCommits} unpushed commit${health.unpushedCommits === 1 ? "" : "s"}`,
|
|
398
|
-
fixable: false,
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
catch {
|
|
404
|
-
// Non-fatal — worktree lifecycle check failed
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
// ── Runtime Health Checks ──────────────────────────────────────────────────
|
|
408
|
-
// Checks for stale crash locks, orphaned completed-units, stale hook state,
|
|
409
|
-
// activity log bloat, STATE.md drift, and gitignore drift.
|
|
410
|
-
export async function checkRuntimeHealth(basePath, issues, fixesApplied, shouldFix) {
|
|
411
|
-
const root = gsdRoot(basePath);
|
|
412
|
-
// ── Stale crash lock ──────────────────────────────────────────────────
|
|
413
|
-
try {
|
|
414
|
-
const lock = readCrashLock(basePath);
|
|
415
|
-
if (lock) {
|
|
416
|
-
const alive = isLockProcessAlive(lock);
|
|
417
|
-
if (!alive) {
|
|
418
|
-
issues.push({
|
|
419
|
-
severity: "error",
|
|
420
|
-
code: "stale_crash_lock",
|
|
421
|
-
scope: "project",
|
|
422
|
-
unitId: "project",
|
|
423
|
-
message: `Stale auto.lock from PID ${lock.pid} (started ${lock.startedAt}, was executing ${lock.unitType} ${lock.unitId}) — process is no longer running`,
|
|
424
|
-
file: ".gsd/auto.lock",
|
|
425
|
-
fixable: true,
|
|
426
|
-
});
|
|
427
|
-
if (shouldFix("stale_crash_lock")) {
|
|
428
|
-
clearLock(basePath);
|
|
429
|
-
fixesApplied.push("cleared stale auto.lock");
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
catch {
|
|
435
|
-
// Non-fatal — crash lock check failed
|
|
436
|
-
}
|
|
437
|
-
// ── Stranded lock directory ────────────────────────────────────────────
|
|
438
|
-
// proper-lockfile creates a `.gsd.lock/` directory as the OS-level lock
|
|
439
|
-
// mechanism. If the process was SIGKILLed or crashed hard, this directory
|
|
440
|
-
// can remain on disk without any live process holding it. The next session
|
|
441
|
-
// fails to acquire the lock until the directory is removed (#1245).
|
|
442
|
-
try {
|
|
443
|
-
const lockDir = join(dirname(root), `${basename(root)}.lock`);
|
|
444
|
-
if (existsSync(lockDir)) {
|
|
445
|
-
const statRes = statSync(lockDir);
|
|
446
|
-
if (statRes.isDirectory()) {
|
|
447
|
-
// Check if any live process actually holds this lock
|
|
448
|
-
const lock = readCrashLock(basePath);
|
|
449
|
-
const lockHolderAlive = lock ? isLockProcessAlive(lock) : false;
|
|
450
|
-
if (!lockHolderAlive) {
|
|
451
|
-
issues.push({
|
|
452
|
-
severity: "error",
|
|
453
|
-
code: "stranded_lock_directory",
|
|
454
|
-
scope: "project",
|
|
455
|
-
unitId: "project",
|
|
456
|
-
message: `Stranded lock directory "${lockDir}" exists but no live process holds the session lock. This blocks new auto-mode sessions from starting.`,
|
|
457
|
-
file: lockDir,
|
|
458
|
-
fixable: true,
|
|
459
|
-
});
|
|
460
|
-
if (shouldFix("stranded_lock_directory")) {
|
|
461
|
-
try {
|
|
462
|
-
rmSync(lockDir, { recursive: true, force: true });
|
|
463
|
-
fixesApplied.push(`removed stranded lock directory ${lockDir}`);
|
|
464
|
-
}
|
|
465
|
-
catch {
|
|
466
|
-
fixesApplied.push(`failed to remove stranded lock directory ${lockDir}`);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
catch {
|
|
474
|
-
// Non-fatal — stranded lock directory check failed
|
|
475
|
-
}
|
|
476
|
-
// ── Stale parallel sessions ────────────────────────────────────────────
|
|
477
|
-
try {
|
|
478
|
-
const parallelStatuses = readAllSessionStatuses(basePath);
|
|
479
|
-
for (const status of parallelStatuses) {
|
|
480
|
-
if (isSessionStale(status)) {
|
|
481
|
-
issues.push({
|
|
482
|
-
severity: "warning",
|
|
483
|
-
code: "stale_parallel_session",
|
|
484
|
-
scope: "project",
|
|
485
|
-
unitId: status.milestoneId,
|
|
486
|
-
message: `Stale parallel session for ${status.milestoneId} (PID ${status.pid}, started ${new Date(status.startedAt).toISOString()}, last heartbeat ${new Date(status.lastHeartbeat).toISOString()}) — process is no longer running`,
|
|
487
|
-
file: `.gsd/parallel/${status.milestoneId}.status.json`,
|
|
488
|
-
fixable: true,
|
|
489
|
-
});
|
|
490
|
-
if (shouldFix("stale_parallel_session")) {
|
|
491
|
-
removeSessionStatus(basePath, status.milestoneId);
|
|
492
|
-
fixesApplied.push(`cleaned up stale parallel session for ${status.milestoneId}`);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
catch {
|
|
498
|
-
// Non-fatal — parallel session check failed
|
|
499
|
-
}
|
|
500
|
-
// ── Orphaned completed-units keys ─────────────────────────────────────
|
|
501
|
-
try {
|
|
502
|
-
const completedKeysFile = join(root, "completed-units.json");
|
|
503
|
-
if (existsSync(completedKeysFile)) {
|
|
504
|
-
const raw = readFileSync(completedKeysFile, "utf-8");
|
|
505
|
-
const keys = JSON.parse(raw);
|
|
506
|
-
const orphaned = [];
|
|
507
|
-
for (const key of keys) {
|
|
508
|
-
// Key format: "unitType/unitId" e.g. "execute-task/M001/S01/T01"
|
|
509
|
-
const slashIdx = key.indexOf("/");
|
|
510
|
-
if (slashIdx === -1)
|
|
511
|
-
continue;
|
|
512
|
-
const unitType = key.slice(0, slashIdx);
|
|
513
|
-
const unitId = key.slice(slashIdx + 1);
|
|
514
|
-
// Only validate artifact-producing unit types
|
|
515
|
-
const { verifyExpectedArtifact } = await import("./auto-recovery.js");
|
|
516
|
-
if (!verifyExpectedArtifact(unitType, unitId, basePath)) {
|
|
517
|
-
orphaned.push(key);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
if (orphaned.length > 0) {
|
|
521
|
-
issues.push({
|
|
522
|
-
severity: "warning",
|
|
523
|
-
code: "orphaned_completed_units",
|
|
524
|
-
scope: "project",
|
|
525
|
-
unitId: "project",
|
|
526
|
-
message: `${orphaned.length} completed-unit key(s) reference missing artifacts: ${orphaned.slice(0, 3).join(", ")}${orphaned.length > 3 ? "..." : ""}`,
|
|
527
|
-
file: ".gsd/completed-units.json",
|
|
528
|
-
fixable: true,
|
|
529
|
-
});
|
|
530
|
-
if (shouldFix("orphaned_completed_units")) {
|
|
531
|
-
const orphanedSet = new Set(orphaned);
|
|
532
|
-
const remaining = keys.filter((key) => !orphanedSet.has(key));
|
|
533
|
-
await saveFile(completedKeysFile, JSON.stringify(remaining));
|
|
534
|
-
fixesApplied.push(`removed ${orphaned.length} orphaned completed-unit key(s)`);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
catch {
|
|
540
|
-
// Non-fatal — completed-units check failed
|
|
541
|
-
}
|
|
542
|
-
// ── Stale hook state ──────────────────────────────────────────────────
|
|
543
|
-
try {
|
|
544
|
-
const hookStateFile = join(root, "hook-state.json");
|
|
545
|
-
if (existsSync(hookStateFile)) {
|
|
546
|
-
const raw = readFileSync(hookStateFile, "utf-8");
|
|
547
|
-
const state = JSON.parse(raw);
|
|
548
|
-
const hasCycleCounts = state.cycleCounts && typeof state.cycleCounts === "object"
|
|
549
|
-
&& Object.keys(state.cycleCounts).length > 0;
|
|
550
|
-
// Only flag if there are actual cycle counts AND no auto-mode is running
|
|
551
|
-
if (hasCycleCounts) {
|
|
552
|
-
const lock = readCrashLock(basePath);
|
|
553
|
-
const autoRunning = lock ? isLockProcessAlive(lock) : false;
|
|
554
|
-
if (!autoRunning) {
|
|
555
|
-
issues.push({
|
|
556
|
-
severity: "info",
|
|
557
|
-
code: "stale_hook_state",
|
|
558
|
-
scope: "project",
|
|
559
|
-
unitId: "project",
|
|
560
|
-
message: `hook-state.json has ${Object.keys(state.cycleCounts).length} residual cycle count(s) from a previous session`,
|
|
561
|
-
file: ".gsd/hook-state.json",
|
|
562
|
-
fixable: true,
|
|
563
|
-
});
|
|
564
|
-
if (shouldFix("stale_hook_state")) {
|
|
565
|
-
const { clearPersistedHookState } = await import("./post-unit-hooks.js");
|
|
566
|
-
clearPersistedHookState(basePath);
|
|
567
|
-
fixesApplied.push("cleared stale hook-state.json");
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
catch {
|
|
574
|
-
// Non-fatal — hook state check failed
|
|
575
|
-
}
|
|
576
|
-
// ── Activity log bloat ────────────────────────────────────────────────
|
|
577
|
-
try {
|
|
578
|
-
const activityDir = join(root, "activity");
|
|
579
|
-
if (existsSync(activityDir)) {
|
|
580
|
-
const files = readdirSync(activityDir);
|
|
581
|
-
let totalSize = 0;
|
|
582
|
-
for (const f of files) {
|
|
583
|
-
try {
|
|
584
|
-
totalSize += statSync(join(activityDir, f)).size;
|
|
585
|
-
}
|
|
586
|
-
catch {
|
|
587
|
-
// stat failed — skip
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
const totalMB = totalSize / (1024 * 1024);
|
|
591
|
-
const BLOAT_FILE_THRESHOLD = 500;
|
|
592
|
-
const BLOAT_SIZE_MB = 100;
|
|
593
|
-
if (files.length > BLOAT_FILE_THRESHOLD || totalMB > BLOAT_SIZE_MB) {
|
|
594
|
-
issues.push({
|
|
595
|
-
severity: "warning",
|
|
596
|
-
code: "activity_log_bloat",
|
|
597
|
-
scope: "project",
|
|
598
|
-
unitId: "project",
|
|
599
|
-
message: `Activity logs: ${files.length} files, ${totalMB.toFixed(1)}MB (thresholds: ${BLOAT_FILE_THRESHOLD} files / ${BLOAT_SIZE_MB}MB)`,
|
|
600
|
-
file: ".gsd/activity/",
|
|
601
|
-
fixable: true,
|
|
602
|
-
});
|
|
603
|
-
if (shouldFix("activity_log_bloat")) {
|
|
604
|
-
const { pruneActivityLogs } = await import("./activity-log.js");
|
|
605
|
-
pruneActivityLogs(activityDir, 7); // 7-day retention
|
|
606
|
-
fixesApplied.push("pruned activity logs (7-day retention)");
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
catch {
|
|
612
|
-
// Non-fatal — activity log check failed
|
|
613
|
-
}
|
|
614
|
-
// ── STATE.md health ───────────────────────────────────────────────────
|
|
615
|
-
try {
|
|
616
|
-
const stateFilePath = resolveGsdRootFile(basePath, "STATE");
|
|
617
|
-
const milestonesPath = milestonesDir(basePath);
|
|
618
|
-
if (existsSync(milestonesPath)) {
|
|
619
|
-
if (!existsSync(stateFilePath)) {
|
|
620
|
-
issues.push({
|
|
621
|
-
severity: "warning",
|
|
622
|
-
code: "state_file_missing",
|
|
623
|
-
scope: "project",
|
|
624
|
-
unitId: "project",
|
|
625
|
-
message: "STATE.md is missing — state display will not work",
|
|
626
|
-
file: ".gsd/STATE.md",
|
|
627
|
-
fixable: true,
|
|
628
|
-
});
|
|
629
|
-
if (shouldFix("state_file_missing")) {
|
|
630
|
-
const state = await deriveState(basePath);
|
|
631
|
-
await saveFile(stateFilePath, buildStateMarkdownForCheck(state));
|
|
632
|
-
fixesApplied.push("created STATE.md from derived state");
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
else {
|
|
636
|
-
// Check if STATE.md is stale by comparing active milestone/slice/phase
|
|
637
|
-
const currentContent = readFileSync(stateFilePath, "utf-8");
|
|
638
|
-
const state = await deriveState(basePath);
|
|
639
|
-
const freshContent = buildStateMarkdownForCheck(state);
|
|
640
|
-
// Extract key fields for comparison — don't compare full content
|
|
641
|
-
// since timestamp/formatting differences are normal
|
|
642
|
-
const extractFields = (content) => {
|
|
643
|
-
const milestone = content.match(/\*\*Active Milestone:\*\*\s*(.+)/)?.[1]?.trim() ?? "";
|
|
644
|
-
const slice = content.match(/\*\*Active Slice:\*\*\s*(.+)/)?.[1]?.trim() ?? "";
|
|
645
|
-
const phase = content.match(/\*\*Phase:\*\*\s*(.+)/)?.[1]?.trim() ?? "";
|
|
646
|
-
return { milestone, slice, phase };
|
|
647
|
-
};
|
|
648
|
-
const current = extractFields(currentContent);
|
|
649
|
-
const fresh = extractFields(freshContent);
|
|
650
|
-
if (current.milestone !== fresh.milestone || current.slice !== fresh.slice || current.phase !== fresh.phase) {
|
|
651
|
-
issues.push({
|
|
652
|
-
severity: "warning",
|
|
653
|
-
code: "state_file_stale",
|
|
654
|
-
scope: "project",
|
|
655
|
-
unitId: "project",
|
|
656
|
-
message: `STATE.md is stale — shows "${current.phase}" but derived state is "${fresh.phase}"`,
|
|
657
|
-
file: ".gsd/STATE.md",
|
|
658
|
-
fixable: true,
|
|
659
|
-
});
|
|
660
|
-
if (shouldFix("state_file_stale")) {
|
|
661
|
-
await saveFile(stateFilePath, freshContent);
|
|
662
|
-
fixesApplied.push("rebuilt STATE.md from derived state");
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
catch {
|
|
669
|
-
// Non-fatal — STATE.md check failed
|
|
670
|
-
}
|
|
671
|
-
// ── Gitignore drift ───────────────────────────────────────────────────
|
|
672
|
-
try {
|
|
673
|
-
const gitignorePath = join(basePath, ".gitignore");
|
|
674
|
-
if (existsSync(gitignorePath) && nativeIsRepo(basePath)) {
|
|
675
|
-
const content = readFileSync(gitignorePath, "utf-8");
|
|
676
|
-
const existingLines = new Set(content.split("\n").map(l => l.trim()).filter(l => l && !l.startsWith("#")));
|
|
677
|
-
// Check for critical runtime patterns that must be present
|
|
678
|
-
const criticalPatterns = [
|
|
679
|
-
".gsd/activity/",
|
|
680
|
-
".gsd/runtime/",
|
|
681
|
-
".gsd/auto.lock",
|
|
682
|
-
".gsd/gsd.db",
|
|
683
|
-
".gsd/completed-units.json",
|
|
684
|
-
];
|
|
685
|
-
// If blanket .gsd/ or .gsd is present, all patterns are covered
|
|
686
|
-
const hasBlanketIgnore = existingLines.has(".gsd/") || existingLines.has(".gsd");
|
|
687
|
-
if (!hasBlanketIgnore) {
|
|
688
|
-
const missing = criticalPatterns.filter(p => !existingLines.has(p));
|
|
689
|
-
if (missing.length > 0) {
|
|
690
|
-
issues.push({
|
|
691
|
-
severity: "warning",
|
|
692
|
-
code: "gitignore_missing_patterns",
|
|
693
|
-
scope: "project",
|
|
694
|
-
unitId: "project",
|
|
695
|
-
message: `${missing.length} critical GSD runtime pattern(s) missing from .gitignore: ${missing.join(", ")}`,
|
|
696
|
-
file: ".gitignore",
|
|
697
|
-
fixable: true,
|
|
698
|
-
});
|
|
699
|
-
if (shouldFix("gitignore_missing_patterns")) {
|
|
700
|
-
ensureGitignore(basePath);
|
|
701
|
-
fixesApplied.push("added missing GSD runtime patterns to .gitignore");
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
catch {
|
|
708
|
-
// Non-fatal — gitignore check failed
|
|
709
|
-
}
|
|
710
|
-
// ── External state symlink health ──────────────────────────────────────
|
|
711
|
-
try {
|
|
712
|
-
const localGsd = join(basePath, ".gsd");
|
|
713
|
-
if (existsSync(localGsd)) {
|
|
714
|
-
const stat = lstatSync(localGsd);
|
|
715
|
-
// Check for .gsd.migrating (failed migration)
|
|
716
|
-
const migratingPath = join(basePath, ".gsd.migrating");
|
|
717
|
-
if (existsSync(migratingPath)) {
|
|
718
|
-
issues.push({
|
|
719
|
-
severity: "error",
|
|
720
|
-
code: "failed_migration",
|
|
721
|
-
scope: "project",
|
|
722
|
-
unitId: "project",
|
|
723
|
-
message: "Found .gsd.migrating — a previous external state migration failed. State may be incomplete.",
|
|
724
|
-
file: ".gsd.migrating",
|
|
725
|
-
fixable: true,
|
|
726
|
-
});
|
|
727
|
-
if (shouldFix("failed_migration")) {
|
|
728
|
-
if (recoverFailedMigration(basePath)) {
|
|
729
|
-
fixesApplied.push("recovered failed migration (.gsd.migrating → .gsd)");
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
// Check symlink target exists
|
|
734
|
-
if (stat.isSymbolicLink()) {
|
|
735
|
-
try {
|
|
736
|
-
realpathSync(localGsd);
|
|
737
|
-
}
|
|
738
|
-
catch {
|
|
739
|
-
issues.push({
|
|
740
|
-
severity: "error",
|
|
741
|
-
code: "broken_symlink",
|
|
742
|
-
scope: "project",
|
|
743
|
-
unitId: "project",
|
|
744
|
-
message: ".gsd symlink target does not exist. External state directory may have been deleted.",
|
|
745
|
-
file: ".gsd",
|
|
746
|
-
fixable: false,
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
catch {
|
|
753
|
-
// Non-fatal — external state check failed
|
|
754
|
-
}
|
|
755
|
-
// ── Numbered .gsd collision variants (#2205) ───────────────────────────
|
|
756
|
-
// macOS APFS can create ".gsd 2", ".gsd 3" etc. when a directory blocks
|
|
757
|
-
// symlink creation. These must be removed so the canonical .gsd is used.
|
|
758
|
-
try {
|
|
759
|
-
const variantPattern = /^\.gsd \d+$/;
|
|
760
|
-
const entries = readdirSync(basePath);
|
|
761
|
-
const variants = entries.filter(e => variantPattern.test(e));
|
|
762
|
-
if (variants.length > 0) {
|
|
763
|
-
for (const v of variants) {
|
|
764
|
-
issues.push({
|
|
765
|
-
severity: "warning",
|
|
766
|
-
code: "numbered_gsd_variant",
|
|
767
|
-
scope: "project",
|
|
768
|
-
unitId: "project",
|
|
769
|
-
message: `Found macOS collision variant "${v}" — this can cause GSD state to appear deleted.`,
|
|
770
|
-
file: v,
|
|
771
|
-
fixable: true,
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
if (shouldFix("numbered_gsd_variant")) {
|
|
775
|
-
const removed = cleanNumberedGsdVariants(basePath);
|
|
776
|
-
for (const name of removed) {
|
|
777
|
-
fixesApplied.push(`removed numbered .gsd variant: ${name}`);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
catch {
|
|
783
|
-
// Non-fatal — variant check failed
|
|
784
|
-
}
|
|
785
|
-
// ── Metrics ledger integrity ───────────────────────────────────────────
|
|
786
|
-
try {
|
|
787
|
-
const metricsPath = join(root, "metrics.json");
|
|
788
|
-
if (existsSync(metricsPath)) {
|
|
789
|
-
try {
|
|
790
|
-
const raw = readFileSync(metricsPath, "utf-8");
|
|
791
|
-
const ledger = JSON.parse(raw);
|
|
792
|
-
if (ledger.version !== 1 || !Array.isArray(ledger.units)) {
|
|
793
|
-
issues.push({
|
|
794
|
-
severity: "warning",
|
|
795
|
-
code: "metrics_ledger_corrupt",
|
|
796
|
-
scope: "project",
|
|
797
|
-
unitId: "project",
|
|
798
|
-
message: "metrics.json has an unexpected structure (version !== 1 or units is not an array) — metrics data may be unreliable",
|
|
799
|
-
file: ".gsd/metrics.json",
|
|
800
|
-
fixable: false,
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
catch {
|
|
805
|
-
issues.push({
|
|
806
|
-
severity: "warning",
|
|
807
|
-
code: "metrics_ledger_corrupt",
|
|
808
|
-
scope: "project",
|
|
809
|
-
unitId: "project",
|
|
810
|
-
message: "metrics.json is not valid JSON — metrics data may be corrupt",
|
|
811
|
-
file: ".gsd/metrics.json",
|
|
812
|
-
fixable: false,
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
catch {
|
|
818
|
-
// Non-fatal — metrics check failed
|
|
819
|
-
}
|
|
820
|
-
// ── Metrics ledger bloat ──────────────────────────────────────────────
|
|
821
|
-
// The metrics ledger has no TTL and grows by one entry per completed unit.
|
|
822
|
-
// At 50 units/day a project can accumulate tens of thousands of entries over
|
|
823
|
-
// months of use. Prune to the newest 1500 when the threshold is exceeded.
|
|
824
|
-
try {
|
|
825
|
-
const metricsFilePath = join(root, "metrics.json");
|
|
826
|
-
if (existsSync(metricsFilePath)) {
|
|
827
|
-
try {
|
|
828
|
-
const raw = readFileSync(metricsFilePath, "utf-8");
|
|
829
|
-
const parsed = JSON.parse(raw);
|
|
830
|
-
const BLOAT_UNITS_THRESHOLD = 2000;
|
|
831
|
-
if (parsed.version === 1 && Array.isArray(parsed.units) && parsed.units.length > BLOAT_UNITS_THRESHOLD) {
|
|
832
|
-
const fileSizeMB = (statSync(metricsFilePath).size / (1024 * 1024)).toFixed(1);
|
|
833
|
-
issues.push({
|
|
834
|
-
severity: "warning",
|
|
835
|
-
code: "metrics_ledger_bloat",
|
|
836
|
-
scope: "project",
|
|
837
|
-
unitId: "project",
|
|
838
|
-
message: `metrics.json has ${parsed.units.length} unit entries (${fileSizeMB}MB) — threshold is ${BLOAT_UNITS_THRESHOLD}. Run /gsd doctor --fix to prune to the newest 1500 entries.`,
|
|
839
|
-
file: ".gsd/metrics.json",
|
|
840
|
-
fixable: true,
|
|
841
|
-
});
|
|
842
|
-
if (shouldFix("metrics_ledger_bloat")) {
|
|
843
|
-
const { pruneMetricsLedger } = await import("./metrics.js");
|
|
844
|
-
const removed = pruneMetricsLedger(basePath, 1500);
|
|
845
|
-
fixesApplied.push(`pruned metrics ledger: removed ${removed} oldest entries (${parsed.units.length - removed} remain)`);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
catch {
|
|
850
|
-
// JSON parse failed — already handled by the integrity check above
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
catch {
|
|
855
|
-
// Non-fatal — metrics bloat check failed
|
|
856
|
-
}
|
|
857
|
-
// ── Large planning file detection ──────────────────────────────────────
|
|
858
|
-
// Files over 100KB can cause LLM context pressure. Report the worst offenders.
|
|
859
|
-
try {
|
|
860
|
-
const MAX_FILE_BYTES = 100 * 1024; // 100KB
|
|
861
|
-
const milestonesPath = milestonesDir(basePath);
|
|
862
|
-
if (existsSync(milestonesPath)) {
|
|
863
|
-
const largeFiles = [];
|
|
864
|
-
function scanForLargeFiles(dir, depth = 0) {
|
|
865
|
-
if (depth > 6)
|
|
866
|
-
return;
|
|
867
|
-
try {
|
|
868
|
-
for (const entry of readdirSync(dir)) {
|
|
869
|
-
const full = join(dir, entry);
|
|
870
|
-
try {
|
|
871
|
-
const s = statSync(full);
|
|
872
|
-
if (s.isDirectory()) {
|
|
873
|
-
scanForLargeFiles(full, depth + 1);
|
|
874
|
-
continue;
|
|
875
|
-
}
|
|
876
|
-
if (entry.endsWith(".md") && s.size > MAX_FILE_BYTES) {
|
|
877
|
-
largeFiles.push({ path: full.replace(basePath + "/", ""), sizeKB: Math.round(s.size / 1024) });
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
catch { /* skip entry */ }
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
catch { /* skip dir */ }
|
|
884
|
-
}
|
|
885
|
-
scanForLargeFiles(milestonesPath);
|
|
886
|
-
if (largeFiles.length > 0) {
|
|
887
|
-
largeFiles.sort((a, b) => b.sizeKB - a.sizeKB);
|
|
888
|
-
const worst = largeFiles[0];
|
|
889
|
-
issues.push({
|
|
890
|
-
severity: "warning",
|
|
891
|
-
code: "large_planning_file",
|
|
892
|
-
scope: "project",
|
|
893
|
-
unitId: "project",
|
|
894
|
-
message: `${largeFiles.length} planning file(s) exceed 100KB — largest: ${worst.path} (${worst.sizeKB}KB). Large files cause LLM context pressure.`,
|
|
895
|
-
file: worst.path,
|
|
896
|
-
fixable: false,
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
catch {
|
|
902
|
-
// Non-fatal — large file scan failed
|
|
903
|
-
}
|
|
904
|
-
// ── Snapshot ref bloat ────────────────────────────────────────────────
|
|
905
|
-
// refs/gsd/snapshots/ accumulate over time. Prune to newest 5 per label
|
|
906
|
-
// when total count exceeds threshold.
|
|
907
|
-
try {
|
|
908
|
-
if (nativeIsRepo(basePath)) {
|
|
909
|
-
const refs = nativeForEachRef(basePath, "refs/gsd/snapshots/");
|
|
910
|
-
if (refs.length > 50) {
|
|
911
|
-
issues.push({
|
|
912
|
-
severity: "warning",
|
|
913
|
-
code: "snapshot_ref_bloat",
|
|
914
|
-
scope: "project",
|
|
915
|
-
unitId: "project",
|
|
916
|
-
message: `${refs.length} snapshot refs found under refs/gsd/snapshots/ — pruning to newest 5 per label will reclaim git storage`,
|
|
917
|
-
fixable: true,
|
|
918
|
-
});
|
|
919
|
-
if (shouldFix("snapshot_ref_bloat")) {
|
|
920
|
-
const byLabel = new Map();
|
|
921
|
-
for (const ref of refs) {
|
|
922
|
-
const parts = ref.split("/");
|
|
923
|
-
const label = parts.slice(0, -1).join("/");
|
|
924
|
-
if (!byLabel.has(label))
|
|
925
|
-
byLabel.set(label, []);
|
|
926
|
-
byLabel.get(label).push(ref);
|
|
927
|
-
}
|
|
928
|
-
let pruned = 0;
|
|
929
|
-
for (const [, labelRefs] of byLabel) {
|
|
930
|
-
const sorted = labelRefs.sort();
|
|
931
|
-
for (const old of sorted.slice(0, -5)) {
|
|
932
|
-
try {
|
|
933
|
-
nativeUpdateRef(basePath, old);
|
|
934
|
-
pruned++;
|
|
935
|
-
}
|
|
936
|
-
catch { /* skip */ }
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
if (pruned > 0) {
|
|
940
|
-
fixesApplied.push(`pruned ${pruned} old snapshot ref(s)`);
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
catch {
|
|
947
|
-
// Non-fatal — snapshot ref check failed
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
/**
|
|
951
|
-
* Build STATE.md markdown content from derived state.
|
|
952
|
-
* Local helper used by checkRuntimeHealth for STATE.md drift detection and repair.
|
|
953
|
-
*/
|
|
954
|
-
function buildStateMarkdownForCheck(state) {
|
|
955
|
-
const lines = [];
|
|
956
|
-
lines.push("# GSD State", "");
|
|
957
|
-
const activeMilestone = state.activeMilestone
|
|
958
|
-
? `${state.activeMilestone.id}: ${state.activeMilestone.title}`
|
|
959
|
-
: "None";
|
|
960
|
-
const activeSlice = state.activeSlice
|
|
961
|
-
? `${state.activeSlice.id}: ${state.activeSlice.title}`
|
|
962
|
-
: "None";
|
|
963
|
-
lines.push(`**Active Milestone:** ${activeMilestone}`);
|
|
964
|
-
lines.push(`**Active Slice:** ${activeSlice}`);
|
|
965
|
-
lines.push(`**Phase:** ${state.phase}`);
|
|
966
|
-
if (state.requirements) {
|
|
967
|
-
lines.push(`**Requirements Status:** ${state.requirements.active} active · ${state.requirements.validated} validated · ${state.requirements.deferred} deferred · ${state.requirements.outOfScope} out of scope`);
|
|
968
|
-
}
|
|
969
|
-
lines.push("");
|
|
970
|
-
lines.push("## Milestone Registry");
|
|
971
|
-
for (const entry of state.registry) {
|
|
972
|
-
const glyph = entry.status === "complete" ? "\u2705" : entry.status === "active" ? "\uD83D\uDD04" : entry.status === "parked" ? "\u23F8\uFE0F" : "\u2B1C";
|
|
973
|
-
lines.push(`- ${glyph} **${entry.id}:** ${entry.title}`);
|
|
974
|
-
}
|
|
975
|
-
lines.push("");
|
|
976
|
-
lines.push("## Recent Decisions");
|
|
977
|
-
if (state.recentDecisions.length > 0) {
|
|
978
|
-
for (const decision of state.recentDecisions)
|
|
979
|
-
lines.push(`- ${decision}`);
|
|
980
|
-
}
|
|
981
|
-
else {
|
|
982
|
-
lines.push("- None recorded");
|
|
983
|
-
}
|
|
984
|
-
lines.push("");
|
|
985
|
-
lines.push("## Blockers");
|
|
986
|
-
if (state.blockers.length > 0) {
|
|
987
|
-
for (const blocker of state.blockers)
|
|
988
|
-
lines.push(`- ${blocker}`);
|
|
989
|
-
}
|
|
990
|
-
else {
|
|
991
|
-
lines.push("- None");
|
|
992
|
-
}
|
|
993
|
-
lines.push("");
|
|
994
|
-
lines.push("## Next Action");
|
|
995
|
-
lines.push(state.nextAction || "None");
|
|
996
|
-
lines.push("");
|
|
997
|
-
return lines.join("\n");
|
|
998
|
-
}
|
|
999
|
-
// ── Global Health Checks ────────────────────────────────────────────────────
|
|
1000
|
-
// Cross-project checks that scan ~/.gsd/ rather than a specific project directory.
|
|
1001
|
-
/**
|
|
1002
|
-
* Check for orphaned project state directories in ~/.gsd/projects/.
|
|
1003
|
-
*
|
|
1004
|
-
* A project directory is orphaned when its recorded gitRoot no longer exists
|
|
1005
|
-
* on disk — the repo was deleted, moved, or the external drive was unmounted.
|
|
1006
|
-
* These directories accumulate silently and waste disk space.
|
|
1007
|
-
*
|
|
1008
|
-
* Severity: info — orphaned state is harmless but takes disk space.
|
|
1009
|
-
* Fixable: yes — rmSync the directory. Never auto-fixed at fixLevel="task".
|
|
1010
|
-
*/
|
|
1011
|
-
export async function checkGlobalHealth(issues, fixesApplied, shouldFix) {
|
|
1012
|
-
try {
|
|
1013
|
-
const projectsDir = externalProjectsRoot();
|
|
1014
|
-
if (!existsSync(projectsDir))
|
|
1015
|
-
return;
|
|
1016
|
-
let entries;
|
|
1017
|
-
try {
|
|
1018
|
-
entries = readdirSync(projectsDir, { withFileTypes: true })
|
|
1019
|
-
.filter(e => e.isDirectory())
|
|
1020
|
-
.map(e => e.name);
|
|
1021
|
-
}
|
|
1022
|
-
catch {
|
|
1023
|
-
return; // Can't read directory — skip
|
|
1024
|
-
}
|
|
1025
|
-
if (entries.length === 0)
|
|
1026
|
-
return;
|
|
1027
|
-
const orphaned = [];
|
|
1028
|
-
let unknownCount = 0;
|
|
1029
|
-
for (const hash of entries) {
|
|
1030
|
-
const dirPath = join(projectsDir, hash);
|
|
1031
|
-
const meta = readRepoMeta(dirPath);
|
|
1032
|
-
if (!meta) {
|
|
1033
|
-
unknownCount++;
|
|
1034
|
-
continue;
|
|
1035
|
-
}
|
|
1036
|
-
if (!existsSync(meta.gitRoot)) {
|
|
1037
|
-
orphaned.push({ hash, gitRoot: meta.gitRoot, remoteUrl: meta.remoteUrl });
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
if (orphaned.length === 0)
|
|
1041
|
-
return;
|
|
1042
|
-
const labels = orphaned.slice(0, 3).map(o => o.gitRoot).join(", ");
|
|
1043
|
-
const overflow = orphaned.length > 3 ? ` (+${orphaned.length - 3} more)` : "";
|
|
1044
|
-
const unknownNote = unknownCount > 0 ? ` — ${unknownCount} additional director${unknownCount === 1 ? "y" : "ies"} have no metadata yet (open those repos once to register them)` : "";
|
|
1045
|
-
issues.push({
|
|
1046
|
-
severity: "info",
|
|
1047
|
-
code: "orphaned_project_state",
|
|
1048
|
-
scope: "project",
|
|
1049
|
-
unitId: "global",
|
|
1050
|
-
message: `${orphaned.length} orphaned GSD project state director${orphaned.length === 1 ? "y" : "ies"} in ${projectsDir} whose git root no longer exists: ${labels}${overflow}${unknownNote}. Run /gsd cleanup projects to audit or /gsd cleanup projects --fix to reclaim disk space.`,
|
|
1051
|
-
file: projectsDir,
|
|
1052
|
-
fixable: true,
|
|
1053
|
-
});
|
|
1054
|
-
if (shouldFix("orphaned_project_state")) {
|
|
1055
|
-
let removed = 0;
|
|
1056
|
-
for (const { hash } of orphaned) {
|
|
1057
|
-
try {
|
|
1058
|
-
rmSync(join(projectsDir, hash), { recursive: true, force: true });
|
|
1059
|
-
removed++;
|
|
1060
|
-
}
|
|
1061
|
-
catch {
|
|
1062
|
-
// Individual removal failure is non-fatal — continue with remaining
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
fixesApplied.push(`removed ${removed} orphaned project state director${removed === 1 ? "y" : "ies"} from ${projectsDir}`);
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
catch {
|
|
1069
|
-
// Non-fatal — global health check must not block per-project doctor
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
// ── Engine Health Checks ────────────────────────────────────────────────────
|
|
1073
|
-
// DB constraint violation detection and projection drift checks.
|
|
1074
|
-
export async function checkEngineHealth(basePath, issues, fixesApplied) {
|
|
1075
|
-
// ── DB constraint violation detection (full doctor only, not pre-dispatch per D-10) ──
|
|
1076
|
-
try {
|
|
1077
|
-
if (isDbAvailable()) {
|
|
1078
|
-
const adapter = _getAdapter();
|
|
1079
|
-
// a. Orphaned tasks (task.slice_id points to non-existent slice)
|
|
1080
|
-
try {
|
|
1081
|
-
const orphanedTasks = adapter
|
|
1082
|
-
.prepare(`SELECT t.id, t.slice_id, t.milestone_id
|
|
1083
|
-
FROM tasks t
|
|
1084
|
-
LEFT JOIN slices s ON t.milestone_id = s.milestone_id AND t.slice_id = s.id
|
|
1085
|
-
WHERE s.id IS NULL`)
|
|
1086
|
-
.all();
|
|
1087
|
-
for (const row of orphanedTasks) {
|
|
1088
|
-
issues.push({
|
|
1089
|
-
severity: "error",
|
|
1090
|
-
code: "db_orphaned_task",
|
|
1091
|
-
scope: "task",
|
|
1092
|
-
unitId: `${row.milestone_id}/${row.slice_id}/${row.id}`,
|
|
1093
|
-
message: `Task ${row.id} references slice ${row.slice_id} in milestone ${row.milestone_id} but no such slice exists in the database`,
|
|
1094
|
-
fixable: false,
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
catch {
|
|
1099
|
-
// Non-fatal — orphaned task check failed
|
|
1100
|
-
}
|
|
1101
|
-
// b. Orphaned slices (slice.milestone_id points to non-existent milestone)
|
|
1102
|
-
try {
|
|
1103
|
-
const orphanedSlices = adapter
|
|
1104
|
-
.prepare(`SELECT s.id, s.milestone_id
|
|
1105
|
-
FROM slices s
|
|
1106
|
-
LEFT JOIN milestones m ON s.milestone_id = m.id
|
|
1107
|
-
WHERE m.id IS NULL`)
|
|
1108
|
-
.all();
|
|
1109
|
-
for (const row of orphanedSlices) {
|
|
1110
|
-
issues.push({
|
|
1111
|
-
severity: "error",
|
|
1112
|
-
code: "db_orphaned_slice",
|
|
1113
|
-
scope: "slice",
|
|
1114
|
-
unitId: `${row.milestone_id}/${row.id}`,
|
|
1115
|
-
message: `Slice ${row.id} references milestone ${row.milestone_id} but no such milestone exists in the database`,
|
|
1116
|
-
fixable: false,
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
catch {
|
|
1121
|
-
// Non-fatal — orphaned slice check failed
|
|
1122
|
-
}
|
|
1123
|
-
// c. Tasks marked complete without summaries
|
|
1124
|
-
try {
|
|
1125
|
-
const doneTasks = adapter
|
|
1126
|
-
.prepare(`SELECT id, slice_id, milestone_id FROM tasks
|
|
1127
|
-
WHERE status = 'done' AND (summary IS NULL OR summary = '')`)
|
|
1128
|
-
.all();
|
|
1129
|
-
for (const row of doneTasks) {
|
|
1130
|
-
issues.push({
|
|
1131
|
-
severity: "warning",
|
|
1132
|
-
code: "db_done_task_no_summary",
|
|
1133
|
-
scope: "task",
|
|
1134
|
-
unitId: `${row.milestone_id}/${row.slice_id}/${row.id}`,
|
|
1135
|
-
message: `Task ${row.id} is marked done but has no summary in the database`,
|
|
1136
|
-
fixable: false,
|
|
1137
|
-
});
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
catch {
|
|
1141
|
-
// Non-fatal — done-task-no-summary check failed
|
|
1142
|
-
}
|
|
1143
|
-
// d. Duplicate entity IDs (safety check)
|
|
1144
|
-
try {
|
|
1145
|
-
const dupMilestones = adapter
|
|
1146
|
-
.prepare("SELECT id, COUNT(*) as cnt FROM milestones GROUP BY id HAVING cnt > 1")
|
|
1147
|
-
.all();
|
|
1148
|
-
for (const row of dupMilestones) {
|
|
1149
|
-
issues.push({
|
|
1150
|
-
severity: "error",
|
|
1151
|
-
code: "db_duplicate_id",
|
|
1152
|
-
scope: "milestone",
|
|
1153
|
-
unitId: row.id,
|
|
1154
|
-
message: `Duplicate milestone ID "${row.id}" appears ${row.cnt} times in the database`,
|
|
1155
|
-
fixable: false,
|
|
1156
|
-
});
|
|
1157
|
-
}
|
|
1158
|
-
const dupSlices = adapter
|
|
1159
|
-
.prepare("SELECT id, milestone_id, COUNT(*) as cnt FROM slices GROUP BY id, milestone_id HAVING cnt > 1")
|
|
1160
|
-
.all();
|
|
1161
|
-
for (const row of dupSlices) {
|
|
1162
|
-
issues.push({
|
|
1163
|
-
severity: "error",
|
|
1164
|
-
code: "db_duplicate_id",
|
|
1165
|
-
scope: "slice",
|
|
1166
|
-
unitId: `${row.milestone_id}/${row.id}`,
|
|
1167
|
-
message: `Duplicate slice ID "${row.id}" in milestone ${row.milestone_id} appears ${row.cnt} times`,
|
|
1168
|
-
fixable: false,
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
const dupTasks = adapter
|
|
1172
|
-
.prepare("SELECT id, slice_id, milestone_id, COUNT(*) as cnt FROM tasks GROUP BY id, slice_id, milestone_id HAVING cnt > 1")
|
|
1173
|
-
.all();
|
|
1174
|
-
for (const row of dupTasks) {
|
|
1175
|
-
issues.push({
|
|
1176
|
-
severity: "error",
|
|
1177
|
-
code: "db_duplicate_id",
|
|
1178
|
-
scope: "task",
|
|
1179
|
-
unitId: `${row.milestone_id}/${row.slice_id}/${row.id}`,
|
|
1180
|
-
message: `Duplicate task ID "${row.id}" in slice ${row.slice_id} appears ${row.cnt} times`,
|
|
1181
|
-
fixable: false,
|
|
1182
|
-
});
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
catch {
|
|
1186
|
-
// Non-fatal — duplicate ID check failed
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
catch {
|
|
1191
|
-
// Non-fatal — DB constraint checks failed entirely
|
|
1192
|
-
}
|
|
1193
|
-
// ── Projection drift detection ──────────────────────────────────────────
|
|
1194
|
-
// If the DB is available, check whether markdown projections are stale
|
|
1195
|
-
// relative to the event log and re-render them.
|
|
1196
|
-
try {
|
|
1197
|
-
if (isDbAvailable()) {
|
|
1198
|
-
const eventLogPath = join(basePath, ".gsd", "event-log.jsonl");
|
|
1199
|
-
const events = readEvents(eventLogPath);
|
|
1200
|
-
if (events.length > 0) {
|
|
1201
|
-
const lastEventTs = new Date(events[events.length - 1].ts).getTime();
|
|
1202
|
-
const state = await deriveState(basePath);
|
|
1203
|
-
for (const milestone of state.registry) {
|
|
1204
|
-
if (milestone.status === "complete")
|
|
1205
|
-
continue;
|
|
1206
|
-
const roadmapPath = resolveMilestoneFile(basePath, milestone.id, "ROADMAP");
|
|
1207
|
-
if (!roadmapPath || !existsSync(roadmapPath)) {
|
|
1208
|
-
try {
|
|
1209
|
-
await renderAllProjections(basePath, milestone.id);
|
|
1210
|
-
fixesApplied.push(`re-rendered missing projections for ${milestone.id}`);
|
|
1211
|
-
}
|
|
1212
|
-
catch {
|
|
1213
|
-
// Non-fatal — projection re-render failed
|
|
1214
|
-
}
|
|
1215
|
-
continue;
|
|
1216
|
-
}
|
|
1217
|
-
const projectionMtime = statSync(roadmapPath).mtimeMs;
|
|
1218
|
-
if (lastEventTs > projectionMtime) {
|
|
1219
|
-
try {
|
|
1220
|
-
await renderAllProjections(basePath, milestone.id);
|
|
1221
|
-
fixesApplied.push(`re-rendered stale projections for ${milestone.id}`);
|
|
1222
|
-
}
|
|
1223
|
-
catch {
|
|
1224
|
-
// Non-fatal — projection re-render failed
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
catch {
|
|
1232
|
-
// Non-fatal — projection drift check must never block doctor
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1
|
+
// Re-exports for backward compatibility
|
|
2
|
+
export { checkGitHealth } from "./doctor-git-checks.js";
|
|
3
|
+
export { checkRuntimeHealth } from "./doctor-runtime-checks.js";
|
|
4
|
+
export { checkGlobalHealth } from "./doctor-global-checks.js";
|
|
5
|
+
export { checkEngineHealth } from "./doctor-engine-checks.js";
|