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.
Files changed (56) hide show
  1. package/dist/onboarding.d.ts +5 -1
  2. package/dist/onboarding.js +5 -3
  3. package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
  4. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +23 -19
  5. package/dist/resources/extensions/gsd/commands/handlers/onboarding.js +28 -4
  6. package/dist/resources/extensions/gsd/model-router.js +9 -5
  7. package/dist/resources/extensions/gsd/notification-overlay.js +7 -22
  8. package/dist/resources/extensions/gsd/tools/skip-slice.js +78 -0
  9. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  10. package/dist/web/standalone/.next/BUILD_ID +1 -1
  11. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  12. package/dist/web/standalone/.next/build-manifest.json +2 -2
  13. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  14. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/index.html +1 -1
  31. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  38. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  39. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  40. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  41. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  42. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  43. package/package.json +1 -1
  44. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
  45. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +24 -20
  46. package/src/resources/extensions/gsd/commands/handlers/onboarding.ts +38 -5
  47. package/src/resources/extensions/gsd/model-router.ts +10 -5
  48. package/src/resources/extensions/gsd/notification-overlay.ts +9 -19
  49. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
  50. package/src/resources/extensions/gsd/tests/model-router.test.ts +50 -0
  51. package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +56 -37
  52. package/src/resources/extensions/gsd/tests/onboarding-handler-loader.test.ts +41 -0
  53. package/src/resources/extensions/gsd/tests/skip-slice-cascades-tasks.test.ts +125 -0
  54. package/src/resources/extensions/gsd/tools/skip-slice.ts +133 -0
  55. /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → J2z3GMC9QtSLr7gyoM38c}/_buildManifest.js +0 -0
  56. /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → J2z3GMC9QtSLr7gyoM38c}/_ssgManifest.js +0 -0
@@ -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>;
@@ -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
- process.stderr.write(renderLogo(pc.cyan));
211
- p.intro(pc.bold('Welcome to GSD — let\'s get you set up'));
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 { getSlice, updateSliceStatus } = await import("../gsd-db.js");
711
+ const { handleSkipSlice } = await import("../tools/skip-slice.js");
712
712
  const { invalidateStateCache } = await import("../state.js");
713
- const slice = getSlice(params.milestoneId, params.sliceId);
714
- if (!slice) {
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: Slice ${params.sliceId} not found in milestone ${params.milestoneId}` }],
717
- details: { operation: "skip_slice", error: "slice_not_found" },
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"}. Auto-mode will advance past this slice.` }],
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 specifier = "../../../../../onboarding.js";
18
- return (await import(/* @vite-ignore */ specifier));
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 builtin = MODEL_CAPABILITY_PROFILES[modelId];
193
- const override = capabilityOverrides?.[modelId];
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 = modelId.includes("/") ? modelId.split("/").pop() : modelId;
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 = modelId.includes("/") ? modelId.split("/").pop() : modelId;
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 = modelId.includes("/") ? modelId.split("/").pop() : modelId;
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
- /** Word-wrap plain text to fit within maxWidth columns. */
18
+ /** Column-aware word wrap using pi-tui's native wrapper (handles unicode/ANSI). */
19
19
  function wrapText(text, maxWidth) {
20
- if (text.length <= maxWidth)
20
+ if (maxWidth <= 0)
21
21
  return [text];
22
- const words = text.split(/\s+/);
23
- const lines = [];
24
- let current = "";
25
- for (const word of words) {
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
+ }