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
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { isDbAvailable, _getAdapter } from "./gsd-db.js";
|
|
4
|
+
import { resolveMilestoneFile } from "./paths.js";
|
|
5
|
+
import { deriveState } from "./state.js";
|
|
6
|
+
import { readEvents } from "./workflow-events.js";
|
|
7
|
+
import { renderAllProjections } from "./workflow-projections.js";
|
|
8
|
+
export async function checkEngineHealth(basePath, issues, fixesApplied) {
|
|
9
|
+
// ── DB constraint violation detection (full doctor only, not pre-dispatch per D-10) ──
|
|
10
|
+
try {
|
|
11
|
+
if (isDbAvailable()) {
|
|
12
|
+
const adapter = _getAdapter();
|
|
13
|
+
// a. Orphaned tasks (task.slice_id points to non-existent slice)
|
|
14
|
+
try {
|
|
15
|
+
const orphanedTasks = adapter
|
|
16
|
+
.prepare(`SELECT t.id, t.slice_id, t.milestone_id
|
|
17
|
+
FROM tasks t
|
|
18
|
+
LEFT JOIN slices s ON t.milestone_id = s.milestone_id AND t.slice_id = s.id
|
|
19
|
+
WHERE s.id IS NULL`)
|
|
20
|
+
.all();
|
|
21
|
+
for (const row of orphanedTasks) {
|
|
22
|
+
issues.push({
|
|
23
|
+
severity: "error",
|
|
24
|
+
code: "db_orphaned_task",
|
|
25
|
+
scope: "task",
|
|
26
|
+
unitId: `${row.milestone_id}/${row.slice_id}/${row.id}`,
|
|
27
|
+
message: `Task ${row.id} references slice ${row.slice_id} in milestone ${row.milestone_id} but no such slice exists in the database`,
|
|
28
|
+
fixable: false,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Non-fatal — orphaned task check failed
|
|
34
|
+
}
|
|
35
|
+
// b. Orphaned slices (slice.milestone_id points to non-existent milestone)
|
|
36
|
+
try {
|
|
37
|
+
const orphanedSlices = adapter
|
|
38
|
+
.prepare(`SELECT s.id, s.milestone_id
|
|
39
|
+
FROM slices s
|
|
40
|
+
LEFT JOIN milestones m ON s.milestone_id = m.id
|
|
41
|
+
WHERE m.id IS NULL`)
|
|
42
|
+
.all();
|
|
43
|
+
for (const row of orphanedSlices) {
|
|
44
|
+
issues.push({
|
|
45
|
+
severity: "error",
|
|
46
|
+
code: "db_orphaned_slice",
|
|
47
|
+
scope: "slice",
|
|
48
|
+
unitId: `${row.milestone_id}/${row.id}`,
|
|
49
|
+
message: `Slice ${row.id} references milestone ${row.milestone_id} but no such milestone exists in the database`,
|
|
50
|
+
fixable: false,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Non-fatal — orphaned slice check failed
|
|
56
|
+
}
|
|
57
|
+
// c. Tasks marked complete without summaries
|
|
58
|
+
try {
|
|
59
|
+
const doneTasks = adapter
|
|
60
|
+
.prepare(`SELECT id, slice_id, milestone_id FROM tasks
|
|
61
|
+
WHERE status = 'done' AND (summary IS NULL OR summary = '')`)
|
|
62
|
+
.all();
|
|
63
|
+
for (const row of doneTasks) {
|
|
64
|
+
issues.push({
|
|
65
|
+
severity: "warning",
|
|
66
|
+
code: "db_done_task_no_summary",
|
|
67
|
+
scope: "task",
|
|
68
|
+
unitId: `${row.milestone_id}/${row.slice_id}/${row.id}`,
|
|
69
|
+
message: `Task ${row.id} is marked done but has no summary in the database`,
|
|
70
|
+
fixable: false,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Non-fatal — done-task-no-summary check failed
|
|
76
|
+
}
|
|
77
|
+
// d. Duplicate entity IDs (safety check)
|
|
78
|
+
try {
|
|
79
|
+
const dupMilestones = adapter
|
|
80
|
+
.prepare("SELECT id, COUNT(*) as cnt FROM milestones GROUP BY id HAVING cnt > 1")
|
|
81
|
+
.all();
|
|
82
|
+
for (const row of dupMilestones) {
|
|
83
|
+
issues.push({
|
|
84
|
+
severity: "error",
|
|
85
|
+
code: "db_duplicate_id",
|
|
86
|
+
scope: "milestone",
|
|
87
|
+
unitId: row.id,
|
|
88
|
+
message: `Duplicate milestone ID "${row.id}" appears ${row.cnt} times in the database`,
|
|
89
|
+
fixable: false,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const dupSlices = adapter
|
|
93
|
+
.prepare("SELECT id, milestone_id, COUNT(*) as cnt FROM slices GROUP BY id, milestone_id HAVING cnt > 1")
|
|
94
|
+
.all();
|
|
95
|
+
for (const row of dupSlices) {
|
|
96
|
+
issues.push({
|
|
97
|
+
severity: "error",
|
|
98
|
+
code: "db_duplicate_id",
|
|
99
|
+
scope: "slice",
|
|
100
|
+
unitId: `${row.milestone_id}/${row.id}`,
|
|
101
|
+
message: `Duplicate slice ID "${row.id}" in milestone ${row.milestone_id} appears ${row.cnt} times`,
|
|
102
|
+
fixable: false,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const dupTasks = adapter
|
|
106
|
+
.prepare("SELECT id, slice_id, milestone_id, COUNT(*) as cnt FROM tasks GROUP BY id, slice_id, milestone_id HAVING cnt > 1")
|
|
107
|
+
.all();
|
|
108
|
+
for (const row of dupTasks) {
|
|
109
|
+
issues.push({
|
|
110
|
+
severity: "error",
|
|
111
|
+
code: "db_duplicate_id",
|
|
112
|
+
scope: "task",
|
|
113
|
+
unitId: `${row.milestone_id}/${row.slice_id}/${row.id}`,
|
|
114
|
+
message: `Duplicate task ID "${row.id}" in slice ${row.slice_id} appears ${row.cnt} times`,
|
|
115
|
+
fixable: false,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Non-fatal — duplicate ID check failed
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Non-fatal — DB constraint checks failed entirely
|
|
126
|
+
}
|
|
127
|
+
// ── Projection drift detection ──────────────────────────────────────────
|
|
128
|
+
// If the DB is available, check whether markdown projections are stale
|
|
129
|
+
// relative to the event log and re-render them.
|
|
130
|
+
try {
|
|
131
|
+
if (isDbAvailable()) {
|
|
132
|
+
const eventLogPath = join(basePath, ".gsd", "event-log.jsonl");
|
|
133
|
+
const events = readEvents(eventLogPath);
|
|
134
|
+
if (events.length > 0) {
|
|
135
|
+
const lastEventTs = new Date(events[events.length - 1].ts).getTime();
|
|
136
|
+
const state = await deriveState(basePath);
|
|
137
|
+
for (const milestone of state.registry) {
|
|
138
|
+
if (milestone.status === "complete")
|
|
139
|
+
continue;
|
|
140
|
+
const roadmapPath = resolveMilestoneFile(basePath, milestone.id, "ROADMAP");
|
|
141
|
+
if (!roadmapPath || !existsSync(roadmapPath)) {
|
|
142
|
+
try {
|
|
143
|
+
await renderAllProjections(basePath, milestone.id);
|
|
144
|
+
fixesApplied.push(`re-rendered missing projections for ${milestone.id}`);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Non-fatal — projection re-render failed
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const projectionMtime = statSync(roadmapPath).mtimeMs;
|
|
152
|
+
if (lastEventTs > projectionMtime) {
|
|
153
|
+
try {
|
|
154
|
+
await renderAllProjections(basePath, milestone.id);
|
|
155
|
+
fixesApplied.push(`re-rendered stale projections for ${milestone.id}`);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Non-fatal — projection re-render failed
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Non-fatal — projection drift check must never block doctor
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -121,20 +121,41 @@ function checkDependenciesInstalled(basePath) {
|
|
|
121
121
|
message: "node_modules missing — run npm install",
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
|
-
// Check if lockfile is newer than
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
// Check if lockfile is newer than the last install.
|
|
125
|
+
//
|
|
126
|
+
// Each package manager writes a metadata marker inside node_modules on
|
|
127
|
+
// every install. Comparing the lockfile mtime against the marker is
|
|
128
|
+
// reliable; comparing against the node_modules *directory* mtime is not,
|
|
129
|
+
// because directory mtime only changes when entries are added or removed
|
|
130
|
+
// — not when files inside it are updated. (#1974)
|
|
131
|
+
const lockfiles = [
|
|
132
|
+
{ lock: "package-lock.json", markers: ["node_modules/.package-lock.json"] },
|
|
133
|
+
{ lock: "yarn.lock", markers: ["node_modules/.yarn-integrity"] },
|
|
134
|
+
{ lock: "pnpm-lock.yaml", markers: ["node_modules/.modules.yaml"] },
|
|
135
|
+
];
|
|
136
|
+
for (const { lock, markers } of lockfiles) {
|
|
137
|
+
const lockPath = join(basePath, lock);
|
|
128
138
|
if (!existsSync(lockPath))
|
|
129
139
|
continue;
|
|
130
140
|
try {
|
|
131
141
|
const lockMtime = statSync(lockPath).mtimeMs;
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
// Prefer the package manager's marker file; fall back to directory mtime
|
|
143
|
+
// only when no marker exists (e.g., manually created node_modules).
|
|
144
|
+
let installMtime = 0;
|
|
145
|
+
for (const marker of markers) {
|
|
146
|
+
const markerPath = join(basePath, marker);
|
|
147
|
+
if (existsSync(markerPath)) {
|
|
148
|
+
installMtime = Math.max(installMtime, statSync(markerPath).mtimeMs);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (installMtime === 0) {
|
|
152
|
+
installMtime = statSync(nodeModules).mtimeMs;
|
|
153
|
+
}
|
|
154
|
+
if (lockMtime > installMtime) {
|
|
134
155
|
return {
|
|
135
156
|
name: "dependencies",
|
|
136
157
|
status: "warning",
|
|
137
|
-
message: `${
|
|
158
|
+
message: `${lock} is newer than node_modules — dependencies may be stale`,
|
|
138
159
|
detail: `Run npm install / yarn / pnpm install to update`,
|
|
139
160
|
};
|
|
140
161
|
}
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { existsSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
2
|
+
import { join, sep } from "node:path";
|
|
3
|
+
import { loadFile } from "./files.js";
|
|
4
|
+
import { parseRoadmap as parseLegacyRoadmap } from "./parsers-legacy.js";
|
|
5
|
+
import { isDbAvailable, getMilestoneSlices } from "./gsd-db.js";
|
|
6
|
+
import { resolveMilestoneFile } from "./paths.js";
|
|
7
|
+
import { deriveState, isMilestoneComplete } from "./state.js";
|
|
8
|
+
import { listWorktrees, resolveGitDir, worktreesDir } from "./worktree-manager.js";
|
|
9
|
+
import { abortAndReset } from "./git-self-heal.js";
|
|
10
|
+
import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch, writeIntegrationBranch } from "./git-service.js";
|
|
11
|
+
import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached } from "./native-git-bridge.js";
|
|
12
|
+
import { getAllWorktreeHealth } from "./worktree-health.js";
|
|
13
|
+
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
14
|
+
export async function checkGitHealth(basePath, issues, fixesApplied, shouldFix, isolationMode = "none") {
|
|
15
|
+
// Degrade gracefully if not a git repo
|
|
16
|
+
if (!nativeIsRepo(basePath)) {
|
|
17
|
+
return; // Not a git repo — skip all git health checks
|
|
18
|
+
}
|
|
19
|
+
const gitDir = resolveGitDir(basePath);
|
|
20
|
+
// ── Orphaned auto-worktrees & Stale milestone branches ────────────────
|
|
21
|
+
// These checks only apply in worktree/branch modes — skip in none mode
|
|
22
|
+
// where no milestone worktrees or branches are created.
|
|
23
|
+
if (isolationMode !== "none") {
|
|
24
|
+
try {
|
|
25
|
+
const worktrees = listWorktrees(basePath);
|
|
26
|
+
const milestoneWorktrees = worktrees.filter(wt => wt.branch.startsWith("milestone/"));
|
|
27
|
+
// Load roadmap state once for cross-referencing
|
|
28
|
+
const state = await deriveState(basePath);
|
|
29
|
+
for (const wt of milestoneWorktrees) {
|
|
30
|
+
// Extract milestone ID from branch name "milestone/M001" → "M001"
|
|
31
|
+
const milestoneId = wt.branch.replace(/^milestone\//, "");
|
|
32
|
+
const milestoneEntry = state.registry.find(m => m.id === milestoneId);
|
|
33
|
+
// Check if milestone is complete via roadmap
|
|
34
|
+
let isComplete = false;
|
|
35
|
+
if (milestoneEntry) {
|
|
36
|
+
if (isDbAvailable()) {
|
|
37
|
+
const dbSlices = getMilestoneSlices(milestoneId);
|
|
38
|
+
isComplete = dbSlices.length > 0 && dbSlices.every(s => s.status === "complete");
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
42
|
+
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
|
|
43
|
+
if (roadmapContent) {
|
|
44
|
+
const roadmap = parseLegacyRoadmap(roadmapContent);
|
|
45
|
+
isComplete = isMilestoneComplete(roadmap);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// When DB unavailable and no roadmap, isComplete stays false
|
|
49
|
+
}
|
|
50
|
+
if (isComplete) {
|
|
51
|
+
issues.push({
|
|
52
|
+
severity: "warning",
|
|
53
|
+
code: "orphaned_auto_worktree",
|
|
54
|
+
scope: "milestone",
|
|
55
|
+
unitId: milestoneId,
|
|
56
|
+
message: `Worktree for completed milestone ${milestoneId} still exists at ${wt.path}`,
|
|
57
|
+
fixable: true,
|
|
58
|
+
});
|
|
59
|
+
if (shouldFix("orphaned_auto_worktree")) {
|
|
60
|
+
// If cwd is inside the worktree, chdir out first — matching the
|
|
61
|
+
// pattern in removeWorktree() (#1946). Without this, git cannot
|
|
62
|
+
// remove the worktree and the doctor enters a deadlock where it
|
|
63
|
+
// detects the orphan every run but never cleans it up.
|
|
64
|
+
const cwd = process.cwd();
|
|
65
|
+
if (wt.path === cwd || cwd.startsWith(wt.path + sep)) {
|
|
66
|
+
try {
|
|
67
|
+
process.chdir(basePath);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
fixesApplied.push(`skipped removing worktree at ${wt.path} (cannot chdir to basePath)`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
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
|
+
// ── Stale milestone branches ─────────────────────────────────────────
|
|
85
|
+
try {
|
|
86
|
+
const branches = nativeBranchList(basePath, "milestone/*");
|
|
87
|
+
if (branches.length > 0) {
|
|
88
|
+
const worktreeBranches = new Set(milestoneWorktrees.map(wt => wt.branch));
|
|
89
|
+
for (const branch of branches) {
|
|
90
|
+
// Skip branches that have a worktree (handled above)
|
|
91
|
+
if (worktreeBranches.has(branch))
|
|
92
|
+
continue;
|
|
93
|
+
const milestoneId = branch.replace(/^milestone\//, "");
|
|
94
|
+
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
95
|
+
let branchMilestoneComplete = false;
|
|
96
|
+
if (isDbAvailable()) {
|
|
97
|
+
const dbSlices = getMilestoneSlices(milestoneId);
|
|
98
|
+
branchMilestoneComplete = dbSlices.length > 0 && dbSlices.every(s => s.status === "complete");
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
|
|
102
|
+
if (!roadmapContent)
|
|
103
|
+
continue;
|
|
104
|
+
const roadmap = parseLegacyRoadmap(roadmapContent);
|
|
105
|
+
branchMilestoneComplete = isMilestoneComplete(roadmap);
|
|
106
|
+
}
|
|
107
|
+
if (branchMilestoneComplete) {
|
|
108
|
+
issues.push({
|
|
109
|
+
severity: "info",
|
|
110
|
+
code: "stale_milestone_branch",
|
|
111
|
+
scope: "milestone",
|
|
112
|
+
unitId: milestoneId,
|
|
113
|
+
message: `Branch ${branch} exists for completed milestone ${milestoneId}`,
|
|
114
|
+
fixable: true,
|
|
115
|
+
});
|
|
116
|
+
if (shouldFix("stale_milestone_branch")) {
|
|
117
|
+
try {
|
|
118
|
+
nativeBranchDelete(basePath, branch, true);
|
|
119
|
+
fixesApplied.push(`deleted stale branch ${branch}`);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
fixesApplied.push(`failed to delete branch ${branch}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// git branch list failed — skip stale branch check
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// listWorktrees or deriveState failed — skip worktree/branch checks
|
|
135
|
+
}
|
|
136
|
+
} // end isolationMode !== "none"
|
|
137
|
+
// ── Corrupt merge state ────────────────────────────────────────────────
|
|
138
|
+
try {
|
|
139
|
+
const mergeStateFiles = ["MERGE_HEAD", "SQUASH_MSG"];
|
|
140
|
+
const mergeStateDirs = ["rebase-apply", "rebase-merge"];
|
|
141
|
+
const found = [];
|
|
142
|
+
for (const f of mergeStateFiles) {
|
|
143
|
+
if (existsSync(join(gitDir, f)))
|
|
144
|
+
found.push(f);
|
|
145
|
+
}
|
|
146
|
+
for (const d of mergeStateDirs) {
|
|
147
|
+
if (existsSync(join(gitDir, d)))
|
|
148
|
+
found.push(d);
|
|
149
|
+
}
|
|
150
|
+
if (found.length > 0) {
|
|
151
|
+
issues.push({
|
|
152
|
+
severity: "error",
|
|
153
|
+
code: "corrupt_merge_state",
|
|
154
|
+
scope: "project",
|
|
155
|
+
unitId: "project",
|
|
156
|
+
message: `Corrupt merge/rebase state detected: ${found.join(", ")}`,
|
|
157
|
+
fixable: true,
|
|
158
|
+
});
|
|
159
|
+
if (shouldFix("corrupt_merge_state")) {
|
|
160
|
+
const result = abortAndReset(basePath);
|
|
161
|
+
fixesApplied.push(`cleaned merge state: ${result.cleaned.join(", ")}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Can't check .git dir — skip
|
|
167
|
+
}
|
|
168
|
+
// ── Tracked runtime files ──────────────────────────────────────────────
|
|
169
|
+
try {
|
|
170
|
+
const trackedPaths = [];
|
|
171
|
+
for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
|
|
172
|
+
try {
|
|
173
|
+
const files = nativeLsFiles(basePath, exclusion);
|
|
174
|
+
if (files.length > 0) {
|
|
175
|
+
trackedPaths.push(...files);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Individual ls-files can fail — continue
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (trackedPaths.length > 0) {
|
|
183
|
+
issues.push({
|
|
184
|
+
severity: "warning",
|
|
185
|
+
code: "tracked_runtime_files",
|
|
186
|
+
scope: "project",
|
|
187
|
+
unitId: "project",
|
|
188
|
+
message: `${trackedPaths.length} runtime file(s) are tracked by git: ${trackedPaths.slice(0, 5).join(", ")}${trackedPaths.length > 5 ? "..." : ""}`,
|
|
189
|
+
fixable: true,
|
|
190
|
+
});
|
|
191
|
+
if (shouldFix("tracked_runtime_files")) {
|
|
192
|
+
try {
|
|
193
|
+
for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
|
|
194
|
+
nativeRmCached(basePath, [exclusion]);
|
|
195
|
+
}
|
|
196
|
+
fixesApplied.push(`untracked ${trackedPaths.length} runtime file(s)`);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
fixesApplied.push("failed to untrack runtime files");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// git ls-files failed — skip
|
|
206
|
+
}
|
|
207
|
+
// ── Legacy slice branches ──────────────────────────────────────────────
|
|
208
|
+
try {
|
|
209
|
+
const branchList = nativeBranchList(basePath, "gsd/*/*")
|
|
210
|
+
.filter((branch) => !branch.startsWith("gsd/quick/"));
|
|
211
|
+
if (branchList.length > 0) {
|
|
212
|
+
issues.push({
|
|
213
|
+
severity: "info",
|
|
214
|
+
code: "legacy_slice_branches",
|
|
215
|
+
scope: "project",
|
|
216
|
+
unitId: "project",
|
|
217
|
+
message: `${branchList.length} legacy slice branch(es) found: ${branchList.slice(0, 3).join(", ")}${branchList.length > 3 ? "..." : ""}. These are no longer used (branchless architecture).`,
|
|
218
|
+
fixable: true,
|
|
219
|
+
});
|
|
220
|
+
if (shouldFix("legacy_slice_branches")) {
|
|
221
|
+
let deleted = 0;
|
|
222
|
+
for (const branch of branchList) {
|
|
223
|
+
try {
|
|
224
|
+
nativeBranchDelete(basePath, branch, true);
|
|
225
|
+
deleted++;
|
|
226
|
+
}
|
|
227
|
+
catch { /* skip branches that can't be deleted */ }
|
|
228
|
+
}
|
|
229
|
+
if (deleted > 0) {
|
|
230
|
+
fixesApplied.push(`deleted ${deleted} legacy slice branch(es)`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// git branch list failed — skip
|
|
237
|
+
}
|
|
238
|
+
// ── Integration branch existence ──────────────────────────────────────
|
|
239
|
+
// For each active (non-complete) milestone, verify the stored integration
|
|
240
|
+
// branch still exists in git. A missing integration branch blocks merge-back
|
|
241
|
+
// and causes the next merge operation to fail silently.
|
|
242
|
+
try {
|
|
243
|
+
const state = await deriveState(basePath);
|
|
244
|
+
const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
|
|
245
|
+
for (const milestone of state.registry) {
|
|
246
|
+
if (milestone.status === "complete")
|
|
247
|
+
continue;
|
|
248
|
+
const resolution = resolveMilestoneIntegrationBranch(basePath, milestone.id, gitPrefs);
|
|
249
|
+
if (!resolution.recordedBranch)
|
|
250
|
+
continue; // No stored branch — skip (not yet set)
|
|
251
|
+
if (resolution.status === "fallback" && resolution.effectiveBranch) {
|
|
252
|
+
issues.push({
|
|
253
|
+
severity: "warning",
|
|
254
|
+
code: "integration_branch_missing",
|
|
255
|
+
scope: "milestone",
|
|
256
|
+
unitId: milestone.id,
|
|
257
|
+
message: resolution.reason,
|
|
258
|
+
fixable: true,
|
|
259
|
+
});
|
|
260
|
+
if (shouldFix("integration_branch_missing")) {
|
|
261
|
+
writeIntegrationBranch(basePath, milestone.id, resolution.effectiveBranch);
|
|
262
|
+
fixesApplied.push(`updated integration branch for ${milestone.id} to "${resolution.effectiveBranch}"`);
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (resolution.status === "missing") {
|
|
267
|
+
issues.push({
|
|
268
|
+
severity: "error",
|
|
269
|
+
code: "integration_branch_missing",
|
|
270
|
+
scope: "milestone",
|
|
271
|
+
unitId: milestone.id,
|
|
272
|
+
message: resolution.reason,
|
|
273
|
+
fixable: false,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Non-fatal — integration branch check failed
|
|
280
|
+
}
|
|
281
|
+
// ── Orphaned worktree directories ────────────────────────────────────
|
|
282
|
+
// Worktree removal can fail after a branch delete, leaving a directory
|
|
283
|
+
// that is no longer registered with git. These orphaned dirs cause
|
|
284
|
+
// "already exists" errors when re-creating the same worktree name.
|
|
285
|
+
try {
|
|
286
|
+
const wtDir = worktreesDir(basePath);
|
|
287
|
+
if (existsSync(wtDir)) {
|
|
288
|
+
// Resolve symlinks and normalize separators so that symlinked .gsd
|
|
289
|
+
// paths (e.g. ~/.gsd/projects/<hash>/worktrees/…) match the paths
|
|
290
|
+
// returned by `git worktree list`.
|
|
291
|
+
const normalizePath = (p) => {
|
|
292
|
+
try {
|
|
293
|
+
p = realpathSync(p);
|
|
294
|
+
}
|
|
295
|
+
catch { /* path may not exist */ }
|
|
296
|
+
return p.replaceAll("\\", "/");
|
|
297
|
+
};
|
|
298
|
+
const registeredPaths = new Set(nativeWorktreeList(basePath).map(entry => normalizePath(entry.path)));
|
|
299
|
+
for (const entry of readdirSync(wtDir)) {
|
|
300
|
+
const fullPath = join(wtDir, entry);
|
|
301
|
+
try {
|
|
302
|
+
if (!statSync(fullPath).isDirectory())
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const normalizedFullPath = normalizePath(fullPath);
|
|
309
|
+
if (!registeredPaths.has(normalizedFullPath)) {
|
|
310
|
+
issues.push({
|
|
311
|
+
severity: "warning",
|
|
312
|
+
code: "worktree_directory_orphaned",
|
|
313
|
+
scope: "project",
|
|
314
|
+
unitId: entry,
|
|
315
|
+
message: `Worktree directory ${fullPath} exists on disk but is not registered with git. Run "git worktree prune" or doctor --fix to remove it.`,
|
|
316
|
+
fixable: true,
|
|
317
|
+
});
|
|
318
|
+
if (shouldFix("worktree_directory_orphaned")) {
|
|
319
|
+
try {
|
|
320
|
+
rmSync(fullPath, { recursive: true, force: true });
|
|
321
|
+
fixesApplied.push(`removed orphaned worktree directory ${fullPath}`);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
fixesApplied.push(`failed to remove orphaned worktree directory ${fullPath}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
// Non-fatal — orphaned worktree directory check failed
|
|
333
|
+
}
|
|
334
|
+
// ── Worktree lifecycle checks ──────────────────────────────────────────
|
|
335
|
+
// Check GSD-managed worktrees for: merged branches, stale work, dirty
|
|
336
|
+
// state, and unpushed commits. Only worktrees under .gsd/worktrees/.
|
|
337
|
+
try {
|
|
338
|
+
const healthStatuses = getAllWorktreeHealth(basePath);
|
|
339
|
+
const cwd = process.cwd();
|
|
340
|
+
for (const health of healthStatuses) {
|
|
341
|
+
const wt = health.worktree;
|
|
342
|
+
const isCwd = wt.path === cwd || cwd.startsWith(wt.path + sep);
|
|
343
|
+
// Branch fully merged into main — safe to remove
|
|
344
|
+
if (health.mergedIntoMain) {
|
|
345
|
+
issues.push({
|
|
346
|
+
severity: "info",
|
|
347
|
+
code: "worktree_branch_merged",
|
|
348
|
+
scope: "project",
|
|
349
|
+
unitId: wt.name,
|
|
350
|
+
message: `Worktree "${wt.name}" (branch ${wt.branch}) is fully merged into main${health.safeToRemove ? " — safe to remove" : ""}`,
|
|
351
|
+
fixable: health.safeToRemove,
|
|
352
|
+
});
|
|
353
|
+
if (health.safeToRemove && shouldFix("worktree_branch_merged") && !isCwd) {
|
|
354
|
+
try {
|
|
355
|
+
const { removeWorktree } = await import("./worktree-manager.js");
|
|
356
|
+
removeWorktree(basePath, wt.name, { deleteBranch: true, branch: wt.branch });
|
|
357
|
+
fixesApplied.push(`removed merged worktree "${wt.name}" and deleted branch ${wt.branch}`);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
fixesApplied.push(`failed to remove merged worktree "${wt.name}"`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// If merged, skip the stale/dirty/unpushed checks — they're irrelevant
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
// Stale: no commits in N days, not merged
|
|
367
|
+
if (health.stale) {
|
|
368
|
+
const days = Math.floor(health.lastCommitAgeDays);
|
|
369
|
+
issues.push({
|
|
370
|
+
severity: "warning",
|
|
371
|
+
code: "worktree_stale",
|
|
372
|
+
scope: "project",
|
|
373
|
+
unitId: wt.name,
|
|
374
|
+
message: `Worktree "${wt.name}" has had no commits in ${days} day${days === 1 ? "" : "s"}`,
|
|
375
|
+
fixable: false,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
// Dirty: uncommitted changes in a worktree (only flag on stale worktrees to avoid noise)
|
|
379
|
+
if (health.dirty && health.stale) {
|
|
380
|
+
issues.push({
|
|
381
|
+
severity: "warning",
|
|
382
|
+
code: "worktree_dirty",
|
|
383
|
+
scope: "project",
|
|
384
|
+
unitId: wt.name,
|
|
385
|
+
message: `Worktree "${wt.name}" has ${health.dirtyFileCount} uncommitted file${health.dirtyFileCount === 1 ? "" : "s"} and is stale`,
|
|
386
|
+
fixable: false,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
// Unpushed: commits not on any remote (only flag on stale worktrees to avoid noise)
|
|
390
|
+
if (health.unpushedCommits > 0 && health.stale) {
|
|
391
|
+
issues.push({
|
|
392
|
+
severity: "warning",
|
|
393
|
+
code: "worktree_unpushed",
|
|
394
|
+
scope: "project",
|
|
395
|
+
unitId: wt.name,
|
|
396
|
+
message: `Worktree "${wt.name}" has ${health.unpushedCommits} unpushed commit${health.unpushedCommits === 1 ? "" : "s"}`,
|
|
397
|
+
fixable: false,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
// Non-fatal — worktree lifecycle check failed
|
|
404
|
+
}
|
|
405
|
+
}
|