@opengsd/gsd-pi 1.6.0 → 1.7.0-dev.30c4ec2f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -10
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +19 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +20 -0
- package/dist/resources/extensions/gsd/auto.js +12 -0
- package/dist/resources/extensions/gsd/browser-daemon-auto-prep.js +68 -2
- package/dist/resources/extensions/gsd/doctor-history.js +65 -0
- package/dist/resources/extensions/gsd/doctor-state-checks.js +509 -0
- package/dist/resources/extensions/gsd/doctor-workspace-checks.js +91 -0
- package/dist/resources/extensions/gsd/doctor.js +14 -653
- package/dist/resources/extensions/gsd/md-importer.js +7 -2
- package/dist/resources/extensions/gsd/migrate/planning-writer.js +8 -2
- package/dist/resources/extensions/gsd/migration-auto-check.js +4 -0
- package/dist/resources/extensions/gsd/reopen-reason.js +98 -0
- package/dist/resources/extensions/gsd/session-status-io.js +31 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +54 -14
- package/dist/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.js +48 -5
- package/dist/resources/extensions/gsd/tools/reopen-task.js +15 -0
- package/dist/resources/extensions/shared/gsd-browser-cli.js +22 -5
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/mcp-connections/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
- package/dist/web/standalone/.next/server/chunks/2842.js +4 -4
- package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware.js +3 -3
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/app/_not-found/page-a6fb1847f67f167c.js +1 -0
- package/dist/web/standalone/.next/static/chunks/app/{layout-b35cbfff38aaf4cf.js → layout-4ae2d68984392bbf.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-72a856634ad14c10.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{main-app-590a74400e35f685.js → main-app-90d1d8d5e5d2dc6b.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-1115d46ac61b9823.js +1 -0
- package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
- package/integrations/hermes/pyproject.toml +1 -1
- package/package.json +6 -6
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/dist/cloud-cli.d.ts +1 -1
- package/packages/daemon/dist/cloud-cli.d.ts.map +1 -1
- package/packages/daemon/dist/cloud-cli.js +42 -2
- package/packages/daemon/dist/cloud-cli.js.map +1 -1
- package/packages/daemon/dist/cloud-config.d.ts +1 -0
- package/packages/daemon/dist/cloud-config.d.ts.map +1 -1
- package/packages/daemon/dist/cloud-config.js +26 -8
- package/packages/daemon/dist/cloud-config.js.map +1 -1
- package/packages/daemon/dist/cloud-config.test.js +19 -1
- package/packages/daemon/dist/cloud-config.test.js.map +1 -1
- package/packages/daemon/dist/device-flow.d.ts +24 -0
- package/packages/daemon/dist/device-flow.d.ts.map +1 -0
- package/packages/daemon/dist/device-flow.js +128 -0
- package/packages/daemon/dist/device-flow.js.map +1 -0
- package/packages/daemon/dist/device-flow.test.d.ts +2 -0
- package/packages/daemon/dist/device-flow.test.d.ts.map +1 -0
- package/packages/daemon/dist/device-flow.test.js +172 -0
- package/packages/daemon/dist/device-flow.test.js.map +1 -0
- package/packages/daemon/package.json +5 -5
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/gsd-cloud/README.md +48 -0
- package/packages/gsd-cloud/bin/gsd-cloud.js +28 -0
- package/packages/gsd-cloud/package.json +40 -0
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +19 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +22 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +20 -0
- package/src/resources/extensions/gsd/auto.ts +12 -0
- package/src/resources/extensions/gsd/browser-daemon-auto-prep.ts +77 -1
- package/src/resources/extensions/gsd/doctor-history.ts +81 -0
- package/src/resources/extensions/gsd/doctor-state-checks.ts +527 -0
- package/src/resources/extensions/gsd/doctor-workspace-checks.ts +97 -0
- package/src/resources/extensions/gsd/doctor.ts +14 -673
- package/src/resources/extensions/gsd/md-importer.ts +7 -2
- package/src/resources/extensions/gsd/migrate/planning-writer.ts +9 -3
- package/src/resources/extensions/gsd/migration-auto-check.ts +3 -0
- package/src/resources/extensions/gsd/reopen-reason.ts +109 -0
- package/src/resources/extensions/gsd/session-status-io.ts +34 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +67 -20
- package/src/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.ts +47 -5
- package/src/resources/extensions/gsd/tests/browser-daemon-auto-prep.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/db-authority-regression.test.ts +76 -1
- package/src/resources/extensions/gsd/tests/doctor-history-public-api.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +53 -0
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +53 -0
- package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/planning-writer.test.ts +18 -1
- package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +18 -3
- package/src/resources/extensions/gsd/tests/reopen-reason.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +177 -25
- package/src/resources/extensions/gsd/tests/worker-resume-wait.test.ts +73 -0
- package/src/resources/extensions/gsd/tools/reopen-task.ts +15 -0
- package/src/resources/extensions/shared/gsd-browser-cli.ts +35 -7
- package/dist/web/standalone/.next/static/chunks/app/_not-found/page-49f565245e1e4afe.js +0 -1
- package/dist/web/standalone/.next/static/chunks/app/page-bc4c723c6a19c6f0.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ec3eaffc9785ba48.js +0 -1
- /package/dist/web/standalone/.next/static/{6xYMOzxVhsQgryneJ5KRc → ue2JDvA1wboxPBPeblD5N}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{6xYMOzxVhsQgryneJ5KRc → ue2JDvA1wboxPBPeblD5N}/_ssgManifest.js +0 -0
|
@@ -1,25 +1,21 @@
|
|
|
1
|
-
import { existsSync
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js";
|
|
6
|
-
import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { loadFile, saveFile } from "./files.js";
|
|
3
|
+
import { parseRoadmap as parseLegacyRoadmap } from "./parsers-legacy.js";
|
|
4
|
+
import { isDbAvailable, getMilestoneSlices } from "./gsd-db.js";
|
|
7
5
|
import { openExistingWorkflowDatabase } from "./db-workspace.js";
|
|
8
|
-
import { resolveMilestoneFile,
|
|
9
|
-
import { findMilestoneIds } from "./milestone-ids.js";
|
|
6
|
+
import { resolveMilestoneFile, milestonesDir, legacyMilestonesDir, resolveGsdRootFile } from "./paths.js";
|
|
10
7
|
import { deriveState, isMilestoneComplete } from "./state.js";
|
|
11
8
|
import { invalidateAllCaches } from "./cache.js";
|
|
12
9
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
10
|
+
import { appendDoctorHistory } from "./doctor-history.js";
|
|
11
|
+
import { checkWorkspaceRepositoryHealth } from "./doctor-workspace-checks.js";
|
|
13
12
|
import { collectPreferenceDiagnostics, formatPreferenceDiagnosticDetail } from "./preferences-diagnostics.js";
|
|
14
|
-
import { isClosedStatus } from "./status-guards.js";
|
|
15
13
|
import { GLOBAL_STATE_CODES } from "./doctor-types.js";
|
|
16
|
-
import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
|
|
17
|
-
import { createRepositoryRegistryFromPreferences } from "./repository-registry.js";
|
|
18
14
|
import { checkGitHealth, checkRuntimeHealth, checkGlobalHealth, checkEngineHealth } from "./doctor-checks.js";
|
|
19
15
|
import { checkEnvironmentHealth } from "./doctor-environment.js";
|
|
20
|
-
import {
|
|
21
|
-
import { validateTitle } from "./validation.js";
|
|
16
|
+
import { checkGsdStateHealth } from "./doctor-state-checks.js";
|
|
22
17
|
export { summarizeDoctorIssues, filterDoctorIssues, formatDoctorReport, formatDoctorIssuesForPrompt, formatDoctorReportJson } from "./doctor-format.js";
|
|
18
|
+
export { readDoctorHistory } from "./doctor-history.js";
|
|
23
19
|
export { runEnvironmentChecks, runFullEnvironmentChecks, formatEnvironmentReport } from "./doctor-environment.js";
|
|
24
20
|
export { computeProgressScore, computeProgressScoreWithContext, formatProgressLine, formatProgressReport } from "./progress-score.js";
|
|
25
21
|
export { validateTitle } from "./validation.js";
|
|
@@ -115,55 +111,6 @@ export async function rebuildState(basePath) {
|
|
|
115
111
|
const path = resolveGsdRootFile(basePath, "STATE");
|
|
116
112
|
await saveFile(path, buildStateMarkdown(state));
|
|
117
113
|
}
|
|
118
|
-
function matchesScope(unitId, scope) {
|
|
119
|
-
if (!scope)
|
|
120
|
-
return true;
|
|
121
|
-
return unitId === scope || unitId.startsWith(`${scope}/`);
|
|
122
|
-
}
|
|
123
|
-
function auditRequirements(content) {
|
|
124
|
-
if (!content)
|
|
125
|
-
return [];
|
|
126
|
-
const issues = [];
|
|
127
|
-
const blocks = content.split(/^###\s+/m).slice(1);
|
|
128
|
-
for (const block of blocks) {
|
|
129
|
-
const idMatch = block.match(/^(R\d+)/);
|
|
130
|
-
if (!idMatch)
|
|
131
|
-
continue;
|
|
132
|
-
const requirementId = idMatch[1];
|
|
133
|
-
const status = block.match(/^-\s+Status:\s+(.+)$/m)?.[1]?.trim().toLowerCase() ?? "";
|
|
134
|
-
const owner = block.match(/^-\s+Primary owning slice:\s+(.+)$/m)?.[1]?.trim().toLowerCase() ?? "";
|
|
135
|
-
const notes = block.match(/^-\s+Notes:\s+(.+)$/m)?.[1]?.trim().toLowerCase() ?? "";
|
|
136
|
-
if (status === "active" && (!owner || owner === "none" || owner === "none yet")) {
|
|
137
|
-
// #4414: Downgrade to warning. A newly-created requirement has
|
|
138
|
-
// primary_owner='' by default until the planning agent wires it to
|
|
139
|
-
// a slice via gsd_requirement_update. Flagging this as an error
|
|
140
|
-
// during normal planning is noisy — the real failure mode is when
|
|
141
|
-
// it persists past milestone completion, which is covered by other
|
|
142
|
-
// audits. Keep the signal but don't treat it as a blocker.
|
|
143
|
-
issues.push({
|
|
144
|
-
severity: "warning",
|
|
145
|
-
code: "active_requirement_missing_owner",
|
|
146
|
-
scope: "project",
|
|
147
|
-
unitId: requirementId,
|
|
148
|
-
message: `${requirementId} is Active but has no primary owning slice`,
|
|
149
|
-
file: relGsdRootFile("REQUIREMENTS"),
|
|
150
|
-
fixable: false,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
if (status === "blocked" && !notes) {
|
|
154
|
-
issues.push({
|
|
155
|
-
severity: "warning",
|
|
156
|
-
code: "blocked_requirement_missing_reason",
|
|
157
|
-
scope: "project",
|
|
158
|
-
unitId: requirementId,
|
|
159
|
-
message: `${requirementId} is Blocked but has no reason in Notes`,
|
|
160
|
-
file: relGsdRootFile("REQUIREMENTS"),
|
|
161
|
-
fixable: false,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return issues;
|
|
166
|
-
}
|
|
167
114
|
export async function selectDoctorScope(basePath, requestedScope) {
|
|
168
115
|
if (requestedScope)
|
|
169
116
|
return requestedScope;
|
|
@@ -197,179 +144,6 @@ export async function selectDoctorScope(basePath, requestedScope) {
|
|
|
197
144
|
}
|
|
198
145
|
return state.registry[0]?.id;
|
|
199
146
|
}
|
|
200
|
-
// ── Helper: circular dependency detection ──────────────────────────────────
|
|
201
|
-
function detectCircularDependencies(slices) {
|
|
202
|
-
const known = new Set(slices.map(s => s.id));
|
|
203
|
-
const adj = new Map();
|
|
204
|
-
for (const s of slices)
|
|
205
|
-
adj.set(s.id, s.depends.filter(d => known.has(d)));
|
|
206
|
-
const state = new Map();
|
|
207
|
-
for (const s of slices)
|
|
208
|
-
state.set(s.id, "unvisited");
|
|
209
|
-
const cycles = [];
|
|
210
|
-
function dfs(id, path) {
|
|
211
|
-
const st = state.get(id);
|
|
212
|
-
if (st === "done")
|
|
213
|
-
return;
|
|
214
|
-
if (st === "visiting") {
|
|
215
|
-
cycles.push([...path.slice(path.indexOf(id)), id]);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
state.set(id, "visiting");
|
|
219
|
-
for (const dep of adj.get(id) ?? [])
|
|
220
|
-
dfs(dep, [...path, id]);
|
|
221
|
-
state.set(id, "done");
|
|
222
|
-
}
|
|
223
|
-
for (const s of slices)
|
|
224
|
-
if (state.get(s.id) === "unvisited")
|
|
225
|
-
dfs(s.id, []);
|
|
226
|
-
return cycles;
|
|
227
|
-
}
|
|
228
|
-
async function appendDoctorHistory(basePath, report) {
|
|
229
|
-
try {
|
|
230
|
-
const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
|
|
231
|
-
const errorCount = report.issues.filter(i => i.severity === "error").length;
|
|
232
|
-
const warningCount = report.issues.filter(i => i.severity === "warning").length;
|
|
233
|
-
const issueDetails = report.issues
|
|
234
|
-
.filter(i => i.severity === "error" || i.severity === "warning")
|
|
235
|
-
.slice(0, 10) // cap to keep JSONL lines bounded
|
|
236
|
-
.map(i => ({ severity: i.severity, code: i.code, message: i.message, unitId: i.unitId }));
|
|
237
|
-
// Human-readable one-line summary
|
|
238
|
-
const summaryParts = [];
|
|
239
|
-
if (report.ok) {
|
|
240
|
-
summaryParts.push("Clean");
|
|
241
|
-
}
|
|
242
|
-
else {
|
|
243
|
-
const counts = [];
|
|
244
|
-
if (errorCount > 0)
|
|
245
|
-
counts.push(`${errorCount} error${errorCount > 1 ? "s" : ""}`);
|
|
246
|
-
if (warningCount > 0)
|
|
247
|
-
counts.push(`${warningCount} warning${warningCount > 1 ? "s" : ""}`);
|
|
248
|
-
summaryParts.push(counts.join(", "));
|
|
249
|
-
}
|
|
250
|
-
if (report.fixesApplied.length > 0) {
|
|
251
|
-
summaryParts.push(`${report.fixesApplied.length} fixed`);
|
|
252
|
-
}
|
|
253
|
-
if (issueDetails.length > 0) {
|
|
254
|
-
const topIssue = issueDetails.find(i => i.severity === "error") ?? issueDetails[0];
|
|
255
|
-
summaryParts.push(topIssue.message);
|
|
256
|
-
}
|
|
257
|
-
const entry = JSON.stringify({
|
|
258
|
-
ts: new Date().toISOString(),
|
|
259
|
-
ok: report.ok,
|
|
260
|
-
errors: errorCount,
|
|
261
|
-
warnings: warningCount,
|
|
262
|
-
fixes: report.fixesApplied.length,
|
|
263
|
-
codes: [...new Set(report.issues.map(i => i.code))],
|
|
264
|
-
issues: issueDetails.length > 0 ? issueDetails : undefined,
|
|
265
|
-
fixDescriptions: report.fixesApplied.length > 0 ? report.fixesApplied : undefined,
|
|
266
|
-
scope: report.scope,
|
|
267
|
-
summary: summaryParts.join(" · "),
|
|
268
|
-
});
|
|
269
|
-
const existing = existsSync(historyPath) ? readFileSync(historyPath, "utf-8") : "";
|
|
270
|
-
await saveFile(historyPath, existing + entry + "\n");
|
|
271
|
-
}
|
|
272
|
-
catch { /* non-fatal */ }
|
|
273
|
-
}
|
|
274
|
-
/** Read the last N doctor history entries. Returns most-recent-first. */
|
|
275
|
-
export async function readDoctorHistory(basePath, lastN = 50) {
|
|
276
|
-
try {
|
|
277
|
-
const historyPath = join(gsdRoot(basePath), "doctor-history.jsonl");
|
|
278
|
-
if (!existsSync(historyPath))
|
|
279
|
-
return [];
|
|
280
|
-
const lines = readFileSync(historyPath, "utf-8").split("\n").filter(l => l.trim());
|
|
281
|
-
return lines.slice(-lastN).reverse().map(l => JSON.parse(l));
|
|
282
|
-
}
|
|
283
|
-
catch {
|
|
284
|
-
return [];
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Resolve the git working-tree root for a path, or null if it is not a repo.
|
|
289
|
-
* Used by the workspace-repository probe to check that a declared child path is
|
|
290
|
-
* itself a git repository (not merely nested inside the parent's repo).
|
|
291
|
-
*/
|
|
292
|
-
function resolveGitToplevel(cwd) {
|
|
293
|
-
try {
|
|
294
|
-
const out = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
295
|
-
cwd,
|
|
296
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
297
|
-
encoding: "utf-8",
|
|
298
|
-
env: GIT_NO_PROMPT_ENV,
|
|
299
|
-
}).trim();
|
|
300
|
-
return out ? resolve(out) : null;
|
|
301
|
-
}
|
|
302
|
-
catch {
|
|
303
|
-
return null;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
/** realpath that falls back to the resolved path if the link cannot be read. */
|
|
307
|
-
function realpathSafe(p) {
|
|
308
|
-
try {
|
|
309
|
-
return realpathSync(p);
|
|
310
|
-
}
|
|
311
|
-
catch {
|
|
312
|
-
return resolve(p);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Parent-workspace probe (#818): validate that every declared child repository
|
|
317
|
-
* exists on disk and is a git repository at its own root. The registry only
|
|
318
|
-
* checks paths stay inside the project root; it never checks existence or
|
|
319
|
-
* git-ness, so a typo'd path (e.g. `frontned`) would otherwise build cleanly
|
|
320
|
-
* and silently produce "no problems found." No-op for single-repo projects.
|
|
321
|
-
*/
|
|
322
|
-
function checkWorkspaceRepositoryHealth(basePath, prefs, issues) {
|
|
323
|
-
if (prefs?.workspace?.mode !== "parent")
|
|
324
|
-
return;
|
|
325
|
-
let registry;
|
|
326
|
-
try {
|
|
327
|
-
registry = createRepositoryRegistryFromPreferences(basePath, prefs);
|
|
328
|
-
}
|
|
329
|
-
catch (err) {
|
|
330
|
-
issues.push({
|
|
331
|
-
severity: "error",
|
|
332
|
-
code: "invalid_preferences",
|
|
333
|
-
scope: "project",
|
|
334
|
-
unitId: "workspace",
|
|
335
|
-
message: `workspace registry failed to build: ${err instanceof Error ? err.message : String(err)}`,
|
|
336
|
-
fixable: false,
|
|
337
|
-
});
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
for (const repo of registry.repositories) {
|
|
341
|
-
if (repo.id === "project")
|
|
342
|
-
continue;
|
|
343
|
-
if (!existsSync(repo.root)) {
|
|
344
|
-
issues.push({
|
|
345
|
-
severity: "error",
|
|
346
|
-
code: "workspace_repo_path_missing",
|
|
347
|
-
scope: "project",
|
|
348
|
-
unitId: `workspace.repositories.${repo.id}`,
|
|
349
|
-
message: `declared repository "${repo.id}" path does not exist on disk: ${repo.root}`,
|
|
350
|
-
fixable: false,
|
|
351
|
-
});
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
// Must be a repo at its OWN root — a plain dir nested in the parent repo
|
|
355
|
-
// would otherwise pass a bare `nativeIsRepo` check via the enclosing
|
|
356
|
-
// parent's .git. Compare on realpath so symlinked temp roots (macOS
|
|
357
|
-
// /var vs /private/var) don't cause a false mismatch.
|
|
358
|
-
const toplevel = resolveGitToplevel(repo.root);
|
|
359
|
-
const declaredReal = realpathSafe(repo.root);
|
|
360
|
-
const toplevelReal = toplevel ? realpathSafe(toplevel) : null;
|
|
361
|
-
if (toplevelReal !== declaredReal) {
|
|
362
|
-
issues.push({
|
|
363
|
-
severity: "warning",
|
|
364
|
-
code: "workspace_repo_not_a_repo",
|
|
365
|
-
scope: "project",
|
|
366
|
-
unitId: `workspace.repositories.${repo.id}`,
|
|
367
|
-
message: `declared repository "${repo.id}" path is not a git repository at its own root: ${repo.root}`,
|
|
368
|
-
fixable: false,
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
147
|
export async function runGSDDoctor(basePath, options) {
|
|
374
148
|
const issues = [];
|
|
375
149
|
const fixesApplied = [];
|
|
@@ -450,424 +224,11 @@ export async function runGSDDoctor(basePath, options) {
|
|
|
450
224
|
await appendDoctorHistory(basePath, report);
|
|
451
225
|
return report;
|
|
452
226
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
// Skipped for idle projects (no active milestone) to avoid noise in environments
|
|
459
|
-
// where CI/test runners have no API key configured.
|
|
460
|
-
if (state.activeMilestone) {
|
|
461
|
-
try {
|
|
462
|
-
const providerResults = runProviderChecks();
|
|
463
|
-
for (const result of providerResults) {
|
|
464
|
-
if (!result.required)
|
|
465
|
-
continue;
|
|
466
|
-
if (result.status === "error") {
|
|
467
|
-
issues.push({
|
|
468
|
-
severity: "warning",
|
|
469
|
-
code: "provider_key_missing",
|
|
470
|
-
scope: "project",
|
|
471
|
-
unitId: "project",
|
|
472
|
-
message: result.message + (result.detail ? ` — ${result.detail}` : ""),
|
|
473
|
-
fixable: false,
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
else if (result.status === "warning") {
|
|
477
|
-
issues.push({
|
|
478
|
-
severity: "warning",
|
|
479
|
-
code: "provider_key_backedoff",
|
|
480
|
-
scope: "project",
|
|
481
|
-
unitId: "project",
|
|
482
|
-
message: result.message + (result.detail ? ` — ${result.detail}` : ""),
|
|
483
|
-
fixable: false,
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
catch {
|
|
489
|
-
// Non-fatal — provider check failure should not block other checks
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
// When DB is unavailable, state.registry is empty. Fall back to a direct
|
|
493
|
-
// filesystem scan so the doctor can still report issues (e.g. missing ROADMAP)
|
|
494
|
-
// for milestone dirs that exist on disk.
|
|
495
|
-
const milestoneEntries = state.registry.length > 0
|
|
496
|
-
? state.registry
|
|
497
|
-
: findMilestoneIds(basePath).map(id => ({ id, title: id }));
|
|
498
|
-
for (const milestone of milestoneEntries) {
|
|
499
|
-
const milestoneId = milestone.id;
|
|
500
|
-
const milestonePath = resolveMilestonePath(basePath, milestoneId);
|
|
501
|
-
if (!milestonePath)
|
|
502
|
-
continue;
|
|
503
|
-
// Validate milestone title for delimiter characters that break state documents.
|
|
504
|
-
const milestoneTitleIssue = validateTitle(milestone.title);
|
|
505
|
-
if (milestoneTitleIssue) {
|
|
506
|
-
const roadmapFile = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
507
|
-
let wasFixed = false;
|
|
508
|
-
if (shouldFix("delimiter_in_title") && roadmapFile) {
|
|
509
|
-
try {
|
|
510
|
-
const raw = readFileSync(roadmapFile, "utf-8");
|
|
511
|
-
// Replace em/en dashes with " - " in the H1 title line only
|
|
512
|
-
const sanitized = raw.replace(/^(# .*)$/m, (line) => line.replace(/[\u2014\u2013]/g, "-"));
|
|
513
|
-
if (sanitized !== raw) {
|
|
514
|
-
await saveFile(roadmapFile, sanitized);
|
|
515
|
-
fixesApplied.push(`sanitized delimiter characters in ${milestoneId} title`);
|
|
516
|
-
wasFixed = true;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
catch { /* non-fatal — report the warning below */ }
|
|
520
|
-
}
|
|
521
|
-
if (!wasFixed) {
|
|
522
|
-
issues.push({
|
|
523
|
-
severity: "warning",
|
|
524
|
-
code: "delimiter_in_title",
|
|
525
|
-
scope: "milestone",
|
|
526
|
-
unitId: milestoneId,
|
|
527
|
-
message: `Milestone ${milestoneId} ${milestoneTitleIssue}. Rename the milestone to remove these characters to prevent state corruption.`,
|
|
528
|
-
file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
|
|
529
|
-
fixable: true,
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
534
|
-
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
|
|
535
|
-
if (!roadmapContent) {
|
|
536
|
-
issues.push({
|
|
537
|
-
severity: "error",
|
|
538
|
-
code: "missing_roadmap",
|
|
539
|
-
scope: "milestone",
|
|
540
|
-
unitId: milestoneId,
|
|
541
|
-
message: `Milestone ${milestoneId} is missing its ROADMAP.md file.`,
|
|
542
|
-
fixable: false,
|
|
543
|
-
});
|
|
544
|
-
continue;
|
|
545
|
-
}
|
|
546
|
-
let slices;
|
|
547
|
-
if (isDbAvailable()) {
|
|
548
|
-
const dbSlices = getMilestoneSlices(milestoneId);
|
|
549
|
-
slices = dbSlices.map(s => ({
|
|
550
|
-
id: s.id,
|
|
551
|
-
title: s.title,
|
|
552
|
-
done: isClosedStatus(s.status),
|
|
553
|
-
pending: s.status === "pending",
|
|
554
|
-
skipped: s.status === "skipped",
|
|
555
|
-
risk: (s.risk || "medium"),
|
|
556
|
-
depends: s.depends,
|
|
557
|
-
demo: s.demo,
|
|
558
|
-
}));
|
|
559
|
-
}
|
|
560
|
-
else {
|
|
561
|
-
const activeMilestoneId = state.activeMilestone?.id;
|
|
562
|
-
const activeSliceId = state.activeSlice?.id;
|
|
563
|
-
slices = parseLegacyRoadmap(roadmapContent).slices.map(s => ({
|
|
564
|
-
...s,
|
|
565
|
-
// Legacy roadmaps only encode done vs not-done. For doctor's
|
|
566
|
-
// missing-directory checks, treat every undone slice except the
|
|
567
|
-
// current active slice as effectively pending/unstarted.
|
|
568
|
-
pending: !s.done && (milestoneId !== activeMilestoneId || s.id !== activeSliceId),
|
|
569
|
-
}));
|
|
570
|
-
}
|
|
571
|
-
// Wrap in Roadmap-compatible shape for detectCircularDependencies
|
|
572
|
-
const roadmap = { slices };
|
|
573
|
-
// ── Circular dependency detection ──────────────────────────────────────
|
|
574
|
-
for (const cycle of detectCircularDependencies(roadmap.slices)) {
|
|
575
|
-
issues.push({
|
|
576
|
-
severity: "error",
|
|
577
|
-
code: "circular_slice_dependency",
|
|
578
|
-
scope: "milestone",
|
|
579
|
-
unitId: milestoneId,
|
|
580
|
-
message: `Circular dependency detected: ${cycle.join(" → ")}`,
|
|
581
|
-
file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
|
|
582
|
-
fixable: false,
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
// ── Orphaned slice directories ─────────────────────────────────────────
|
|
586
|
-
try {
|
|
587
|
-
const slicesDir = join(milestonePath, "slices");
|
|
588
|
-
if (existsSync(slicesDir)) {
|
|
589
|
-
const knownSliceIds = new Set(roadmap.slices.map(s => s.id));
|
|
590
|
-
for (const entry of readdirSync(slicesDir)) {
|
|
591
|
-
try {
|
|
592
|
-
if (!lstatSync(join(slicesDir, entry)).isDirectory())
|
|
593
|
-
continue;
|
|
594
|
-
}
|
|
595
|
-
catch {
|
|
596
|
-
continue;
|
|
597
|
-
}
|
|
598
|
-
if (entry === "parallel-research")
|
|
599
|
-
continue;
|
|
600
|
-
if (!knownSliceIds.has(entry)) {
|
|
601
|
-
const quarantineExample = `.gsd/quarantine/milestones/${milestoneId}/slices/${entry}-manual-review`;
|
|
602
|
-
issues.push({
|
|
603
|
-
severity: "warning",
|
|
604
|
-
code: "orphaned_slice_directory",
|
|
605
|
-
scope: "milestone",
|
|
606
|
-
unitId: milestoneId,
|
|
607
|
-
message: `Directory "${entry}" exists in ${milestoneId}/slices/ but is not referenced in the roadmap or DB. ` +
|
|
608
|
-
`Review it; if stale, move or delete it. To preserve it, move it under ${quarantineExample}. ` +
|
|
609
|
-
"If it contains work to keep, copy or merge that content into a DB-backed slice before resuming.",
|
|
610
|
-
file: `${relMilestonePath(basePath, milestoneId)}/slices/${entry}`,
|
|
611
|
-
fixable: false,
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
catch { /* non-fatal */ }
|
|
618
|
-
for (const slice of roadmap.slices) {
|
|
619
|
-
const unitId = `${milestoneId}/${slice.id}`;
|
|
620
|
-
if (options?.scope && !matchesScope(unitId, options.scope) && options.scope !== milestoneId)
|
|
621
|
-
continue;
|
|
622
|
-
// Validate slice title for delimiter characters.
|
|
623
|
-
const sliceTitleIssue = validateTitle(slice.title);
|
|
624
|
-
if (sliceTitleIssue) {
|
|
625
|
-
// Slice titles live inside the roadmap H1/checkbox lines — the milestone-level
|
|
626
|
-
// fix above already sanitizes the roadmap file. For slices we only report, because
|
|
627
|
-
// the title comes from the checkbox text and requires careful regex to fix safely.
|
|
628
|
-
issues.push({
|
|
629
|
-
severity: "warning",
|
|
630
|
-
code: "delimiter_in_title",
|
|
631
|
-
scope: "slice",
|
|
632
|
-
unitId,
|
|
633
|
-
message: `Slice ${unitId} ${sliceTitleIssue}. Rename the slice to remove these characters to prevent state corruption.`,
|
|
634
|
-
file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
|
|
635
|
-
fixable: false,
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
// Check for unresolvable dependency IDs
|
|
639
|
-
const knownSliceIds = new Set(roadmap.slices.map(s => s.id));
|
|
640
|
-
for (const dep of slice.depends) {
|
|
641
|
-
if (!knownSliceIds.has(dep)) {
|
|
642
|
-
issues.push({
|
|
643
|
-
severity: "warning",
|
|
644
|
-
code: "unresolvable_dependency",
|
|
645
|
-
scope: "slice",
|
|
646
|
-
unitId,
|
|
647
|
-
message: `Slice ${unitId} depends on "${dep}" which is not a slice ID in this roadmap. This permanently blocks the slice. Use comma-separated IDs: \`depends:[S01,S02]\``,
|
|
648
|
-
file: relMilestoneFile(basePath, milestoneId, "ROADMAP"),
|
|
649
|
-
fixable: false,
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
const slicePath = resolveSlicePath(basePath, milestoneId, slice.id);
|
|
654
|
-
if (!slicePath) {
|
|
655
|
-
// Pending slices haven't been planned yet — directories are created
|
|
656
|
-
// lazily by ensurePreconditions() at dispatch time. Skipped slices are
|
|
657
|
-
// intentionally allowed to remain summary-less and directory-less.
|
|
658
|
-
if (slice.pending || slice.skipped)
|
|
659
|
-
continue;
|
|
660
|
-
const expectedPath = relSlicePath(basePath, milestoneId, slice.id);
|
|
661
|
-
issues.push({
|
|
662
|
-
severity: slice.done ? "warning" : "error",
|
|
663
|
-
code: "missing_slice_dir",
|
|
664
|
-
scope: "slice",
|
|
665
|
-
unitId,
|
|
666
|
-
message: slice.done
|
|
667
|
-
? `Missing slice directory for ${unitId} (slice is complete — cosmetic only)`
|
|
668
|
-
: `Missing slice directory for ${unitId}`,
|
|
669
|
-
file: expectedPath,
|
|
670
|
-
fixable: true,
|
|
671
|
-
});
|
|
672
|
-
if (fix) {
|
|
673
|
-
const absoluteSliceDir = join(milestonePath, "slices", slice.id);
|
|
674
|
-
mkdirSync(absoluteSliceDir, { recursive: true });
|
|
675
|
-
fixesApplied.push(`created ${absoluteSliceDir}`);
|
|
676
|
-
}
|
|
677
|
-
continue;
|
|
678
|
-
}
|
|
679
|
-
const tasksDir = resolveTasksDir(basePath, milestoneId, slice.id);
|
|
680
|
-
if (!tasksDir) {
|
|
681
|
-
// Pending slices haven't been planned yet — tasks/ is created on demand.
|
|
682
|
-
// Skipped slices may legitimately never create tasks/.
|
|
683
|
-
if (slice.pending || slice.skipped)
|
|
684
|
-
continue;
|
|
685
|
-
// Flat-phase: tasks are embedded in plan files; no tasks/ subdir expected.
|
|
686
|
-
if (!existsSync(legacyMilestonesDir(basePath)))
|
|
687
|
-
continue;
|
|
688
|
-
issues.push({
|
|
689
|
-
severity: slice.done ? "warning" : "error",
|
|
690
|
-
code: "missing_tasks_dir",
|
|
691
|
-
scope: "slice",
|
|
692
|
-
unitId,
|
|
693
|
-
message: slice.done
|
|
694
|
-
? `Missing tasks directory for ${unitId} (slice is complete \u2014 cosmetic only)`
|
|
695
|
-
: `Missing tasks directory for ${unitId}`,
|
|
696
|
-
file: relSlicePath(basePath, milestoneId, slice.id),
|
|
697
|
-
fixable: true,
|
|
698
|
-
});
|
|
699
|
-
if (fix) {
|
|
700
|
-
mkdirSync(join(slicePath, "tasks"), { recursive: true });
|
|
701
|
-
fixesApplied.push(`created ${join(slicePath, "tasks")}`);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
const planPath = resolveSliceFile(basePath, milestoneId, slice.id, "PLAN");
|
|
705
|
-
const planContent = planPath ? await loadFile(planPath) : null;
|
|
706
|
-
// Normalize plan tasks: prefer DB, fall back to parsers-legacy
|
|
707
|
-
let plan = null;
|
|
708
|
-
if (isDbAvailable()) {
|
|
709
|
-
const dbTasks = getSliceTasks(milestoneId, slice.id);
|
|
710
|
-
if (dbTasks.length > 0) {
|
|
711
|
-
plan = { tasks: dbTasks.map(t => ({ id: t.id, done: t.status === "complete" || t.status === "done", title: t.title, estimate: t.estimate || undefined })) };
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
if (!plan && planContent) {
|
|
715
|
-
plan = parseLegacyPlan(planContent);
|
|
716
|
-
}
|
|
717
|
-
if (!plan) {
|
|
718
|
-
if (!slice.done) {
|
|
719
|
-
issues.push({
|
|
720
|
-
severity: "warning",
|
|
721
|
-
code: "missing_slice_plan",
|
|
722
|
-
scope: "slice",
|
|
723
|
-
unitId,
|
|
724
|
-
message: `Slice ${unitId} has no plan file`,
|
|
725
|
-
file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"),
|
|
726
|
-
fixable: false,
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
continue;
|
|
730
|
-
}
|
|
731
|
-
// ── Duplicate task IDs ───────────────────────────────────────────────
|
|
732
|
-
const taskIdCounts = new Map();
|
|
733
|
-
for (const task of plan.tasks)
|
|
734
|
-
taskIdCounts.set(task.id, (taskIdCounts.get(task.id) ?? 0) + 1);
|
|
735
|
-
for (const [taskId, count] of taskIdCounts) {
|
|
736
|
-
if (count > 1) {
|
|
737
|
-
issues.push({ severity: "error", code: "duplicate_task_id", scope: "slice", unitId,
|
|
738
|
-
message: `Task ID "${taskId}" appears ${count} times in ${slice.id}-PLAN.md — duplicate IDs cause dispatch failures`,
|
|
739
|
-
file: relSliceFile(basePath, milestoneId, slice.id, "PLAN"), fixable: false });
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
// ── Task files on disk not in plan ────────────────────────────────────
|
|
743
|
-
try {
|
|
744
|
-
if (tasksDir) {
|
|
745
|
-
const planTaskIds = new Set(plan.tasks.map(t => t.id));
|
|
746
|
-
for (const f of readdirSync(tasksDir)) {
|
|
747
|
-
if (!f.endsWith("-SUMMARY.md"))
|
|
748
|
-
continue;
|
|
749
|
-
const diskTaskId = f.replace(/-SUMMARY\.md$/, "");
|
|
750
|
-
if (!planTaskIds.has(diskTaskId)) {
|
|
751
|
-
issues.push({ severity: "info", code: "task_file_not_in_plan", scope: "slice", unitId,
|
|
752
|
-
message: `Task summary "${f}" exists on disk but "${diskTaskId}" is not in ${slice.id}-PLAN.md`,
|
|
753
|
-
file: relTaskFile(basePath, milestoneId, slice.id, diskTaskId, "SUMMARY"), fixable: false });
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
catch { /* non-fatal */ }
|
|
759
|
-
let allTasksDone = plan.tasks.length > 0;
|
|
760
|
-
for (const task of plan.tasks) {
|
|
761
|
-
const taskUnitId = `${unitId}/${task.id}`;
|
|
762
|
-
const summaryPath = resolveTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY");
|
|
763
|
-
const hasSummary = !!(summaryPath && await loadFile(summaryPath));
|
|
764
|
-
// Must-have verification
|
|
765
|
-
if (task.done && hasSummary) {
|
|
766
|
-
const taskPlanPath = resolveTaskFile(basePath, milestoneId, slice.id, task.id, "PLAN");
|
|
767
|
-
if (taskPlanPath) {
|
|
768
|
-
const taskPlanContent = await loadFile(taskPlanPath);
|
|
769
|
-
if (taskPlanContent) {
|
|
770
|
-
const mustHaves = parseTaskPlanMustHaves(taskPlanContent);
|
|
771
|
-
if (mustHaves.length > 0) {
|
|
772
|
-
const summaryContent = await loadFile(summaryPath);
|
|
773
|
-
const mentionedCount = summaryContent
|
|
774
|
-
? countMustHavesMentionedInSummary(mustHaves, summaryContent)
|
|
775
|
-
: 0;
|
|
776
|
-
if (mentionedCount < mustHaves.length) {
|
|
777
|
-
issues.push({
|
|
778
|
-
severity: "warning",
|
|
779
|
-
code: "task_done_must_haves_not_verified",
|
|
780
|
-
scope: "task",
|
|
781
|
-
unitId: taskUnitId,
|
|
782
|
-
message: `Task ${task.id} has ${mustHaves.length} must-haves but summary addresses only ${mentionedCount}`,
|
|
783
|
-
file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"),
|
|
784
|
-
fixable: false,
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
// ── Future timestamp check ─────────────────────────────────────
|
|
792
|
-
if (task.done && hasSummary && summaryPath) {
|
|
793
|
-
try {
|
|
794
|
-
const rawSummary = await loadFile(summaryPath);
|
|
795
|
-
const m = rawSummary?.match(/^completed_at:\s*(.+)$/m);
|
|
796
|
-
if (m) {
|
|
797
|
-
const ts = new Date(m[1].trim());
|
|
798
|
-
if (!isNaN(ts.getTime()) && ts.getTime() > Date.now() + 24 * 60 * 60 * 1000) {
|
|
799
|
-
issues.push({ severity: "warning", code: "future_timestamp", scope: "task", unitId: taskUnitId,
|
|
800
|
-
message: `Task ${task.id} has completed_at "${m[1].trim()}" which is more than 24h in the future`,
|
|
801
|
-
file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"), fixable: false });
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
catch { /* non-fatal */ }
|
|
806
|
-
}
|
|
807
|
-
allTasksDone = allTasksDone && task.done;
|
|
808
|
-
}
|
|
809
|
-
// Blocker-without-replan detection
|
|
810
|
-
// Skip when all tasks are done — the blocker was implicitly resolved
|
|
811
|
-
// within the task and the slice is not stuck (#3105 Bug 2).
|
|
812
|
-
const replanPath = resolveSliceFile(basePath, milestoneId, slice.id, "REPLAN");
|
|
813
|
-
if (!replanPath && !allTasksDone) {
|
|
814
|
-
for (const task of plan.tasks) {
|
|
815
|
-
if (!task.done)
|
|
816
|
-
continue;
|
|
817
|
-
const summaryPath = resolveTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY");
|
|
818
|
-
if (!summaryPath)
|
|
819
|
-
continue;
|
|
820
|
-
const summaryContent = await loadFile(summaryPath);
|
|
821
|
-
if (!summaryContent)
|
|
822
|
-
continue;
|
|
823
|
-
const summary = parseSummary(summaryContent);
|
|
824
|
-
if (summary.frontmatter.blocker_discovered) {
|
|
825
|
-
issues.push({
|
|
826
|
-
severity: "warning",
|
|
827
|
-
code: "blocker_discovered_no_replan",
|
|
828
|
-
scope: "slice",
|
|
829
|
-
unitId,
|
|
830
|
-
message: `Task ${task.id} reported blocker_discovered but no REPLAN.md exists for ${slice.id} \u2014 slice may be stuck`,
|
|
831
|
-
file: relSliceFile(basePath, milestoneId, slice.id, "REPLAN"),
|
|
832
|
-
fixable: false,
|
|
833
|
-
});
|
|
834
|
-
break;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
// ── Stale REPLAN: exists but all tasks done ────────────────────────
|
|
839
|
-
if (replanPath && allTasksDone) {
|
|
840
|
-
issues.push({ severity: "info", code: "stale_replan_file", scope: "slice", unitId,
|
|
841
|
-
message: `${slice.id} has a REPLAN.md but all tasks are done — REPLAN.md may be stale`,
|
|
842
|
-
file: relSliceFile(basePath, milestoneId, slice.id, "REPLAN"), fixable: false });
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
// Milestone-level check: all slices done but no validation file
|
|
846
|
-
const milestoneComplete = roadmap.slices.length > 0 && roadmap.slices.every(s => s.done);
|
|
847
|
-
if (milestoneComplete && !resolveMilestoneFile(basePath, milestoneId, "VALIDATION") && !resolveMilestoneFile(basePath, milestoneId, "SUMMARY")) {
|
|
848
|
-
issues.push({
|
|
849
|
-
severity: "info",
|
|
850
|
-
code: "all_slices_done_missing_milestone_validation",
|
|
851
|
-
scope: "milestone",
|
|
852
|
-
unitId: milestoneId,
|
|
853
|
-
message: `All slices are done but the milestone VALIDATION.md is missing \u2014 milestone is in validating-milestone phase`,
|
|
854
|
-
file: relMilestoneFile(basePath, milestoneId, "VALIDATION"),
|
|
855
|
-
fixable: false,
|
|
856
|
-
});
|
|
857
|
-
}
|
|
858
|
-
// Milestone-level check: all slices done but no milestone summary
|
|
859
|
-
if (milestoneComplete && !resolveMilestoneFile(basePath, milestoneId, "SUMMARY")) {
|
|
860
|
-
issues.push({
|
|
861
|
-
severity: "warning",
|
|
862
|
-
code: "all_slices_done_missing_milestone_summary",
|
|
863
|
-
scope: "milestone",
|
|
864
|
-
unitId: milestoneId,
|
|
865
|
-
message: `All slices are done but ${milestoneId}-SUMMARY.md is missing \u2014 milestone is stuck in completing-milestone phase`,
|
|
866
|
-
file: relMilestoneFile(basePath, milestoneId, "SUMMARY"),
|
|
867
|
-
fixable: false,
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
}
|
|
227
|
+
await checkGsdStateHealth(basePath, issues, fixesApplied, {
|
|
228
|
+
fix,
|
|
229
|
+
shouldFix,
|
|
230
|
+
scope: options?.scope,
|
|
231
|
+
});
|
|
871
232
|
if (fix && !dryRun && fixesApplied.length > 0) {
|
|
872
233
|
await updateStateFile(basePath, fixesApplied);
|
|
873
234
|
}
|