gsd-pi 2.75.0-dev.2203010a0 → 2.75.0-dev.b6ad8c5f7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/onboarding.d.ts +5 -1
- package/dist/onboarding.js +5 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +23 -19
- package/dist/resources/extensions/gsd/commands/handlers/onboarding.js +28 -4
- package/dist/resources/extensions/gsd/model-router.js +9 -5
- package/dist/resources/extensions/gsd/notification-overlay.js +7 -22
- package/dist/resources/extensions/gsd/tools/skip-slice.js +78 -0
- 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 +11 -11
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- 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.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- 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/package.json +1 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +24 -20
- package/src/resources/extensions/gsd/commands/handlers/onboarding.ts +38 -5
- package/src/resources/extensions/gsd/model-router.ts +10 -5
- package/src/resources/extensions/gsd/notification-overlay.ts +9 -19
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +56 -37
- package/src/resources/extensions/gsd/tests/onboarding-handler-loader.test.ts +41 -0
- package/src/resources/extensions/gsd/tests/skip-slice-cascades-tasks.test.ts +125 -0
- package/src/resources/extensions/gsd/tools/skip-slice.ts +133 -0
- /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → J2z3GMC9QtSLr7gyoM38c}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → J2z3GMC9QtSLr7gyoM38c}/_ssgManifest.js +0 -0
package/dist/onboarding.d.ts
CHANGED
|
@@ -20,6 +20,10 @@ type PicoModule = {
|
|
|
20
20
|
red: (s: string) => string;
|
|
21
21
|
reset: (s: string) => string;
|
|
22
22
|
};
|
|
23
|
+
interface RunOnboardingOptions {
|
|
24
|
+
/** Show logo + intro banner. Disable when onboarding is launched inside an active TUI session. */
|
|
25
|
+
showIntro?: boolean;
|
|
26
|
+
}
|
|
23
27
|
/**
|
|
24
28
|
* Determine if the onboarding wizard should run.
|
|
25
29
|
*
|
|
@@ -46,7 +50,7 @@ export declare function shouldRunOnboarding(authStorage: AuthStorage, settingsDe
|
|
|
46
50
|
* All steps are skippable. All errors are recoverable.
|
|
47
51
|
* Writes status to stderr during execution.
|
|
48
52
|
*/
|
|
49
|
-
export declare function runOnboarding(authStorage: AuthStorage): Promise<void>;
|
|
53
|
+
export declare function runOnboarding(authStorage: AuthStorage, opts?: RunOnboardingOptions): Promise<void>;
|
|
50
54
|
export declare function runLlmStep(p: ClackModule, pc: PicoModule, authStorage: AuthStorage): Promise<boolean>;
|
|
51
55
|
export declare function runWebSearchStep(p: ClackModule, pc: PicoModule, authStorage: AuthStorage, isAnthropicAuth: boolean): Promise<string | null>;
|
|
52
56
|
export declare function runToolKeysStep(p: ClackModule, pc: PicoModule, authStorage: AuthStorage): Promise<number>;
|
package/dist/onboarding.js
CHANGED
|
@@ -194,7 +194,7 @@ export function shouldRunOnboarding(authStorage, settingsDefaultProvider) {
|
|
|
194
194
|
* All steps are skippable. All errors are recoverable.
|
|
195
195
|
* Writes status to stderr during execution.
|
|
196
196
|
*/
|
|
197
|
-
export async function runOnboarding(authStorage) {
|
|
197
|
+
export async function runOnboarding(authStorage, opts = {}) {
|
|
198
198
|
let p;
|
|
199
199
|
let pc;
|
|
200
200
|
try {
|
|
@@ -207,8 +207,10 @@ export async function runOnboarding(authStorage) {
|
|
|
207
207
|
return;
|
|
208
208
|
}
|
|
209
209
|
// ── Intro ─────────────────────────────────────────────────────────────────
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
if (opts.showIntro !== false) {
|
|
211
|
+
process.stderr.write(renderLogo(pc.cyan));
|
|
212
|
+
p.intro(pc.bold('Welcome to GSD — let\'s get you set up'));
|
|
213
|
+
}
|
|
212
214
|
const completedSteps = [];
|
|
213
215
|
// ── LLM Provider Selection ────────────────────────────────────────────────
|
|
214
216
|
const llmResult = await runStep(p, 'LLM setup failed', () => runLlmStep(p, pc, authStorage), {
|
|
@@ -150,7 +150,7 @@ sessionModelOverride) {
|
|
|
150
150
|
const shouldClassify = !isHook || routingConfig.hooks !== false;
|
|
151
151
|
if (shouldClassify) {
|
|
152
152
|
let classification = classifyUnitComplexity(unitType, unitId, basePath, budgetPct, taskMetadataForPolicy);
|
|
153
|
-
const availableModelIds = routingEligibleModels.map(m => m.id);
|
|
153
|
+
const availableModelIds = routingEligibleModels.map(m => `${m.provider}/${m.id}`);
|
|
154
154
|
// Escalate tier on retry when escalate_on_failure is enabled (default: true)
|
|
155
155
|
if (retryContext?.isRetry &&
|
|
156
156
|
retryContext.previousTier &&
|
|
@@ -708,28 +708,23 @@ export function registerDbTools(pi) {
|
|
|
708
708
|
};
|
|
709
709
|
}
|
|
710
710
|
try {
|
|
711
|
-
const {
|
|
711
|
+
const { handleSkipSlice } = await import("../tools/skip-slice.js");
|
|
712
712
|
const { invalidateStateCache } = await import("../state.js");
|
|
713
|
-
const
|
|
714
|
-
|
|
713
|
+
const result = handleSkipSlice({
|
|
714
|
+
milestoneId: params.milestoneId,
|
|
715
|
+
sliceId: params.sliceId,
|
|
716
|
+
reason: params.reason,
|
|
717
|
+
});
|
|
718
|
+
if (result.error) {
|
|
715
719
|
return {
|
|
716
|
-
content: [{ type: "text", text: `Error:
|
|
717
|
-
details: {
|
|
720
|
+
content: [{ type: "text", text: `Error: ${result.error}` }],
|
|
721
|
+
details: {
|
|
722
|
+
operation: "skip_slice",
|
|
723
|
+
error: result.error,
|
|
724
|
+
errorCode: result.errorCode ?? "skip_failed",
|
|
725
|
+
},
|
|
718
726
|
};
|
|
719
727
|
}
|
|
720
|
-
if (slice.status === "complete" || slice.status === "done") {
|
|
721
|
-
return {
|
|
722
|
-
content: [{ type: "text", text: `Error: Slice ${params.sliceId} is already complete — cannot skip.` }],
|
|
723
|
-
details: { operation: "skip_slice", error: "already_complete" },
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
if (slice.status === "skipped") {
|
|
727
|
-
return {
|
|
728
|
-
content: [{ type: "text", text: `Slice ${params.sliceId} is already skipped.` }],
|
|
729
|
-
details: { operation: "skip_slice", sliceId: params.sliceId, milestoneId: params.milestoneId },
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
updateSliceStatus(params.milestoneId, params.sliceId, "skipped");
|
|
733
728
|
invalidateStateCache();
|
|
734
729
|
// Rebuild STATE.md so it reflects the skip immediately (#3477).
|
|
735
730
|
// Without this, /gsd auto reads stale STATE.md and resumes the skipped slice.
|
|
@@ -741,13 +736,20 @@ export function registerDbTools(pi) {
|
|
|
741
736
|
catch (err) {
|
|
742
737
|
logError("tool", `skip_slice rebuildState failed: ${err.message}`, { tool: "gsd_skip_slice" });
|
|
743
738
|
}
|
|
739
|
+
const suffix = result.wasAlreadySkipped
|
|
740
|
+
? result.tasksSkipped > 0
|
|
741
|
+
? ` (already skipped; cascaded ${result.tasksSkipped} leftover task(s) to skipped).`
|
|
742
|
+
: " (already skipped; no pending tasks to cascade)."
|
|
743
|
+
: ` Cascaded ${result.tasksSkipped} task(s) to skipped. Auto-mode will advance past this slice.`;
|
|
744
744
|
return {
|
|
745
|
-
content: [{ type: "text", text: `Skipped slice ${params.sliceId} (${params.milestoneId}). Reason: ${params.reason ?? "User-directed skip"}
|
|
745
|
+
content: [{ type: "text", text: `Skipped slice ${params.sliceId} (${params.milestoneId}). Reason: ${params.reason ?? "User-directed skip"}.${suffix}` }],
|
|
746
746
|
details: {
|
|
747
747
|
operation: "skip_slice",
|
|
748
748
|
sliceId: params.sliceId,
|
|
749
749
|
milestoneId: params.milestoneId,
|
|
750
750
|
reason: params.reason,
|
|
751
|
+
tasksSkipped: result.tasksSkipped,
|
|
752
|
+
wasAlreadySkipped: result.wasAlreadySkipped,
|
|
751
753
|
},
|
|
752
754
|
};
|
|
753
755
|
}
|
|
@@ -764,12 +766,14 @@ export function registerDbTools(pi) {
|
|
|
764
766
|
name: "gsd_skip_slice",
|
|
765
767
|
label: "Skip Slice",
|
|
766
768
|
description: "Mark a slice as skipped so auto-mode advances past it without executing. " +
|
|
769
|
+
"Non-closed tasks within the slice are cascaded to skipped so milestone completion is not blocked by leftover pending tasks (#4375). " +
|
|
767
770
|
"The slice data is preserved for reference. The state machine treats skipped slices like completed ones for dependency satisfaction.",
|
|
768
771
|
promptSnippet: "Skip a GSD slice (mark as skipped, auto-mode will advance past it)",
|
|
769
772
|
promptGuidelines: [
|
|
770
773
|
"Use gsd_skip_slice when a slice should be bypassed — descoped, superseded, or no longer relevant.",
|
|
771
774
|
"Cannot skip a slice that is already complete.",
|
|
772
775
|
"Skipped slices satisfy downstream dependencies just like completed slices.",
|
|
776
|
+
"All pending/active tasks in the slice are cascaded to skipped; completed tasks are never downgraded.",
|
|
773
777
|
],
|
|
774
778
|
parameters: Type.Object({
|
|
775
779
|
sliceId: Type.String({ description: "Slice ID (e.g. S02)" }),
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// this handler lets users re-launch it on demand.
|
|
6
6
|
import { AuthStorage } from "@gsd/pi-coding-agent";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
|
-
import { join } from "node:path";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
9
10
|
import { ONBOARDING_STEPS, isValidStepId, nearestResumeStep, } from "../../setup-catalog.js";
|
|
10
11
|
import { isOnboardingComplete, readOnboardingRecord, resetOnboarding, } from "../../onboarding-state.js";
|
|
11
12
|
// Inline auth path (mirrors src/app-paths.ts) — keep this module rootDir-clean
|
|
@@ -14,8 +15,31 @@ import { isOnboardingComplete, readOnboardingRecord, resetOnboarding, } from "..
|
|
|
14
15
|
const AUTH_FILE_PATH = join(process.env.GSD_CODING_AGENT_DIR ||
|
|
15
16
|
join(process.env.GSD_HOME || join(homedir(), ".gsd"), "agent"), "auth.json");
|
|
16
17
|
async function loadFirstRunWizard() {
|
|
17
|
-
const
|
|
18
|
-
|
|
18
|
+
const candidates = [];
|
|
19
|
+
// Primary deployed path: loader sets GSD_PKG_ROOT (gsd package root).
|
|
20
|
+
if (process.env.GSD_PKG_ROOT) {
|
|
21
|
+
candidates.push(pathToFileURL(join(process.env.GSD_PKG_ROOT, "dist", "onboarding.js")).href);
|
|
22
|
+
}
|
|
23
|
+
// Fallback: derive package root from process entry (typically dist/loader.js).
|
|
24
|
+
// This keeps /gsd onboarding resilient if GSD_PKG_ROOT is absent.
|
|
25
|
+
const argvEntry = process.argv[1];
|
|
26
|
+
if (argvEntry) {
|
|
27
|
+
const pkgRootFromArgv = resolve(dirname(argvEntry), "..");
|
|
28
|
+
candidates.push(pathToFileURL(join(pkgRootFromArgv, "dist", "onboarding.js")).href);
|
|
29
|
+
}
|
|
30
|
+
// Source-tree/dev fallback (works in dist/resources/... and ts test loaders).
|
|
31
|
+
candidates.push("../../../../../onboarding.js");
|
|
32
|
+
let lastError = null;
|
|
33
|
+
for (const specifier of candidates) {
|
|
34
|
+
try {
|
|
35
|
+
return (await import(/* @vite-ignore */ specifier));
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
lastError = err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const reason = lastError instanceof Error ? lastError.message : String(lastError);
|
|
42
|
+
throw new Error(`[gsd] Failed to load onboarding wizard module. Tried: ${candidates.join(", ")}. Last error: ${reason}`);
|
|
19
43
|
}
|
|
20
44
|
function parseArgs(raw) {
|
|
21
45
|
const tokens = raw.split(/\s+/).filter(Boolean);
|
|
@@ -55,7 +79,7 @@ async function runWholeWizard(ctx, fromStep) {
|
|
|
55
79
|
ctx.ui.notify(`Resuming from step: ${fromStep}. The wizard runs all remaining steps; press skip on any you've already configured.`, "info");
|
|
56
80
|
}
|
|
57
81
|
const { runOnboarding } = await loadFirstRunWizard();
|
|
58
|
-
await runOnboarding(authStorage);
|
|
82
|
+
await runOnboarding(authStorage, { showIntro: false });
|
|
59
83
|
}
|
|
60
84
|
async function runSingleStep(ctx, stepId) {
|
|
61
85
|
const authStorage = await getAuthStorage();
|
|
@@ -189,8 +189,9 @@ export function computeTaskRequirements(unitType, metadata) {
|
|
|
189
189
|
*/
|
|
190
190
|
export function scoreEligibleModels(eligibleModelIds, requirements, capabilityOverrides) {
|
|
191
191
|
const scored = eligibleModelIds.map(modelId => {
|
|
192
|
-
const
|
|
193
|
-
const
|
|
192
|
+
const bareId = bareModelId(modelId);
|
|
193
|
+
const builtin = MODEL_CAPABILITY_PROFILES[bareId];
|
|
194
|
+
const override = capabilityOverrides?.[modelId] ?? capabilityOverrides?.[bareId];
|
|
194
195
|
const profile = builtin
|
|
195
196
|
? override ? { ...builtin, ...override } : builtin
|
|
196
197
|
: { coding: 50, debugging: 50, research: 50, reasoning: 50, speed: 50, longContext: 50, instruction: 50 };
|
|
@@ -400,7 +401,7 @@ export function defaultRoutingConfig() {
|
|
|
400
401
|
// ─── Internal ────────────────────────────────────────────────────────────────
|
|
401
402
|
function getModelTier(modelId) {
|
|
402
403
|
// Strip provider prefix if present
|
|
403
|
-
const bareId =
|
|
404
|
+
const bareId = bareModelId(modelId);
|
|
404
405
|
// Check exact match first
|
|
405
406
|
if (MODEL_CAPABILITY_TIER[bareId])
|
|
406
407
|
return MODEL_CAPABILITY_TIER[bareId];
|
|
@@ -414,7 +415,7 @@ function getModelTier(modelId) {
|
|
|
414
415
|
}
|
|
415
416
|
/** Check if a model ID has a known capability tier mapping. (#2192) */
|
|
416
417
|
function isKnownModel(modelId) {
|
|
417
|
-
const bareId =
|
|
418
|
+
const bareId = bareModelId(modelId);
|
|
418
419
|
if (MODEL_CAPABILITY_TIER[bareId])
|
|
419
420
|
return true;
|
|
420
421
|
for (const knownId of Object.keys(MODEL_CAPABILITY_TIER)) {
|
|
@@ -424,7 +425,7 @@ function isKnownModel(modelId) {
|
|
|
424
425
|
return false;
|
|
425
426
|
}
|
|
426
427
|
function getModelCost(modelId) {
|
|
427
|
-
const bareId =
|
|
428
|
+
const bareId = bareModelId(modelId);
|
|
428
429
|
if (MODEL_COST_PER_1K_INPUT[bareId] !== undefined) {
|
|
429
430
|
return MODEL_COST_PER_1K_INPUT[bareId];
|
|
430
431
|
}
|
|
@@ -436,6 +437,9 @@ function getModelCost(modelId) {
|
|
|
436
437
|
// Unknown cost — assume expensive to avoid routing to unknown cheap models
|
|
437
438
|
return 999;
|
|
438
439
|
}
|
|
440
|
+
function bareModelId(modelId) {
|
|
441
|
+
return modelId.includes("/") ? modelId.split("/").pop() : modelId;
|
|
442
|
+
}
|
|
439
443
|
// ─── Tool Compatibility Filter (ADR-005 Phase 3) ───────────────────────────
|
|
440
444
|
/**
|
|
441
445
|
* Check if a tool is compatible with a provider's capabilities.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GSD Extension — Notification History Overlay
|
|
2
2
|
// Scrollable panel showing all persisted notifications with severity filtering.
|
|
3
3
|
// Toggled with Ctrl+Alt+N (⌃⌥N on macOS), Ctrl+Shift+N fallback, or /gsd notifications.
|
|
4
|
-
import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
|
|
4
|
+
import { truncateToWidth, visibleWidth, wrapTextWithAnsi, matchesKey, Key } from "@gsd/pi-tui";
|
|
5
5
|
import { readNotifications, markAllRead, clearNotifications, onNotificationStoreChange, } from "./notification-store.js";
|
|
6
6
|
import { formattedShortcutPair } from "./shortcut-defs.js";
|
|
7
7
|
import { padRight, joinColumns } from "../shared/mod.js";
|
|
@@ -15,29 +15,14 @@ function severityIcon(severity) {
|
|
|
15
15
|
default: return "●";
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
18
|
+
/** Column-aware word wrap using pi-tui's native wrapper (handles unicode/ANSI). */
|
|
19
19
|
function wrapText(text, maxWidth) {
|
|
20
|
-
if (
|
|
20
|
+
if (maxWidth <= 0)
|
|
21
21
|
return [text];
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (current.length === 0) {
|
|
27
|
-
current = word;
|
|
28
|
-
}
|
|
29
|
-
else if (current.length + 1 + word.length <= maxWidth) {
|
|
30
|
-
current += " " + word;
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
lines.push(current);
|
|
34
|
-
current = word;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
if (current.length > 0)
|
|
38
|
-
lines.push(current);
|
|
39
|
-
// If a single word exceeds maxWidth, truncate it
|
|
40
|
-
return lines.map((l) => l.length > maxWidth ? l.slice(0, maxWidth - 1) + "…" : l);
|
|
22
|
+
const lines = wrapTextWithAnsi(text, maxWidth);
|
|
23
|
+
// Safety clamp: if any line still exceeds maxWidth (e.g. unbreakable long token),
|
|
24
|
+
// truncate it with an ellipsis so it cannot bleed past the box border.
|
|
25
|
+
return lines.map((l) => visibleWidth(l) > maxWidth ? truncateToWidth(l, maxWidth, "…") : l);
|
|
41
26
|
}
|
|
42
27
|
function formatTimestamp(ts) {
|
|
43
28
|
try {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skip-slice handler — the core operation behind gsd_skip_slice.
|
|
3
|
+
*
|
|
4
|
+
* Marks a slice as skipped and cascades the skip to every non-closed task in
|
|
5
|
+
* that slice. Without the task cascade the deep-check in
|
|
6
|
+
* executeCompleteMilestone reports pending tasks inside the skipped slice and
|
|
7
|
+
* blocks milestone completion (see #4375).
|
|
8
|
+
*
|
|
9
|
+
* This function performs DB writes only. The MCP wrapper in
|
|
10
|
+
* bootstrap/db-tools.ts handles state-cache invalidation and STATE.md rebuild.
|
|
11
|
+
*/
|
|
12
|
+
import { getSlice, getSliceTasks, isDbAvailable, transaction, updateSliceStatus, updateTaskStatus, } from "../gsd-db.js";
|
|
13
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
14
|
+
/**
|
|
15
|
+
* Mark a slice as "skipped" and cascade the skip to every non-closed task in
|
|
16
|
+
* that slice. Runs as a single transaction so slice status and task statuses
|
|
17
|
+
* are always consistent.
|
|
18
|
+
*
|
|
19
|
+
* Behaviour summary:
|
|
20
|
+
* - Unknown slice → returns {@link SkipSliceResult} with `error`.
|
|
21
|
+
* - Slice already complete/done → returns `error` (cannot un-complete).
|
|
22
|
+
* - Slice already skipped → still cascades leftover non-closed tasks
|
|
23
|
+
* (heals inconsistent historical state from projects that ran older
|
|
24
|
+
* versions before the #4375 cascade fix).
|
|
25
|
+
* - Tasks in closed status (complete/done/skipped) are never downgraded.
|
|
26
|
+
*/
|
|
27
|
+
export function handleSkipSlice(params) {
|
|
28
|
+
const base = {
|
|
29
|
+
milestoneId: params.milestoneId,
|
|
30
|
+
sliceId: params.sliceId,
|
|
31
|
+
tasksSkipped: 0,
|
|
32
|
+
wasAlreadySkipped: false,
|
|
33
|
+
reason: params.reason,
|
|
34
|
+
};
|
|
35
|
+
// Fail loudly on a closed DB so a `null` from getSlice() inside the
|
|
36
|
+
// transaction unambiguously means "slice not found", never "DB unavailable".
|
|
37
|
+
// The MCP wrapper in bootstrap/db-tools.ts runs ensureDbOpen() before calling
|
|
38
|
+
// this helper; this guard protects direct callers (tests, future code).
|
|
39
|
+
if (!isDbAvailable()) {
|
|
40
|
+
throw new Error("handleSkipSlice: GSD database is not available");
|
|
41
|
+
}
|
|
42
|
+
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ────
|
|
43
|
+
let guardError = null;
|
|
44
|
+
let guardCode = null;
|
|
45
|
+
let wasAlreadySkipped = false;
|
|
46
|
+
let tasksSkipped = 0;
|
|
47
|
+
transaction(() => {
|
|
48
|
+
const slice = getSlice(params.milestoneId, params.sliceId);
|
|
49
|
+
if (!slice) {
|
|
50
|
+
guardError = `Slice ${params.sliceId} not found in milestone ${params.milestoneId}`;
|
|
51
|
+
guardCode = "slice_not_found";
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (slice.status === "complete" || slice.status === "done") {
|
|
55
|
+
guardError = `Slice ${params.sliceId} is already complete — cannot skip.`;
|
|
56
|
+
guardCode = "already_complete";
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
wasAlreadySkipped = slice.status === "skipped";
|
|
60
|
+
if (!wasAlreadySkipped) {
|
|
61
|
+
updateSliceStatus(params.milestoneId, params.sliceId, "skipped");
|
|
62
|
+
}
|
|
63
|
+
// Cascade: mark every non-closed task as skipped so milestone completion
|
|
64
|
+
// doesn't trip the deep-task guard (#4375). Closed tasks (complete/done/
|
|
65
|
+
// skipped) are left untouched — we never downgrade.
|
|
66
|
+
const tasks = getSliceTasks(params.milestoneId, params.sliceId);
|
|
67
|
+
for (const task of tasks) {
|
|
68
|
+
if (!isClosedStatus(task.status)) {
|
|
69
|
+
updateTaskStatus(params.milestoneId, params.sliceId, task.id, "skipped");
|
|
70
|
+
tasksSkipped++;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
if (guardError) {
|
|
75
|
+
return { ...base, error: guardError, errorCode: guardCode ?? undefined };
|
|
76
|
+
}
|
|
77
|
+
return { ...base, tasksSkipped, wasAlreadySkipped };
|
|
78
|
+
}
|