gsd-pi 2.52.0 → 2.53.0-dev.07ffe51
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 +55 -32
- package/dist/headless-query.js +1 -1
- package/dist/resources/extensions/get-secrets-from-user.js +7 -0
- package/dist/resources/extensions/gsd/auto/phases.js +28 -8
- package/dist/resources/extensions/gsd/auto-dispatch.js +5 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +70 -14
- package/dist/resources/extensions/gsd/auto.js +22 -0
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +4 -10
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +3 -3
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -2
- package/dist/resources/extensions/gsd/git-service.js +4 -3
- package/dist/resources/extensions/gsd/guided-flow.js +4 -3
- package/dist/resources/extensions/gsd/markdown-renderer.js +5 -4
- package/dist/resources/extensions/gsd/parallel-orchestrator.js +18 -2
- package/dist/resources/extensions/gsd/preferences-types.js +1 -1
- package/dist/resources/extensions/gsd/state.js +18 -29
- package/dist/resources/extensions/gsd/status-guards.js +12 -0
- package/dist/resources/extensions/gsd/tools/complete-milestone.js +4 -3
- package/dist/resources/extensions/gsd/tools/complete-slice.js +4 -3
- package/dist/resources/extensions/gsd/tools/complete-task.js +4 -3
- package/dist/resources/extensions/gsd/tools/plan-milestone.js +4 -14
- package/dist/resources/extensions/gsd/tools/plan-slice.js +4 -14
- package/dist/resources/extensions/gsd/tools/plan-task.js +4 -14
- package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +6 -7
- package/dist/resources/extensions/gsd/tools/reopen-slice.js +4 -3
- package/dist/resources/extensions/gsd/tools/reopen-task.js +5 -4
- package/dist/resources/extensions/gsd/tools/replan-slice.js +5 -6
- package/dist/resources/extensions/gsd/validation.js +21 -0
- package/dist/resources/extensions/shared/rtk.js +14 -4
- package/dist/rtk.js +3 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
- package/dist/web/standalone/.next/build-manifest.json +4 -4
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
- package/dist/web/standalone/.next/required-server-files.json +4 -4
- 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 +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/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/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 +2 -2
- 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 +2 -2
- 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 +16 -16
- package/dist/web/standalone/.next/server/chunks/2229.js +1 -1
- package/dist/web/standalone/.next/server/chunks/7471.js +3 -3
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware.js +2 -2
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/4024.87fd909ae0110f50.js +9 -0
- package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-b950e4e384cc62b3.js +1 -0
- package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-024d82be84800e52.js → webpack-bca0e732db0dcec3.js} +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/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/scripts/ensure-workspace-builds.cjs +36 -8
- package/src/resources/extensions/get-secrets-from-user.ts +8 -0
- package/src/resources/extensions/gsd/auto/phases.ts +38 -7
- package/src/resources/extensions/gsd/auto-dispatch.ts +6 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +73 -14
- package/src/resources/extensions/gsd/auto.ts +21 -0
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +4 -11
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +3 -3
- package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -2
- package/src/resources/extensions/gsd/git-service.ts +4 -3
- package/src/resources/extensions/gsd/guided-flow.ts +4 -3
- package/src/resources/extensions/gsd/markdown-renderer.ts +5 -4
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +23 -1
- package/src/resources/extensions/gsd/preferences-types.ts +1 -1
- package/src/resources/extensions/gsd/state.ts +18 -29
- package/src/resources/extensions/gsd/status-guards.ts +13 -0
- package/src/resources/extensions/gsd/tests/active-milestone-id-guard.test.ts +91 -0
- package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-auto-resolve.test.ts +80 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +64 -30
- package/src/resources/extensions/gsd/tests/milestone-report-path.test.ts +51 -0
- package/src/resources/extensions/gsd/tests/parallel-orchestrator-zombie-cleanup.test.ts +277 -0
- package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +103 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/rate-limit-model-fallback.test.ts +90 -0
- package/src/resources/extensions/gsd/tests/session-lock-transient-read.test.ts +9 -8
- package/src/resources/extensions/gsd/tests/stash-pop-gsd-conflict.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/status-guards.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +12 -2
- package/src/resources/extensions/gsd/tests/validation-gate-patterns.test.ts +124 -0
- package/src/resources/extensions/gsd/tests/validation.test.ts +72 -0
- package/src/resources/extensions/gsd/tools/complete-milestone.ts +4 -3
- package/src/resources/extensions/gsd/tools/complete-slice.ts +4 -3
- package/src/resources/extensions/gsd/tools/complete-task.ts +4 -3
- package/src/resources/extensions/gsd/tools/plan-milestone.ts +4 -16
- package/src/resources/extensions/gsd/tools/plan-slice.ts +4 -16
- package/src/resources/extensions/gsd/tools/plan-task.ts +4 -16
- package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +6 -7
- package/src/resources/extensions/gsd/tools/reopen-slice.ts +4 -3
- package/src/resources/extensions/gsd/tools/reopen-task.ts +5 -4
- package/src/resources/extensions/gsd/tools/replan-slice.ts +5 -7
- package/src/resources/extensions/gsd/validation.ts +23 -0
- package/src/resources/extensions/shared/rtk.ts +22 -4
- package/dist/web/standalone/.next/static/chunks/4024.21054f459af5cc78.js +0 -9
- package/dist/web/standalone/.next/static/chunks/app/page-fbecd1237e2d6d1f.js +0 -1
- package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
- /package/dist/web/standalone/.next/static/{vlgS2rkXjxeKhgXhdp4lh → Q5pfrfJIvgUKR3LJLVB0T}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{vlgS2rkXjxeKhgXhdp4lh → Q5pfrfJIvgUKR3LJLVB0T}/_ssgManifest.js +0 -0
|
@@ -6,6 +6,7 @@ import { parseSummary, loadFile, parseRequirementCounts, parseContextDependsOn,
|
|
|
6
6
|
import { resolveMilestoneFile, resolveSlicePath, resolveSliceFile, resolveTaskFile, resolveTasksDir, resolveGsdRootFile, gsdRoot, } from './paths.js';
|
|
7
7
|
import { findMilestoneIds } from './milestone-ids.js';
|
|
8
8
|
import { loadQueueOrder, sortByQueueOrder } from './queue-order.js';
|
|
9
|
+
import { isClosedStatus } from './status-guards.js';
|
|
9
10
|
import { nativeBatchParseGsdFiles } from './native-parser-bridge.js';
|
|
10
11
|
import { join, resolve } from 'path';
|
|
11
12
|
import { existsSync, readdirSync } from 'node:fs';
|
|
@@ -39,19 +40,13 @@ export function isMilestoneComplete(roadmap) {
|
|
|
39
40
|
return roadmap.slices.length > 0 && roadmap.slices.every(s => s.done);
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
42
|
-
* Check whether a VALIDATION file's verdict is terminal
|
|
43
|
-
*
|
|
44
|
-
*
|
|
43
|
+
* Check whether a VALIDATION file's verdict is terminal.
|
|
44
|
+
* Any successfully extracted verdict (pass, needs-attention, needs-remediation,
|
|
45
|
+
* fail, etc.) means validation completed. Only return false when no verdict
|
|
46
|
+
* could be parsed — i.e. extractVerdict() returns undefined (#2769).
|
|
45
47
|
*/
|
|
46
48
|
export function isValidationTerminal(validationContent) {
|
|
47
|
-
|
|
48
|
-
if (!v)
|
|
49
|
-
return false;
|
|
50
|
-
// 'pass' and 'needs-attention' are always terminal.
|
|
51
|
-
// 'needs-remediation' is treated as terminal to prevent infinite loops
|
|
52
|
-
// when no remediation slices exist in the roadmap (#832). The validation
|
|
53
|
-
// report is preserved on disk for manual review.
|
|
54
|
-
return v === 'pass' || v === 'needs-attention' || v === 'needs-remediation';
|
|
49
|
+
return extractVerdict(validationContent) != null;
|
|
55
50
|
}
|
|
56
51
|
const CACHE_TTL_MS = 100;
|
|
57
52
|
let _stateCache = null;
|
|
@@ -203,12 +198,6 @@ function extractContextTitle(content, fallback) {
|
|
|
203
198
|
return stripMilestonePrefix(h1.slice(2).trim()) || fallback;
|
|
204
199
|
}
|
|
205
200
|
// ─── DB-backed State Derivation ────────────────────────────────────────────
|
|
206
|
-
/**
|
|
207
|
-
* Helper: check if a DB status counts as "done" (handles K002 ambiguity).
|
|
208
|
-
*/
|
|
209
|
-
function isStatusDone(status) {
|
|
210
|
-
return status === 'complete' || status === 'done';
|
|
211
|
-
}
|
|
212
201
|
/**
|
|
213
202
|
* Derive GSD state from the milestones/slices/tasks DB tables.
|
|
214
203
|
* Flag files (PARKED, VALIDATION, CONTINUE, REPLAN, REPLAN-TRIGGER, CONTEXT-DRAFT)
|
|
@@ -292,7 +281,7 @@ export async function deriveStateFromDb(basePath) {
|
|
|
292
281
|
parkedMilestoneIds.add(m.id);
|
|
293
282
|
continue;
|
|
294
283
|
}
|
|
295
|
-
if (
|
|
284
|
+
if (isClosedStatus(m.status)) {
|
|
296
285
|
completeMilestoneIds.add(m.id);
|
|
297
286
|
continue;
|
|
298
287
|
}
|
|
@@ -304,7 +293,7 @@ export async function deriveStateFromDb(basePath) {
|
|
|
304
293
|
}
|
|
305
294
|
// Check roadmap: all slices done means milestone is complete
|
|
306
295
|
const slices = getMilestoneSlices(m.id);
|
|
307
|
-
if (slices.length > 0 && slices.every(s =>
|
|
296
|
+
if (slices.length > 0 && slices.every(s => isClosedStatus(s.status))) {
|
|
308
297
|
// All slices done but no summary — still counts as complete for dep resolution
|
|
309
298
|
// if a summary file exists
|
|
310
299
|
// Note: without summary file, the milestone is in validating/completing state, not complete
|
|
@@ -323,7 +312,7 @@ export async function deriveStateFromDb(basePath) {
|
|
|
323
312
|
}
|
|
324
313
|
// Ghost milestone check: no slices in DB AND no substantive files on disk
|
|
325
314
|
const slices = getMilestoneSlices(m.id);
|
|
326
|
-
if (slices.length === 0 && !
|
|
315
|
+
if (slices.length === 0 && !isClosedStatus(m.status)) {
|
|
327
316
|
// Check disk for ghost detection
|
|
328
317
|
if (isGhostMilestone(basePath, m.id))
|
|
329
318
|
continue;
|
|
@@ -344,7 +333,7 @@ export async function deriveStateFromDb(basePath) {
|
|
|
344
333
|
continue;
|
|
345
334
|
}
|
|
346
335
|
// Not complete — determine if it should be active
|
|
347
|
-
const allSlicesDone = slices.length > 0 && slices.every(s =>
|
|
336
|
+
const allSlicesDone = slices.length > 0 && slices.every(s => isClosedStatus(s.status));
|
|
348
337
|
// Get title — prefer DB, fall back to context file extraction
|
|
349
338
|
let title = stripMilestonePrefix(m.title) || m.id;
|
|
350
339
|
if (title === m.id) {
|
|
@@ -484,7 +473,7 @@ export async function deriveStateFromDb(basePath) {
|
|
|
484
473
|
// Guard: [].every() === true (vacuous truth). Without the length check,
|
|
485
474
|
// an empty slice array causes a premature phase transition to
|
|
486
475
|
// validating-milestone. See: https://github.com/gsd-build/gsd-2/issues/2667
|
|
487
|
-
const allSlicesDone = activeMilestoneSlices.length > 0 && activeMilestoneSlices.every(s =>
|
|
476
|
+
const allSlicesDone = activeMilestoneSlices.length > 0 && activeMilestoneSlices.every(s => isClosedStatus(s.status));
|
|
488
477
|
if (allSlicesDone) {
|
|
489
478
|
const validationFile = resolveMilestoneFile(basePath, activeMilestone.id, "VALIDATION");
|
|
490
479
|
const validationContent = validationFile ? await loadFile(validationFile) : null;
|
|
@@ -514,14 +503,14 @@ export async function deriveStateFromDb(basePath) {
|
|
|
514
503
|
}
|
|
515
504
|
// ── Find active slice (first incomplete with deps satisfied) ─────────
|
|
516
505
|
const sliceProgress = {
|
|
517
|
-
done: activeMilestoneSlices.filter(s =>
|
|
506
|
+
done: activeMilestoneSlices.filter(s => isClosedStatus(s.status)).length,
|
|
518
507
|
total: activeMilestoneSlices.length,
|
|
519
508
|
};
|
|
520
|
-
const doneSliceIds = new Set(activeMilestoneSlices.filter(s =>
|
|
509
|
+
const doneSliceIds = new Set(activeMilestoneSlices.filter(s => isClosedStatus(s.status)).map(s => s.id));
|
|
521
510
|
let activeSlice = null;
|
|
522
511
|
let activeSliceRow = null;
|
|
523
512
|
for (const s of activeMilestoneSlices) {
|
|
524
|
-
if (
|
|
513
|
+
if (isClosedStatus(s.status))
|
|
525
514
|
continue;
|
|
526
515
|
if (s.depends.every(dep => doneSliceIds.has(dep))) {
|
|
527
516
|
activeSlice = { id: s.id, title: s.title };
|
|
@@ -561,7 +550,7 @@ export async function deriveStateFromDb(basePath) {
|
|
|
561
550
|
// causing the dispatcher to re-dispatch the same completed task forever.
|
|
562
551
|
let reconciled = false;
|
|
563
552
|
for (const t of tasks) {
|
|
564
|
-
if (
|
|
553
|
+
if (isClosedStatus(t.status))
|
|
565
554
|
continue;
|
|
566
555
|
const summaryPath = resolveTaskFile(basePath, activeMilestone.id, activeSlice.id, t.id, "SUMMARY");
|
|
567
556
|
if (summaryPath && existsSync(summaryPath)) {
|
|
@@ -581,10 +570,10 @@ export async function deriveStateFromDb(basePath) {
|
|
|
581
570
|
tasks = getSliceTasks(activeMilestone.id, activeSlice.id);
|
|
582
571
|
}
|
|
583
572
|
const taskProgress = {
|
|
584
|
-
done: tasks.filter(t =>
|
|
573
|
+
done: tasks.filter(t => isClosedStatus(t.status)).length,
|
|
585
574
|
total: tasks.length,
|
|
586
575
|
};
|
|
587
|
-
const activeTaskRow = tasks.find(t => !
|
|
576
|
+
const activeTaskRow = tasks.find(t => !isClosedStatus(t.status));
|
|
588
577
|
if (!activeTaskRow && tasks.length > 0) {
|
|
589
578
|
// All tasks done but slice not marked complete → summarizing
|
|
590
579
|
return {
|
|
@@ -639,7 +628,7 @@ export async function deriveStateFromDb(basePath) {
|
|
|
639
628
|
};
|
|
640
629
|
}
|
|
641
630
|
// ── Blocker detection: check completed tasks for blocker_discovered ──
|
|
642
|
-
const completedTasks = tasks.filter(t =>
|
|
631
|
+
const completedTasks = tasks.filter(t => isClosedStatus(t.status));
|
|
643
632
|
let blockerTaskId = null;
|
|
644
633
|
for (const ct of completedTasks) {
|
|
645
634
|
if (ct.blocker_discovered) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status predicates for GSD state-machine guards.
|
|
3
|
+
*
|
|
4
|
+
* The DB stores status as free-form strings. Two values indicate
|
|
5
|
+
* "closed": "complete" (canonical) and "done" (legacy / alias).
|
|
6
|
+
* Every inline `status === "complete" || status === "done"` should
|
|
7
|
+
* use isClosedStatus() instead.
|
|
8
|
+
*/
|
|
9
|
+
/** Returns true when a milestone, slice, or task status indicates closure. */
|
|
10
|
+
export function isClosedStatus(status) {
|
|
11
|
+
return status === "complete" || status === "done";
|
|
12
|
+
}
|
|
@@ -9,6 +9,7 @@ import { join } from "node:path";
|
|
|
9
9
|
import { mkdirSync } from "node:fs";
|
|
10
10
|
import { transaction, getMilestone, getMilestoneSlices, getSliceTasks, updateMilestoneStatus, } from "../gsd-db.js";
|
|
11
11
|
import { resolveMilestonePath, clearPathCache } from "../paths.js";
|
|
12
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
12
13
|
import { saveFile, clearParseCache } from "../files.js";
|
|
13
14
|
import { invalidateStateCache } from "../state.js";
|
|
14
15
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
@@ -89,7 +90,7 @@ export async function handleCompleteMilestone(params, basePath) {
|
|
|
89
90
|
guardError = `milestone not found: ${params.milestoneId}`;
|
|
90
91
|
return;
|
|
91
92
|
}
|
|
92
|
-
if (milestone.status
|
|
93
|
+
if (isClosedStatus(milestone.status)) {
|
|
93
94
|
guardError = `milestone ${params.milestoneId} is already complete`;
|
|
94
95
|
return;
|
|
95
96
|
}
|
|
@@ -99,7 +100,7 @@ export async function handleCompleteMilestone(params, basePath) {
|
|
|
99
100
|
guardError = `no slices found for milestone ${params.milestoneId}`;
|
|
100
101
|
return;
|
|
101
102
|
}
|
|
102
|
-
const incompleteSlices = slices.filter(s => s.status
|
|
103
|
+
const incompleteSlices = slices.filter(s => !isClosedStatus(s.status));
|
|
103
104
|
if (incompleteSlices.length > 0) {
|
|
104
105
|
const incompleteIds = incompleteSlices.map(s => `${s.id} (status: ${s.status})`).join(", ");
|
|
105
106
|
guardError = `incomplete slices: ${incompleteIds}`;
|
|
@@ -108,7 +109,7 @@ export async function handleCompleteMilestone(params, basePath) {
|
|
|
108
109
|
// Deep check: verify all tasks in all slices are complete
|
|
109
110
|
for (const slice of slices) {
|
|
110
111
|
const tasks = getSliceTasks(params.milestoneId, slice.id);
|
|
111
|
-
const incompleteTasks = tasks.filter(t => t.status
|
|
112
|
+
const incompleteTasks = tasks.filter(t => !isClosedStatus(t.status));
|
|
112
113
|
if (incompleteTasks.length > 0) {
|
|
113
114
|
const ids = incompleteTasks.map(t => `${t.id} (status: ${t.status})`).join(", ");
|
|
114
115
|
guardError = `slice ${slice.id} has incomplete tasks: ${ids}`;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { mkdirSync } from "node:fs";
|
|
11
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
11
12
|
import { transaction, insertMilestone, insertSlice, getSlice, getSliceTasks, getMilestone, updateSliceStatus, setSliceSummaryMd, } from "../gsd-db.js";
|
|
12
13
|
import { resolveSlicePath, clearPathCache } from "../paths.js";
|
|
13
14
|
import { checkOwnership, sliceUnitKey } from "../unit-ownership.js";
|
|
@@ -179,12 +180,12 @@ export async function handleCompleteSlice(params, basePath) {
|
|
|
179
180
|
// Milestone/slice not existing is OK — insertMilestone/insertSlice below will auto-create.
|
|
180
181
|
// Only block if they exist and are closed.
|
|
181
182
|
const milestone = getMilestone(params.milestoneId);
|
|
182
|
-
if (milestone && (milestone.status
|
|
183
|
+
if (milestone && isClosedStatus(milestone.status)) {
|
|
183
184
|
guardError = `cannot complete slice in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
184
185
|
return;
|
|
185
186
|
}
|
|
186
187
|
const slice = getSlice(params.milestoneId, params.sliceId);
|
|
187
|
-
if (slice && (slice.status
|
|
188
|
+
if (slice && isClosedStatus(slice.status)) {
|
|
188
189
|
guardError = `slice ${params.sliceId} is already complete — use gsd_slice_reopen first if you need to redo it`;
|
|
189
190
|
return;
|
|
190
191
|
}
|
|
@@ -194,7 +195,7 @@ export async function handleCompleteSlice(params, basePath) {
|
|
|
194
195
|
guardError = `no tasks found for slice ${params.sliceId} in milestone ${params.milestoneId}`;
|
|
195
196
|
return;
|
|
196
197
|
}
|
|
197
|
-
const incompleteTasks = tasks.filter(t => t.status
|
|
198
|
+
const incompleteTasks = tasks.filter(t => !isClosedStatus(t.status));
|
|
198
199
|
if (incompleteTasks.length > 0) {
|
|
199
200
|
const incompleteIds = incompleteTasks.map(t => `${t.id} (status: ${t.status})`).join(", ");
|
|
200
201
|
guardError = `incomplete tasks: ${incompleteIds}`;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { mkdirSync } from "node:fs";
|
|
11
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
11
12
|
import { transaction, insertMilestone, insertSlice, insertTask, insertVerificationEvidence, getMilestone, getSlice, getTask, updateTaskStatus, setTaskSummaryMd, deleteVerificationEvidence, } from "../gsd-db.js";
|
|
12
13
|
import { resolveSliceFile, resolveTasksDir, clearPathCache } from "../paths.js";
|
|
13
14
|
import { checkOwnership, taskUnitKey } from "../unit-ownership.js";
|
|
@@ -122,17 +123,17 @@ export async function handleCompleteTask(params, basePath) {
|
|
|
122
123
|
// Milestone/slice not existing is OK — insertMilestone/insertSlice below will auto-create.
|
|
123
124
|
// Only block if they exist and are closed.
|
|
124
125
|
const milestone = getMilestone(params.milestoneId);
|
|
125
|
-
if (milestone && (milestone.status
|
|
126
|
+
if (milestone && isClosedStatus(milestone.status)) {
|
|
126
127
|
guardError = `cannot complete task in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
127
128
|
return;
|
|
128
129
|
}
|
|
129
130
|
const slice = getSlice(params.milestoneId, params.sliceId);
|
|
130
|
-
if (slice && (slice.status
|
|
131
|
+
if (slice && isClosedStatus(slice.status)) {
|
|
131
132
|
guardError = `cannot complete task in a closed slice: ${params.sliceId} (status: ${slice.status})`;
|
|
132
133
|
return;
|
|
133
134
|
}
|
|
134
135
|
const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
|
|
135
|
-
if (existingTask && (existingTask.status
|
|
136
|
+
if (existingTask && isClosedStatus(existingTask.status)) {
|
|
136
137
|
guardError = `task ${params.taskId} is already complete — use gsd_task_reopen first if you need to redo it`;
|
|
137
138
|
return;
|
|
138
139
|
}
|
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import { clearParseCache } from "../files.js";
|
|
2
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
3
|
+
import { isNonEmptyString, validateStringArray } from "../validation.js";
|
|
2
4
|
import { transaction, getMilestone, insertMilestone, insertSlice, upsertMilestonePlanning, upsertSlicePlanning, } from "../gsd-db.js";
|
|
3
5
|
import { invalidateStateCache } from "../state.js";
|
|
4
6
|
import { renderRoadmapFromDb } from "../markdown-renderer.js";
|
|
5
7
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
6
8
|
import { writeManifest } from "../workflow-manifest.js";
|
|
7
9
|
import { appendEvent } from "../workflow-events.js";
|
|
8
|
-
function isNonEmptyString(value) {
|
|
9
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
10
|
-
}
|
|
11
|
-
function validateStringArray(value, field) {
|
|
12
|
-
if (!Array.isArray(value)) {
|
|
13
|
-
throw new Error(`${field} must be an array`);
|
|
14
|
-
}
|
|
15
|
-
if (value.some((item) => !isNonEmptyString(item))) {
|
|
16
|
-
throw new Error(`${field} must contain only non-empty strings`);
|
|
17
|
-
}
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
10
|
function validateRiskEntries(value) {
|
|
21
11
|
if (!Array.isArray(value)) {
|
|
22
12
|
throw new Error("keyRisks must be an array");
|
|
@@ -152,7 +142,7 @@ export async function handlePlanMilestone(rawParams, basePath) {
|
|
|
152
142
|
try {
|
|
153
143
|
transaction(() => {
|
|
154
144
|
const existingMilestone = getMilestone(params.milestoneId);
|
|
155
|
-
if (existingMilestone && (existingMilestone.status
|
|
145
|
+
if (existingMilestone && isClosedStatus(existingMilestone.status)) {
|
|
156
146
|
guardError = `cannot re-plan milestone ${params.milestoneId}: it is already complete`;
|
|
157
147
|
return;
|
|
158
148
|
}
|
|
@@ -164,7 +154,7 @@ export async function handlePlanMilestone(rawParams, basePath) {
|
|
|
164
154
|
guardError = `depends_on references unknown milestone: ${depId}`;
|
|
165
155
|
return;
|
|
166
156
|
}
|
|
167
|
-
if (dep.status
|
|
157
|
+
if (!isClosedStatus(dep.status)) {
|
|
168
158
|
guardError = `depends_on milestone ${depId} is not yet complete (status: ${dep.status})`;
|
|
169
159
|
return;
|
|
170
160
|
}
|
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import { clearParseCache } from "../files.js";
|
|
2
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
3
|
+
import { isNonEmptyString } from "../validation.js";
|
|
2
4
|
import { transaction, getMilestone, getSlice, insertTask, upsertSlicePlanning, upsertTaskPlanning, insertGateRow, } from "../gsd-db.js";
|
|
3
5
|
import { invalidateStateCache } from "../state.js";
|
|
4
6
|
import { renderPlanFromDb } from "../markdown-renderer.js";
|
|
5
7
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
6
8
|
import { writeManifest } from "../workflow-manifest.js";
|
|
7
9
|
import { appendEvent } from "../workflow-events.js";
|
|
8
|
-
function isNonEmptyString(value) {
|
|
9
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
10
|
-
}
|
|
11
|
-
function validateStringArray(value, field) {
|
|
12
|
-
if (!Array.isArray(value)) {
|
|
13
|
-
throw new Error(`${field} must be an array`);
|
|
14
|
-
}
|
|
15
|
-
if (value.some((item) => !isNonEmptyString(item))) {
|
|
16
|
-
throw new Error(`${field} must contain only non-empty strings`);
|
|
17
|
-
}
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
10
|
function validateTasks(value) {
|
|
21
11
|
if (!Array.isArray(value) || value.length === 0) {
|
|
22
12
|
throw new Error("tasks must be a non-empty array");
|
|
@@ -113,7 +103,7 @@ export async function handlePlanSlice(rawParams, basePath) {
|
|
|
113
103
|
guardError = `milestone not found: ${params.milestoneId}`;
|
|
114
104
|
return;
|
|
115
105
|
}
|
|
116
|
-
if (parentMilestone.status
|
|
106
|
+
if (isClosedStatus(parentMilestone.status)) {
|
|
117
107
|
guardError = `cannot plan slice in a closed milestone: ${params.milestoneId} (status: ${parentMilestone.status})`;
|
|
118
108
|
return;
|
|
119
109
|
}
|
|
@@ -122,7 +112,7 @@ export async function handlePlanSlice(rawParams, basePath) {
|
|
|
122
112
|
guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
|
|
123
113
|
return;
|
|
124
114
|
}
|
|
125
|
-
if (parentSlice.status
|
|
115
|
+
if (isClosedStatus(parentSlice.status)) {
|
|
126
116
|
guardError = `cannot re-plan slice ${params.sliceId}: it is already complete — use gsd_slice_reopen first`;
|
|
127
117
|
return;
|
|
128
118
|
}
|
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
import { clearParseCache } from "../files.js";
|
|
2
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
3
|
+
import { isNonEmptyString, validateStringArray } from "../validation.js";
|
|
2
4
|
import { transaction, getSlice, getTask, insertTask, upsertTaskPlanning } from "../gsd-db.js";
|
|
3
5
|
import { invalidateStateCache } from "../state.js";
|
|
4
6
|
import { renderTaskPlanFromDb } from "../markdown-renderer.js";
|
|
5
7
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
6
8
|
import { writeManifest } from "../workflow-manifest.js";
|
|
7
9
|
import { appendEvent } from "../workflow-events.js";
|
|
8
|
-
function isNonEmptyString(value) {
|
|
9
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
10
|
-
}
|
|
11
|
-
function validateStringArray(value, field) {
|
|
12
|
-
if (!Array.isArray(value)) {
|
|
13
|
-
throw new Error(`${field} must be an array`);
|
|
14
|
-
}
|
|
15
|
-
if (value.some((item) => !isNonEmptyString(item))) {
|
|
16
|
-
throw new Error(`${field} must contain only non-empty strings`);
|
|
17
|
-
}
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
10
|
function validateParams(params) {
|
|
21
11
|
if (!isNonEmptyString(params?.milestoneId))
|
|
22
12
|
throw new Error("milestoneId is required");
|
|
@@ -61,12 +51,12 @@ export async function handlePlanTask(rawParams, basePath) {
|
|
|
61
51
|
guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
|
|
62
52
|
return;
|
|
63
53
|
}
|
|
64
|
-
if (parentSlice.status
|
|
54
|
+
if (isClosedStatus(parentSlice.status)) {
|
|
65
55
|
guardError = `cannot plan task in a closed slice: ${params.sliceId} (status: ${parentSlice.status})`;
|
|
66
56
|
return;
|
|
67
57
|
}
|
|
68
58
|
const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
|
|
69
|
-
if (existingTask && (existingTask.status
|
|
59
|
+
if (existingTask && isClosedStatus(existingTask.status)) {
|
|
70
60
|
guardError = `cannot re-plan task ${params.taskId}: it is already complete — use gsd_task_reopen first`;
|
|
71
61
|
return;
|
|
72
62
|
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
1
2
|
import { clearParseCache } from "../files.js";
|
|
3
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
4
|
+
import { isNonEmptyString } from "../validation.js";
|
|
2
5
|
import { transaction, getMilestone, getMilestoneSlices, getSlice, insertSlice, updateSliceFields, insertAssessment, deleteSlice, } from "../gsd-db.js";
|
|
3
6
|
import { invalidateStateCache } from "../state.js";
|
|
4
7
|
import { renderRoadmapFromDb, renderAssessmentFromDb } from "../markdown-renderer.js";
|
|
5
8
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
6
9
|
import { writeManifest } from "../workflow-manifest.js";
|
|
7
10
|
import { appendEvent } from "../workflow-events.js";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
function isNonEmptyString(value) {
|
|
10
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
11
|
-
}
|
|
12
11
|
function validateParams(params) {
|
|
13
12
|
if (!isNonEmptyString(params?.milestoneId))
|
|
14
13
|
throw new Error("milestoneId is required");
|
|
@@ -76,7 +75,7 @@ export async function handleReassessRoadmap(rawParams, basePath) {
|
|
|
76
75
|
guardError = `milestone not found: ${params.milestoneId}`;
|
|
77
76
|
return;
|
|
78
77
|
}
|
|
79
|
-
if (milestone.status
|
|
78
|
+
if (isClosedStatus(milestone.status)) {
|
|
80
79
|
guardError = `cannot reassess a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
81
80
|
return;
|
|
82
81
|
}
|
|
@@ -86,7 +85,7 @@ export async function handleReassessRoadmap(rawParams, basePath) {
|
|
|
86
85
|
guardError = `completedSliceId not found: ${params.milestoneId}/${params.completedSliceId}`;
|
|
87
86
|
return;
|
|
88
87
|
}
|
|
89
|
-
if (completedSlice.status
|
|
88
|
+
if (!isClosedStatus(completedSlice.status)) {
|
|
90
89
|
guardError = `completedSliceId ${params.completedSliceId} is not complete (status: ${completedSlice.status}) — reassess can only be called after a slice finishes`;
|
|
91
90
|
return;
|
|
92
91
|
}
|
|
@@ -94,7 +93,7 @@ export async function handleReassessRoadmap(rawParams, basePath) {
|
|
|
94
93
|
const existingSlices = getMilestoneSlices(params.milestoneId);
|
|
95
94
|
const completedSliceIds = new Set();
|
|
96
95
|
for (const slice of existingSlices) {
|
|
97
|
-
if (slice.status
|
|
96
|
+
if (isClosedStatus(slice.status)) {
|
|
98
97
|
completedSliceIds.add(slice.id);
|
|
99
98
|
}
|
|
100
99
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
12
12
|
import { getMilestone, getSlice, getSliceTasks, updateSliceStatus, updateTaskStatus, transaction, } from "../gsd-db.js";
|
|
13
13
|
import { invalidateStateCache } from "../state.js";
|
|
14
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
14
15
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
15
16
|
import { writeManifest } from "../workflow-manifest.js";
|
|
16
17
|
import { appendEvent } from "../workflow-events.js";
|
|
@@ -31,8 +32,8 @@ export async function handleReopenSlice(params, basePath) {
|
|
|
31
32
|
guardError = `milestone not found: ${params.milestoneId}`;
|
|
32
33
|
return;
|
|
33
34
|
}
|
|
34
|
-
if (milestone.status
|
|
35
|
-
guardError = `cannot reopen slice
|
|
35
|
+
if (isClosedStatus(milestone.status)) {
|
|
36
|
+
guardError = `cannot reopen slice in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
38
39
|
const slice = getSlice(params.milestoneId, params.sliceId);
|
|
@@ -40,7 +41,7 @@ export async function handleReopenSlice(params, basePath) {
|
|
|
40
41
|
guardError = `slice not found: ${params.milestoneId}/${params.sliceId}`;
|
|
41
42
|
return;
|
|
42
43
|
}
|
|
43
|
-
if (slice.status
|
|
44
|
+
if (!isClosedStatus(slice.status)) {
|
|
44
45
|
guardError = `slice ${params.sliceId} is not complete (status: ${slice.status}) — nothing to reopen`;
|
|
45
46
|
return;
|
|
46
47
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
11
11
|
import { getMilestone, getSlice, getTask, updateTaskStatus, transaction, } from "../gsd-db.js";
|
|
12
12
|
import { invalidateStateCache } from "../state.js";
|
|
13
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
13
14
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
14
15
|
import { writeManifest } from "../workflow-manifest.js";
|
|
15
16
|
import { appendEvent } from "../workflow-events.js";
|
|
@@ -32,7 +33,7 @@ export async function handleReopenTask(params, basePath) {
|
|
|
32
33
|
guardError = `milestone not found: ${params.milestoneId}`;
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
|
-
if (milestone.status
|
|
36
|
+
if (isClosedStatus(milestone.status)) {
|
|
36
37
|
guardError = `cannot reopen task in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
@@ -41,8 +42,8 @@ export async function handleReopenTask(params, basePath) {
|
|
|
41
42
|
guardError = `slice not found: ${params.milestoneId}/${params.sliceId}`;
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
|
-
if (slice.status
|
|
45
|
-
guardError = `cannot reopen task
|
|
45
|
+
if (isClosedStatus(slice.status)) {
|
|
46
|
+
guardError = `cannot reopen task in a closed slice: ${params.sliceId} (status: ${slice.status}) — use gsd_slice_reopen first`;
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
48
49
|
const task = getTask(params.milestoneId, params.sliceId, params.taskId);
|
|
@@ -50,7 +51,7 @@ export async function handleReopenTask(params, basePath) {
|
|
|
50
51
|
guardError = `task not found: ${params.milestoneId}/${params.sliceId}/${params.taskId}`;
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
|
-
if (task.status
|
|
54
|
+
if (!isClosedStatus(task.status)) {
|
|
54
55
|
guardError = `task ${params.taskId} is not complete (status: ${task.status}) — nothing to reopen`;
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { clearParseCache } from "../files.js";
|
|
2
2
|
import { transaction, getSlice, getSliceTasks, getTask, insertTask, upsertTaskPlanning, insertReplanHistory, deleteTask, } from "../gsd-db.js";
|
|
3
3
|
import { invalidateStateCache } from "../state.js";
|
|
4
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
5
|
+
import { isNonEmptyString } from "../validation.js";
|
|
4
6
|
import { renderPlanFromDb, renderReplanFromDb } from "../markdown-renderer.js";
|
|
5
7
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
6
8
|
import { writeManifest } from "../workflow-manifest.js";
|
|
7
9
|
import { appendEvent } from "../workflow-events.js";
|
|
8
|
-
function isNonEmptyString(value) {
|
|
9
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
10
|
-
}
|
|
11
10
|
function validateParams(params) {
|
|
12
11
|
if (!isNonEmptyString(params?.milestoneId))
|
|
13
12
|
throw new Error("milestoneId is required");
|
|
@@ -59,7 +58,7 @@ export async function handleReplanSlice(rawParams, basePath) {
|
|
|
59
58
|
guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
|
|
60
59
|
return;
|
|
61
60
|
}
|
|
62
|
-
if (parentSlice.status
|
|
61
|
+
if (isClosedStatus(parentSlice.status)) {
|
|
63
62
|
guardError = `cannot replan a closed slice: ${params.sliceId} (status: ${parentSlice.status})`;
|
|
64
63
|
return;
|
|
65
64
|
}
|
|
@@ -69,7 +68,7 @@ export async function handleReplanSlice(rawParams, basePath) {
|
|
|
69
68
|
guardError = `blockerTaskId not found: ${params.milestoneId}/${params.sliceId}/${params.blockerTaskId}`;
|
|
70
69
|
return;
|
|
71
70
|
}
|
|
72
|
-
if (blockerTask.status
|
|
71
|
+
if (!isClosedStatus(blockerTask.status)) {
|
|
73
72
|
guardError = `blockerTaskId ${params.blockerTaskId} is not complete (status: ${blockerTask.status}) — the blocker task must be finished before a replan is triggered`;
|
|
74
73
|
return;
|
|
75
74
|
}
|
|
@@ -77,7 +76,7 @@ export async function handleReplanSlice(rawParams, basePath) {
|
|
|
77
76
|
const existingTasks = getSliceTasks(params.milestoneId, params.sliceId);
|
|
78
77
|
const completedTaskIds = new Set();
|
|
79
78
|
for (const task of existingTasks) {
|
|
80
|
-
if (task.status
|
|
79
|
+
if (isClosedStatus(task.status)) {
|
|
81
80
|
completedTaskIds.add(task.id);
|
|
82
81
|
}
|
|
83
82
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared input-validation primitives for GSD tool handlers.
|
|
3
|
+
*/
|
|
4
|
+
/** Type guard: value is a string with at least one non-whitespace character. */
|
|
5
|
+
export function isNonEmptyString(value) {
|
|
6
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Validate that `value` is an array of non-empty strings.
|
|
10
|
+
* Throws with a message referencing `field` on failure.
|
|
11
|
+
* Returns the validated array (narrowed to string[]).
|
|
12
|
+
*/
|
|
13
|
+
export function validateStringArray(value, field) {
|
|
14
|
+
if (!Array.isArray(value)) {
|
|
15
|
+
throw new Error(`${field} must be an array`);
|
|
16
|
+
}
|
|
17
|
+
if (value.some((item) => !isNonEmptyString(item))) {
|
|
18
|
+
throw new Error(`${field} must contain only non-empty strings`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { delimiter, join } from "node:path";
|
|
5
5
|
const GSD_RTK_PATH_ENV = "GSD_RTK_PATH";
|
|
6
6
|
const GSD_RTK_DISABLED_ENV = "GSD_RTK_DISABLED";
|
|
7
|
+
const GSD_RTK_REWRITE_TIMEOUT_MS_ENV = "GSD_RTK_REWRITE_TIMEOUT_MS";
|
|
7
8
|
const RTK_TELEMETRY_DISABLED_ENV = "RTK_TELEMETRY_DISABLED";
|
|
8
9
|
const RTK_REWRITE_TIMEOUT_MS = 5_000;
|
|
9
10
|
function isTruthy(value) {
|
|
@@ -12,6 +13,13 @@ function isTruthy(value) {
|
|
|
12
13
|
const normalized = value.trim().toLowerCase();
|
|
13
14
|
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
14
15
|
}
|
|
16
|
+
function getRewriteTimeoutMs(env = process.env) {
|
|
17
|
+
const configured = Number.parseInt(env[GSD_RTK_REWRITE_TIMEOUT_MS_ENV] ?? "", 10);
|
|
18
|
+
if (Number.isFinite(configured) && configured > 0) {
|
|
19
|
+
return configured;
|
|
20
|
+
}
|
|
21
|
+
return RTK_REWRITE_TIMEOUT_MS;
|
|
22
|
+
}
|
|
15
23
|
export function isRtkEnabled(env = process.env) {
|
|
16
24
|
return !isTruthy(env[GSD_RTK_DISABLED_ENV]);
|
|
17
25
|
}
|
|
@@ -75,19 +83,21 @@ export function resolveRtkBinaryPath(options = {}) {
|
|
|
75
83
|
}
|
|
76
84
|
return resolveSystemRtkPath(options.pathValue ?? getPathValue(env), platform);
|
|
77
85
|
}
|
|
78
|
-
export function rewriteCommandWithRtk(command,
|
|
86
|
+
export function rewriteCommandWithRtk(command, options = {}) {
|
|
87
|
+
const env = options.env ?? process.env;
|
|
79
88
|
if (!command.trim())
|
|
80
89
|
return command;
|
|
81
90
|
if (!isRtkEnabled(env))
|
|
82
91
|
return command;
|
|
83
|
-
const binaryPath = resolveRtkBinaryPath({ env });
|
|
92
|
+
const binaryPath = options.binaryPath ?? resolveRtkBinaryPath({ env });
|
|
84
93
|
if (!binaryPath)
|
|
85
94
|
return command;
|
|
86
|
-
const
|
|
95
|
+
const run = options.spawnSyncImpl ?? spawnSync;
|
|
96
|
+
const result = run(binaryPath, ["rewrite", command], {
|
|
87
97
|
encoding: "utf-8",
|
|
88
98
|
env: buildRtkEnv(env),
|
|
89
99
|
stdio: ["ignore", "pipe", "ignore"],
|
|
90
|
-
timeout:
|
|
100
|
+
timeout: getRewriteTimeoutMs(env),
|
|
91
101
|
// .cmd/.bat wrappers (used by fake-rtk in tests) require shell:true on Windows
|
|
92
102
|
shell: /\.(cmd|bat)$/i.test(binaryPath),
|
|
93
103
|
});
|
package/dist/rtk.js
CHANGED
|
@@ -175,7 +175,9 @@ function resolveSystemRtkPath(pathValue, platform = process.platform) {
|
|
|
175
175
|
export function resolveRtkBinaryPath(options = {}) {
|
|
176
176
|
const env = options.env ?? process.env;
|
|
177
177
|
const platform = options.platform ?? process.platform;
|
|
178
|
-
|
|
178
|
+
if (options.binaryPath)
|
|
179
|
+
return options.binaryPath;
|
|
180
|
+
const explicitPath = env[GSD_RTK_PATH_ENV];
|
|
179
181
|
if (explicitPath && existsSync(explicitPath)) {
|
|
180
182
|
return explicitPath;
|
|
181
183
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
Q5pfrfJIvgUKR3LJLVB0T
|