@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
|
@@ -810,8 +810,13 @@ export function migrateHierarchyToDb(basePath: string): {
|
|
|
810
810
|
}
|
|
811
811
|
counts.slices++;
|
|
812
812
|
|
|
813
|
-
// Insert tasks from parsed plan
|
|
814
|
-
|
|
813
|
+
// Insert tasks from parsed plan.
|
|
814
|
+
// Sketch slices carry only a stub PLAN until refined; do not seed DB
|
|
815
|
+
// tasks from a placeholder <tasks> block (would falsely flip
|
|
816
|
+
// planning -> executing before the slice is decomposed). Once the
|
|
817
|
+
// `[sketch]` badge is removed by a real plan-slice, tasks import
|
|
818
|
+
// normally on the next pass. (#1286)
|
|
819
|
+
if (!plan || sliceEntry.isSketch) continue;
|
|
815
820
|
|
|
816
821
|
for (const taskEntry of plan.tasks) {
|
|
817
822
|
// Per K002: use 'complete' not 'done'
|
|
@@ -11,6 +11,7 @@ import { join, relative } from "node:path";
|
|
|
11
11
|
|
|
12
12
|
import { getAllMilestones, getMilestoneSlices, getSliceTasks } from "../gsd-db.js";
|
|
13
13
|
import { saveFile } from "../files.js";
|
|
14
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
14
15
|
import type { PlanningLayout } from "../compat/compat-marker.js";
|
|
15
16
|
import {
|
|
16
17
|
applyPlanningProjectionWrites,
|
|
@@ -104,7 +105,7 @@ function formatPlanningPlan(
|
|
|
104
105
|
phaseNum: number,
|
|
105
106
|
planNum: number,
|
|
106
107
|
title: string,
|
|
107
|
-
tasks: Array<{ id: string; title: string; estimate?: string }>,
|
|
108
|
+
tasks: Array<{ id: string; title: string; estimate?: string; done?: boolean }>,
|
|
108
109
|
): string {
|
|
109
110
|
const lines: string[] = [];
|
|
110
111
|
lines.push(`# ${pad(phaseNum)}-${pad(planNum)}: ${title}`, "");
|
|
@@ -115,7 +116,11 @@ function formatPlanningPlan(
|
|
|
115
116
|
lines.push("<tasks>");
|
|
116
117
|
for (const t of tasks) {
|
|
117
118
|
const est = t.estimate ? ` _(${t.estimate})_` : "";
|
|
118
|
-
|
|
119
|
+
// Render the checkbox from the task's DB status so a DB→.planning
|
|
120
|
+
// projection preserves completion. Hardcoding `[ ]` here silently reset
|
|
121
|
+
// every completed historical task to unchecked on each reconcile (#1276).
|
|
122
|
+
const box = t.done ? "[x]" : "[ ]";
|
|
123
|
+
lines.push(`- ${box} **${t.id}**: ${t.title}${est}`);
|
|
119
124
|
}
|
|
120
125
|
lines.push("</tasks>");
|
|
121
126
|
lines.push("");
|
|
@@ -173,7 +178,7 @@ export async function writePlanningDirectory(
|
|
|
173
178
|
|
|
174
179
|
const tasks = getSliceTasks(milestone.id, slice.id);
|
|
175
180
|
const isDone =
|
|
176
|
-
tasks.length > 0 && tasks.every((t) => t.status
|
|
181
|
+
tasks.length > 0 && tasks.every((t) => isClosedStatus(t.status));
|
|
177
182
|
roadmapEntries.push({
|
|
178
183
|
number: phaseNum,
|
|
179
184
|
title: slice.title || slice.id,
|
|
@@ -204,6 +209,7 @@ export async function writePlanningDirectory(
|
|
|
204
209
|
id: task.id,
|
|
205
210
|
title: task.title || task.id,
|
|
206
211
|
estimate: task.estimate || undefined,
|
|
212
|
+
done: isClosedStatus(task.status),
|
|
207
213
|
},
|
|
208
214
|
]),
|
|
209
215
|
);
|
|
@@ -169,6 +169,9 @@ export function scanMarkdownHierarchy(basePath: string): HierarchyScan {
|
|
|
169
169
|
|
|
170
170
|
for (const slice of roadmap.slices) {
|
|
171
171
|
scan.slices.add(`${milestoneId}/${slice.id}`);
|
|
172
|
+
// Sketch slices carry only a stub PLAN until refined; match migrateHierarchyToDb
|
|
173
|
+
// and do not count placeholder tasks from the <tasks> block (#1286).
|
|
174
|
+
if (slice.isSketch) continue;
|
|
172
175
|
const planPath = resolveSliceFile(basePath, milestoneId, slice.id, "PLAN");
|
|
173
176
|
if (!planPath || !existsSync(planPath)) continue;
|
|
174
177
|
const plan = parsePlan(readFileSync(planPath, "utf-8"));
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Project/App: gsd-pi
|
|
2
|
+
// File Purpose: Persist a reopened task's failure diagnosis and inject it into the re-dispatched execute-task prompt.
|
|
3
|
+
// GSD Extension — follow-up to #1225 (#1272)
|
|
4
|
+
//
|
|
5
|
+
// When complete-slice's gate reopens a task via gsd_task_reopen, the operator
|
|
6
|
+
// supplies a `reason` explaining exactly what the full-suite gate caught and
|
|
7
|
+
// how to fix it. That reason previously only landed in the workflow event log
|
|
8
|
+
// (audit trail) and never reached the re-dispatched executor, so the executor
|
|
9
|
+
// re-ran the original (green) verify and re-completed the task without touching
|
|
10
|
+
// the regression. This module is the "inject unresolved context into the next
|
|
11
|
+
// dispatch" mechanism for reopen reasons — a lightweight, file-based sibling of
|
|
12
|
+
// escalation.js#claimOverrideForInjection, keyed by (milestone, slice, task).
|
|
13
|
+
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
import { legacyMilestonesDir, resolveSlicePath, resolveTasksDir } from "./paths.js";
|
|
18
|
+
import { atomicWriteSync } from "./atomic-write.js";
|
|
19
|
+
import { logWarning } from "./workflow-logger.js";
|
|
20
|
+
|
|
21
|
+
interface ReopenReasonArtifact {
|
|
22
|
+
version: 1;
|
|
23
|
+
milestoneId: string;
|
|
24
|
+
sliceId: string;
|
|
25
|
+
taskId: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Canonical reopen-reason artifact path, parallel to T##-SUMMARY.md and
|
|
32
|
+
* T##-ESCALATION.json:
|
|
33
|
+
* .gsd/milestones/{M}/slices/{S}/tasks/{T}-REOPEN.json
|
|
34
|
+
* Flat-phase: the artifact sits directly in the phase dir. Legacy layouts
|
|
35
|
+
* without a tasks/ subdir return null (caller degrades gracefully).
|
|
36
|
+
*/
|
|
37
|
+
export function reopenReasonArtifactPath(
|
|
38
|
+
basePath: string, milestoneId: string, sliceId: string, taskId: string,
|
|
39
|
+
): string | null {
|
|
40
|
+
const tDir = resolveTasksDir(basePath, milestoneId, sliceId);
|
|
41
|
+
if (tDir) return join(tDir, `${taskId}-REOPEN.json`);
|
|
42
|
+
if (existsSync(legacyMilestonesDir(basePath))) return null;
|
|
43
|
+
const phaseDir = resolveSlicePath(basePath, milestoneId, sliceId);
|
|
44
|
+
if (!phaseDir) return null;
|
|
45
|
+
return join(phaseDir, `${taskId}-REOPEN.json`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Persist the reopen reason so the next execute-task dispatch can surface it.
|
|
50
|
+
* A no-op when the reason is empty or the artifact path cannot be resolved —
|
|
51
|
+
* reopen itself must still succeed even if we cannot attach context.
|
|
52
|
+
*/
|
|
53
|
+
export function writeReopenReason(
|
|
54
|
+
basePath: string, milestoneId: string, sliceId: string, taskId: string, reason: string,
|
|
55
|
+
): void {
|
|
56
|
+
const trimmed = reason.trim();
|
|
57
|
+
if (!trimmed) return;
|
|
58
|
+
const path = reopenReasonArtifactPath(basePath, milestoneId, sliceId, taskId);
|
|
59
|
+
if (!path) {
|
|
60
|
+
logWarning("tool", `reopen-reason: cannot resolve artifact path for ${milestoneId}/${sliceId}/${taskId}; reopen reason not attached to next dispatch`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const artifact: ReopenReasonArtifact = {
|
|
64
|
+
version: 1,
|
|
65
|
+
milestoneId, sliceId, taskId,
|
|
66
|
+
reason: trimmed,
|
|
67
|
+
createdAt: new Date().toISOString(),
|
|
68
|
+
};
|
|
69
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
70
|
+
atomicWriteSync(path, JSON.stringify(artifact, null, 2));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* If a reopen reason is pending for this task, claim it (one-shot: the artifact
|
|
75
|
+
* is deleted on read) and return the markdown block to prepend to the executor's
|
|
76
|
+
* prompt. Returns null when nothing is pending or the artifact is malformed.
|
|
77
|
+
*/
|
|
78
|
+
export function claimReopenReasonForInjection(
|
|
79
|
+
basePath: string, milestoneId: string, sliceId: string, taskId: string,
|
|
80
|
+
): { injectionBlock: string } | null {
|
|
81
|
+
const path = reopenReasonArtifactPath(basePath, milestoneId, sliceId, taskId);
|
|
82
|
+
if (!path || !existsSync(path)) return null;
|
|
83
|
+
let art: ReopenReasonArtifact | null = null;
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8")) as Partial<ReopenReasonArtifact>;
|
|
86
|
+
if (parsed && parsed.version === 1 && typeof parsed.reason === "string" && parsed.reason.trim()) {
|
|
87
|
+
art = parsed as ReopenReasonArtifact;
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
art = null;
|
|
91
|
+
}
|
|
92
|
+
// Claim (delete) regardless of validity so a malformed artifact doesn't
|
|
93
|
+
// wedge every future dispatch of this task.
|
|
94
|
+
try { unlinkSync(path); } catch { /* already gone */ }
|
|
95
|
+
if (!art) return null;
|
|
96
|
+
return { injectionBlock: formatReopenReasonBlock(art) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatReopenReasonBlock(art: ReopenReasonArtifact): string {
|
|
100
|
+
return [
|
|
101
|
+
`## Reopened — Reason (from ${art.taskId})`,
|
|
102
|
+
"",
|
|
103
|
+
"This task was previously marked complete but a downstream gate (e.g. complete-slice's full-suite run) reopened it. The verify command in your task plan passed in isolation — it did **not** catch the regression below. Do **not** call `gsd_task_complete` until you have reproduced and fixed exactly what this diagnosis describes, then re-run the specific failing command it names (not just the original scoped verify).",
|
|
104
|
+
"",
|
|
105
|
+
"**Diagnosis / fix instructions from the gate:**",
|
|
106
|
+
"",
|
|
107
|
+
art.reason.trim(),
|
|
108
|
+
].join("\n");
|
|
109
|
+
}
|
|
@@ -48,6 +48,11 @@ const PARALLEL_DIR = "parallel";
|
|
|
48
48
|
const STATUS_SUFFIX = ".status.json";
|
|
49
49
|
const SIGNAL_SUFFIX = ".signal.json";
|
|
50
50
|
const DEFAULT_STALE_TIMEOUT_MS = 30_000;
|
|
51
|
+
// How long a paused worker waits for the coordinator to lift the pause before
|
|
52
|
+
// it degrades to in-process serialization (#1273). Kept below the stale
|
|
53
|
+
// timeout so the wait never races the coordinator's liveness detection.
|
|
54
|
+
const DEFAULT_RESUME_WAIT_MS = 10_000;
|
|
55
|
+
const DEFAULT_RESUME_POLL_MS = 250;
|
|
51
56
|
|
|
52
57
|
function isSessionStatus(data: unknown): data is SessionStatus {
|
|
53
58
|
return data !== null && typeof data === "object" && "milestoneId" in data && "pid" in data;
|
|
@@ -143,6 +148,35 @@ export function consumeSignal(basePath: string, milestoneId: string): SignalMess
|
|
|
143
148
|
return msg;
|
|
144
149
|
}
|
|
145
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Wait for a coordinator to lift a `pause` on a worker by sending `resume`
|
|
153
|
+
* (or `stop`). Polls the signal file until one of those arrives or the timeout
|
|
154
|
+
* elapses. Intervening `pause`/`rebase` signals are consumed and ignored so a
|
|
155
|
+
* repeated pause doesn't reset the wait.
|
|
156
|
+
*
|
|
157
|
+
* A worker's `pause` is only ever lifted by the interactive/dashboard resumer;
|
|
158
|
+
* unattended `gsd headless auto` owns no resumer, so callers use the `"timeout"`
|
|
159
|
+
* result to degrade to in-process serialization instead of stranding the worker
|
|
160
|
+
* at a terminal pause it cannot resume (#1273).
|
|
161
|
+
*/
|
|
162
|
+
export async function awaitWorkerResume(
|
|
163
|
+
basePath: string,
|
|
164
|
+
milestoneId: string,
|
|
165
|
+
opts: { timeoutMs?: number; pollMs?: number } = {},
|
|
166
|
+
): Promise<"resume" | "stop" | "timeout"> {
|
|
167
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_RESUME_WAIT_MS;
|
|
168
|
+
const pollMs = opts.pollMs ?? DEFAULT_RESUME_POLL_MS;
|
|
169
|
+
const deadline = Date.now() + timeoutMs;
|
|
170
|
+
|
|
171
|
+
for (;;) {
|
|
172
|
+
const msg = consumeSignal(basePath, milestoneId);
|
|
173
|
+
if (msg?.signal === "resume") return "resume";
|
|
174
|
+
if (msg?.signal === "stop") return "stop";
|
|
175
|
+
if (Date.now() >= deadline) return "timeout";
|
|
176
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
146
180
|
// ─── Stale Detection ───────────────────────────────────────────────────────
|
|
147
181
|
|
|
148
182
|
/** Check whether a session is stale (PID dead or heartbeat timed out). */
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
|
|
10
|
-
import { getMilestone, isDbAvailable } from "../../gsd-db.js";
|
|
11
|
-
import { findMilestoneIds } from "../../milestone-ids.js";
|
|
10
|
+
import { getAllMilestones, getMilestone, isDbAvailable } from "../../gsd-db.js";
|
|
11
|
+
import { findMilestoneIds, parseMilestoneId } from "../../milestone-ids.js";
|
|
12
12
|
import { resolveMilestoneFile, resolveMilestonePath } from "../../paths.js";
|
|
13
13
|
import type { GSDState } from "../../types.js";
|
|
14
14
|
import type { DriftContext, DriftHandler, DriftRecord } from "../types.js";
|
|
@@ -29,6 +29,26 @@ function milestoneHasContent(basePath: string, milestoneId: string): boolean {
|
|
|
29
29
|
);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* A directory extracted to a bare `M{NNN}` id is not unregistered when the
|
|
34
|
+
* milestone is actually registered under a `unique_milestone_ids` suffixed id
|
|
35
|
+
* (`M{NNN}-<suffix>`) for the same sequence number. The flat-phase extractor in
|
|
36
|
+
* `milestone-ids` cannot recover the suffix from a descriptive slug (e.g.
|
|
37
|
+
* `07-v40fmq-m007-...-footer-system`), so it falls through to the bare `M007`.
|
|
38
|
+
* Probing the DB for a suffixed row on that sequence resolves the directory to
|
|
39
|
+
* its registered identity instead of firing a false-positive drift (issue
|
|
40
|
+
* #1281).
|
|
41
|
+
*/
|
|
42
|
+
function hasRegisteredSuffixedVariant(milestoneId: string): boolean {
|
|
43
|
+
const { suffix, num } = parseMilestoneId(milestoneId);
|
|
44
|
+
// Only bare, well-formed `M{NNN}` ids can be ambiguous with a suffixed row.
|
|
45
|
+
if (suffix || num === 0) return false;
|
|
46
|
+
return getAllMilestones().some((m) => {
|
|
47
|
+
const parsed = parseMilestoneId(m.id);
|
|
48
|
+
return parsed.num === num && parsed.suffix !== undefined;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
32
52
|
export function detectUnregisteredMilestoneDrift(
|
|
33
53
|
_state: GSDState,
|
|
34
54
|
ctx: DriftContext,
|
|
@@ -38,6 +58,7 @@ export function detectUnregisteredMilestoneDrift(
|
|
|
38
58
|
const drifts: UnregisteredMilestoneDrift[] = [];
|
|
39
59
|
for (const milestoneId of findMilestoneIds(ctx.basePath)) {
|
|
40
60
|
if (getMilestone(milestoneId)) continue;
|
|
61
|
+
if (hasRegisteredSuffixedVariant(milestoneId)) continue;
|
|
41
62
|
if (!milestoneHasContent(ctx.basePath, milestoneId)) continue;
|
|
42
63
|
drifts.push({ kind: "unregistered-milestone", milestoneId });
|
|
43
64
|
}
|
|
@@ -45,35 +66,61 @@ export function detectUnregisteredMilestoneDrift(
|
|
|
45
66
|
}
|
|
46
67
|
|
|
47
68
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* directory — not a full DB reimport. `/gsd recover --confirm` is a destructive
|
|
56
|
-
* clear-and-reimport of the entire DB and is offered only as a last resort, so
|
|
57
|
-
* users do not reach for it expecting a targeted repair (see issue #826).
|
|
69
|
+
* The recovery hint deliberately leads with the *targeted*, non-destructive
|
|
70
|
+
* options. The common cause of this drift is a directory left under an old ID
|
|
71
|
+
* after a `unique_milestone_ids` rename, where the right fix is to rename (move)
|
|
72
|
+
* the directory — not a full DB reimport. `/gsd recover --confirm` is a
|
|
73
|
+
* destructive clear-and-reimport of the entire DB and is offered only as a last
|
|
74
|
+
* resort, so users do not reach for it expecting a targeted repair (see issue
|
|
75
|
+
* #826).
|
|
58
76
|
*/
|
|
59
|
-
|
|
77
|
+
function unregisteredMilestoneGuidance(
|
|
60
78
|
record: UnregisteredMilestoneDrift,
|
|
61
79
|
ctx: DriftContext,
|
|
62
|
-
):
|
|
80
|
+
): string {
|
|
63
81
|
const dir = resolveMilestonePath(ctx.basePath, record.milestoneId);
|
|
64
82
|
const dirHint = dir ?? `the .gsd directory for ${record.milestoneId}`;
|
|
65
|
-
|
|
83
|
+
return (
|
|
66
84
|
`Milestone ${record.milestoneId} exists only as markdown projection ` +
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
"(on-disk ROADMAP/CONTEXT/SUMMARY with no authoritative DB row). " +
|
|
86
|
+
"Runtime reconciliation will not import markdown into the DB. Choose one:\n" +
|
|
87
|
+
` • Rename: if this directory is the same milestone under an old ID (e.g. a unique_milestone_ids rename), move \`${dirHint}\` to the current ID's directory and re-run.\n` +
|
|
88
|
+
` • Discard: if this milestone is no longer relevant, delete \`${dirHint}\` and re-run.\n` +
|
|
89
|
+
" • Last resort: `/gsd recover --confirm` performs a destructive full DB clear-and-reimport — it does NOT do a targeted import and can replace or duplicate existing DB milestones."
|
|
72
90
|
);
|
|
73
91
|
}
|
|
74
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Terminal blocker for an unregistered milestone. The project-root DB is
|
|
95
|
+
* authoritative at runtime; markdown-only milestones must be reconciled through
|
|
96
|
+
* an explicit, operator-controlled action so operators opt into changing
|
|
97
|
+
* canonical state. Exposing this as a `blocker` (matching the `artifact-db`
|
|
98
|
+
* convention) surfaces the drift as a pause-with-hint that gates dispatch,
|
|
99
|
+
* instead of a guaranteed repair throw that escalates auto-mode health to red
|
|
100
|
+
* (see issue #1281).
|
|
101
|
+
*/
|
|
102
|
+
export function describeUnregisteredMilestoneBlocker(
|
|
103
|
+
record: UnregisteredMilestoneDrift,
|
|
104
|
+
ctx: DriftContext,
|
|
105
|
+
): string {
|
|
106
|
+
return unregisteredMilestoneGuidance(record, ctx);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Repair intentionally fails closed. Retained as a defensive fallback for the
|
|
111
|
+
* reconciliation engine; in practice `blocker` short-circuits this drift into a
|
|
112
|
+
* non-fatal pause before repair is ever attempted.
|
|
113
|
+
*/
|
|
114
|
+
export function repairUnregisteredMilestone(
|
|
115
|
+
record: UnregisteredMilestoneDrift,
|
|
116
|
+
ctx: DriftContext,
|
|
117
|
+
): void {
|
|
118
|
+
throw new Error(unregisteredMilestoneGuidance(record, ctx));
|
|
119
|
+
}
|
|
120
|
+
|
|
75
121
|
export const unregisteredMilestoneHandler: DriftHandler<UnregisteredMilestoneDrift> = {
|
|
76
122
|
kind: "unregistered-milestone",
|
|
77
123
|
detect: detectUnregisteredMilestoneDrift,
|
|
124
|
+
blocker: describeUnregisteredMilestoneBlocker,
|
|
78
125
|
repair: repairUnregisteredMilestone,
|
|
79
126
|
};
|
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
// sketch slices to plan-slice, which writes PLAN.md but leaves
|
|
9
9
|
// is_sketch=1 — the next reconciliation pass clears it.
|
|
10
10
|
|
|
11
|
-
import { existsSync } from "node:fs";
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
14
|
getSketchedSliceIds,
|
|
15
15
|
isDbAvailable,
|
|
16
16
|
setSliceSketchFlag,
|
|
17
17
|
} from "../../gsd-db.js";
|
|
18
|
+
import { parsePlan } from "../../parsers-legacy.js";
|
|
18
19
|
import { resolveSliceFile } from "../../paths.js";
|
|
19
20
|
import type { GSDState } from "../../types.js";
|
|
20
21
|
import type { DriftContext, DriftHandler, DriftRecord } from "../types.js";
|
|
@@ -31,13 +32,54 @@ export function detectStaleSketchFlags(
|
|
|
31
32
|
|
|
32
33
|
const sliceIds = getSketchedSliceIds(mid);
|
|
33
34
|
return sliceIds
|
|
34
|
-
.filter((sid) =>
|
|
35
|
-
const planPath = resolveSliceFile(ctx.basePath, mid, sid, "PLAN");
|
|
36
|
-
return planPath !== null && existsSync(planPath);
|
|
37
|
-
})
|
|
35
|
+
.filter((sid) => sketchIsPlanned(ctx.basePath, mid, sid))
|
|
38
36
|
.map((sid) => ({ kind: "stale-sketch-flag" as const, mid, sid }));
|
|
39
37
|
}
|
|
40
38
|
|
|
39
|
+
/**
|
|
40
|
+
* True when a task title is a synthetic placeholder emitted by a projection
|
|
41
|
+
* round-trip rather than a real, refined task. `migrate/transformer.buildTaskTitle`
|
|
42
|
+
* produces two shapes when a plan was never decomposed:
|
|
43
|
+
* - `Plan NN` (fallback when phase/plan frontmatter is absent)
|
|
44
|
+
* - `${phase} ${plan}` (e.g. `00 01`, `00 03b`, `29-auth-system 01`)
|
|
45
|
+
* Neither carries genuine planning intent, so both must be treated as stubs.
|
|
46
|
+
*/
|
|
47
|
+
function isPlaceholderTaskTitle(title: string): boolean {
|
|
48
|
+
const t = title.trim();
|
|
49
|
+
if (/^Plan\s+\d+[a-z]*$/i.test(t)) return true;
|
|
50
|
+
// buildTaskTitle returns `${phase} ${plan}` when frontmatter has both fields.
|
|
51
|
+
// Phase slugs are digit-led (e.g. `00`, `29-auth-system`); require that so
|
|
52
|
+
// legitimate titles like `Step 1` are not misclassified as stubs.
|
|
53
|
+
return /^\d[\w-]*\s+\d+[a-z]*$/i.test(t);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A sketch slice counts as "planned" (so its is_sketch flag may be cleared)
|
|
58
|
+
* only when it has a *real* plan on disk — not merely any PLAN file.
|
|
59
|
+
*
|
|
60
|
+
* File existence alone (#1287) is too weak: a stub/placeholder PLAN, a
|
|
61
|
+
* crash-leftover, or a projection round-trip that emits synthetic placeholder
|
|
62
|
+
* task titles (migrate/transformer.buildTaskTitle) all satisfy existsSync yet
|
|
63
|
+
* were never actually refined. Clearing the flag for one strips the `refining`
|
|
64
|
+
* guard in phase derivation and lets a phantom task drive dispatch.
|
|
65
|
+
*
|
|
66
|
+
* A legitimate plan-slice always decomposes into >= 1 genuine task, so require
|
|
67
|
+
* at least one non-placeholder task. This is strictly stricter than the old
|
|
68
|
+
* existence check — the documented crash-recovery scenarios (a real plan-slice
|
|
69
|
+
* that wrote its tasks) still clear, a bare stub no longer does.
|
|
70
|
+
*/
|
|
71
|
+
function sketchIsPlanned(basePath: string, mid: string, sid: string): boolean {
|
|
72
|
+
const planPath = resolveSliceFile(basePath, mid, sid, "PLAN");
|
|
73
|
+
if (planPath === null || !existsSync(planPath)) return false;
|
|
74
|
+
try {
|
|
75
|
+
const plan = parsePlan(readFileSync(planPath, "utf-8"));
|
|
76
|
+
return plan.tasks.some((t) => !isPlaceholderTaskTitle(t.title));
|
|
77
|
+
} catch {
|
|
78
|
+
// A PLAN we cannot parse is not a trustworthy "planning done" signal.
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
41
83
|
export function repairStaleSketchFlag(record: SketchFlagDrift): void {
|
|
42
84
|
setSliceSketchFlag(record.mid, record.sid, false);
|
|
43
85
|
}
|
|
@@ -4,6 +4,8 @@ import assert from "node:assert/strict";
|
|
|
4
4
|
import {
|
|
5
5
|
prepareBrowserDaemonForUat,
|
|
6
6
|
shouldWarmBrowserDaemonForUat,
|
|
7
|
+
stopBrowserDaemon,
|
|
8
|
+
teardownWarmedBrowserDaemons,
|
|
7
9
|
} from "../browser-daemon-auto-prep.ts";
|
|
8
10
|
import { commitBrowserEngineResolution } from "../../browser-tools/engine/selection.ts";
|
|
9
11
|
import { resolveGsdBrowserCliAvailability } from "../../shared/gsd-browser-cli.ts";
|
|
@@ -142,3 +144,27 @@ test("prepareBrowserDaemonForUat returns actionable error when daemon start fail
|
|
|
142
144
|
|
|
143
145
|
assert.match(error ?? "", /gsd-browser daemon failed to start/i);
|
|
144
146
|
});
|
|
147
|
+
|
|
148
|
+
test("stopBrowserDaemon reports failure when the gsd-browser CLI is missing", () => {
|
|
149
|
+
const result = stopBrowserDaemon("/tmp/example-project", {
|
|
150
|
+
env: { GSD_BROWSER_MCP_COMMAND: "/definitely/missing/gsd-browser" },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
assert.equal(result.ok, false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("teardownWarmedBrowserDaemons is a no-op when nothing was warmed", () => {
|
|
157
|
+
// A failed warm-up must not register a project root for teardown.
|
|
158
|
+
prepareBrowserDaemonForUat({
|
|
159
|
+
uatType: "browser-executable",
|
|
160
|
+
sessionProvider: "claude-code",
|
|
161
|
+
sessionAuthMode: "externalCli",
|
|
162
|
+
projectRoot: "/tmp/example-project",
|
|
163
|
+
env: {
|
|
164
|
+
...GSD_BROWSER_ENGINE,
|
|
165
|
+
GSD_BROWSER_MCP_COMMAND: "/definitely/missing/gsd-browser",
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
assert.deepEqual(teardownWarmedBrowserDaemons(), []);
|
|
170
|
+
});
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
openDatabase,
|
|
18
18
|
} from "../gsd-db.ts";
|
|
19
19
|
import { migrateHierarchyToDb } from "../md-importer.ts";
|
|
20
|
-
import { checkMarkdownHierarchyAgainstDb } from "../migration-auto-check.ts";
|
|
20
|
+
import { checkMarkdownHierarchyAgainstDb, countMarkdownHierarchy } from "../migration-auto-check.ts";
|
|
21
21
|
import { queryDecisions } from "../context-store.ts";
|
|
22
22
|
import { deriveStateFromDb, invalidateStateCache } from "../state.ts";
|
|
23
23
|
import type { Requirement } from "../types.ts";
|
|
@@ -206,3 +206,78 @@ test("explicit markdown import remains opt-in and is not run by startup mismatch
|
|
|
206
206
|
assert.equal(getAllMilestones().length, 1);
|
|
207
207
|
assert.equal(getSliceTasks("M001", "S01").length, 1);
|
|
208
208
|
});
|
|
209
|
+
|
|
210
|
+
// #1286: migrateHierarchyToDb skips a `[sketch]` slice's stub PLAN tasks, so
|
|
211
|
+
// scanMarkdownHierarchy must skip counting them too. Otherwise the markdown
|
|
212
|
+
// scan claims task rows the DB authoritatively omits, and the startup drift
|
|
213
|
+
// check perpetually reports recovery-required / recommends
|
|
214
|
+
// `/gsd recover --confirm` — which re-skips the stub tasks and never converges.
|
|
215
|
+
test("sketch slice stub tasks are not treated as markdown/DB drift", async (t) => {
|
|
216
|
+
const base = makeBase();
|
|
217
|
+
t.after(() => cleanup(base));
|
|
218
|
+
|
|
219
|
+
const milestoneDir = join(base, ".gsd", "milestones", "M001");
|
|
220
|
+
const sketchSliceDir = join(milestoneDir, "slices", "S01");
|
|
221
|
+
const realSliceDir = join(milestoneDir, "slices", "S02");
|
|
222
|
+
mkdirSync(sketchSliceDir, { recursive: true });
|
|
223
|
+
mkdirSync(realSliceDir, { recursive: true });
|
|
224
|
+
|
|
225
|
+
// S01 is a sketch (badge before risk, per ADR-011 render); S02 is refined.
|
|
226
|
+
writeFileSync(
|
|
227
|
+
join(milestoneDir, "M001-ROADMAP.md"),
|
|
228
|
+
[
|
|
229
|
+
"# M001: Sketch Drift",
|
|
230
|
+
"",
|
|
231
|
+
"**Vision:** Sketch stub tasks must not count as drift",
|
|
232
|
+
"",
|
|
233
|
+
"## Slices",
|
|
234
|
+
"- [ ] **S01: Sketch Slice** `[sketch]` `risk:low` `depends:[]`",
|
|
235
|
+
" > After this: Sketch decomposed.",
|
|
236
|
+
"",
|
|
237
|
+
"- [ ] **S02: Real Slice** `risk:low` `depends:[]`",
|
|
238
|
+
" > After this: Real work done.",
|
|
239
|
+
"",
|
|
240
|
+
].join("\n"),
|
|
241
|
+
);
|
|
242
|
+
// The sketch slice still carries a stub PLAN with a placeholder <tasks> block.
|
|
243
|
+
writeFileSync(
|
|
244
|
+
join(sketchSliceDir, "S01-PLAN.md"),
|
|
245
|
+
[
|
|
246
|
+
"# S01: Sketch Slice",
|
|
247
|
+
"",
|
|
248
|
+
"**Goal:** Placeholder.",
|
|
249
|
+
"",
|
|
250
|
+
"## Tasks",
|
|
251
|
+
"- [ ] **T01: Plan 01**",
|
|
252
|
+
" Stub placeholder task.",
|
|
253
|
+
"",
|
|
254
|
+
].join("\n"),
|
|
255
|
+
);
|
|
256
|
+
writeFileSync(
|
|
257
|
+
join(realSliceDir, "S02-PLAN.md"),
|
|
258
|
+
[
|
|
259
|
+
"# S02: Real Slice",
|
|
260
|
+
"",
|
|
261
|
+
"**Goal:** Real work.",
|
|
262
|
+
"",
|
|
263
|
+
"## Tasks",
|
|
264
|
+
"- [ ] **T01: Task** `est:5m`",
|
|
265
|
+
"",
|
|
266
|
+
].join("\n"),
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
openProjectDb(base);
|
|
270
|
+
// The importer seeds S02's task but skips S01's stub (1 task total).
|
|
271
|
+
const imported = migrateHierarchyToDb(base);
|
|
272
|
+
assert.deepEqual(imported, { milestones: 1, slices: 2, tasks: 1 });
|
|
273
|
+
assert.equal(getSliceTasks("M001", "S01").length, 0, "sketch slice seeds no tasks");
|
|
274
|
+
assert.equal(getSliceTasks("M001", "S02").length, 1, "real slice seeds its task");
|
|
275
|
+
|
|
276
|
+
// The markdown scan must mirror the importer: the sketch stub task is not
|
|
277
|
+
// counted, so markdown (1 task) matches the DB (1 task) and the check is
|
|
278
|
+
// in-sync rather than recommending a recover that would never converge.
|
|
279
|
+
assert.deepEqual(countMarkdownHierarchy(base), { milestones: 1, slices: 2, tasks: 1 });
|
|
280
|
+
const check = await checkMarkdownHierarchyAgainstDb(base);
|
|
281
|
+
assert.equal(check.action, "none", "sketch stub tasks must not be flagged as drift");
|
|
282
|
+
assert.equal(check.reason, "in-sync");
|
|
283
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
import { readDoctorHistory, runGSDDoctor, type DoctorHistoryEntry } from "../doctor.ts";
|
|
8
|
+
|
|
9
|
+
test("doctor.ts preserves readDoctorHistory public API and records doctor runs", async (t) => {
|
|
10
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-history-api-"));
|
|
11
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
12
|
+
|
|
13
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
14
|
+
|
|
15
|
+
const report = await runGSDDoctor(base, { fix: false });
|
|
16
|
+
const history: DoctorHistoryEntry[] = await readDoctorHistory(base, 1);
|
|
17
|
+
|
|
18
|
+
assert.equal(history.length, 1);
|
|
19
|
+
assert.equal(history[0].ok, report.ok);
|
|
20
|
+
assert.equal(history[0].errors, report.issues.filter(issue => issue.severity === "error").length);
|
|
21
|
+
assert.equal(history[0].warnings, report.issues.filter(issue => issue.severity === "warning").length);
|
|
22
|
+
assert.equal(history[0].fixes, 0);
|
|
23
|
+
assert.ok(Array.isArray(history[0].codes));
|
|
24
|
+
assert.equal(typeof history[0].summary, "string");
|
|
25
|
+
});
|
|
@@ -427,3 +427,56 @@ test('migrate-hier: demo text extracted', () => {
|
|
|
427
427
|
}
|
|
428
428
|
});
|
|
429
429
|
|
|
430
|
+
// ─── Test (i): Sketch slice stub tasks are not seeded (#1286) ─────────
|
|
431
|
+
|
|
432
|
+
test('migrate-hier: sketch slice stub PLAN tasks are not imported', () => {
|
|
433
|
+
const base = createFixtureBase();
|
|
434
|
+
try {
|
|
435
|
+
// S01 is a sketch (badge before risk, per ADR-011 render); its PLAN is a
|
|
436
|
+
// stub with a placeholder <tasks> block. S02 is a real, refined slice.
|
|
437
|
+
const roadmap = `# M001: Test Milestone
|
|
438
|
+
|
|
439
|
+
**Vision:** Sketch slices must not seed DB tasks.
|
|
440
|
+
|
|
441
|
+
## Slices
|
|
442
|
+
|
|
443
|
+
- [ ] **S01: Sketch Slice** \`[sketch]\` \`risk:low\` \`depends:[]\`
|
|
444
|
+
> After this: Sketch decomposed.
|
|
445
|
+
|
|
446
|
+
- [ ] **S02: Real Slice** \`risk:low\` \`depends:[]\`
|
|
447
|
+
> After this: Real work done.
|
|
448
|
+
`;
|
|
449
|
+
const sketchPlan = `# S01: Sketch Slice
|
|
450
|
+
|
|
451
|
+
**Goal:** Placeholder.
|
|
452
|
+
|
|
453
|
+
## Tasks
|
|
454
|
+
|
|
455
|
+
- [ ] **T01: Plan 01**
|
|
456
|
+
Stub placeholder task.
|
|
457
|
+
`;
|
|
458
|
+
writeFile(base, 'milestones/M001/M001-ROADMAP.md', roadmap);
|
|
459
|
+
writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', sketchPlan);
|
|
460
|
+
writeFile(base, 'milestones/M001/slices/S02/S02-PLAN.md', PLAN_S02_1_TASK);
|
|
461
|
+
|
|
462
|
+
openDatabase(':memory:');
|
|
463
|
+
const counts = migrateHierarchyToDb(base);
|
|
464
|
+
|
|
465
|
+
const slices = getMilestoneSlices('M001');
|
|
466
|
+
assert.deepStrictEqual(slices.length, 2, 'sketch: 2 slices inserted');
|
|
467
|
+
assert.deepStrictEqual(slices[0]!.is_sketch, 1, 'sketch: S01 preserves is_sketch flag');
|
|
468
|
+
|
|
469
|
+
const s01Tasks = getSliceTasks('M001', 'S01');
|
|
470
|
+
assert.deepStrictEqual(s01Tasks.length, 0, 'sketch: no stub tasks seeded for sketch slice S01');
|
|
471
|
+
|
|
472
|
+
const s02Tasks = getSliceTasks('M001', 'S02');
|
|
473
|
+
assert.deepStrictEqual(s02Tasks.length, 1, 'sketch: real slice S02 still imports its task');
|
|
474
|
+
assert.deepStrictEqual(counts.tasks, 1, 'sketch: only the non-sketch slice contributes tasks');
|
|
475
|
+
|
|
476
|
+
closeDatabase();
|
|
477
|
+
} finally {
|
|
478
|
+
closeDatabase();
|
|
479
|
+
cleanup(base);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|