gsd-pi 2.75.0-dev.2203010a0 → 2.75.0-dev.96d4bb599

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 (109) 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/bootstrap/memory-tools.js +128 -0
  6. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  7. package/dist/resources/extensions/gsd/bootstrap/system-context.js +17 -4
  8. package/dist/resources/extensions/gsd/commands/handlers/onboarding.js +52 -68
  9. package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
  10. package/dist/resources/extensions/gsd/commands-memory.js +462 -0
  11. package/dist/resources/extensions/gsd/gsd-db.js +237 -4
  12. package/dist/resources/extensions/gsd/memory-embeddings.js +219 -0
  13. package/dist/resources/extensions/gsd/memory-extractor.js +78 -27
  14. package/dist/resources/extensions/gsd/memory-ingest.js +218 -0
  15. package/dist/resources/extensions/gsd/memory-relations.js +189 -0
  16. package/dist/resources/extensions/gsd/memory-source-store.js +113 -0
  17. package/dist/resources/extensions/gsd/memory-store.js +299 -6
  18. package/dist/resources/extensions/gsd/model-router.js +9 -5
  19. package/dist/resources/extensions/gsd/notification-overlay.js +7 -22
  20. package/dist/resources/extensions/gsd/tools/memory-tools.js +306 -0
  21. package/dist/resources/extensions/gsd/tools/skip-slice.js +78 -0
  22. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  23. package/dist/web/standalone/.next/BUILD_ID +1 -1
  24. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  25. package/dist/web/standalone/.next/build-manifest.json +2 -2
  26. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  27. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.html +1 -1
  44. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  51. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  53. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  54. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  55. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  56. package/package.json +1 -1
  57. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  58. package/packages/mcp-server/dist/server.js +12 -10
  59. package/packages/mcp-server/dist/server.js.map +1 -1
  60. package/packages/mcp-server/dist/session-manager.d.ts.map +1 -1
  61. package/packages/mcp-server/dist/session-manager.js +8 -1
  62. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  63. package/packages/mcp-server/dist/workflow-tools.d.ts +1 -0
  64. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  65. package/packages/mcp-server/dist/workflow-tools.js +113 -14
  66. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  67. package/packages/mcp-server/src/mcp-server.test.ts +40 -4
  68. package/packages/mcp-server/src/server.ts +12 -10
  69. package/packages/mcp-server/src/session-manager.ts +10 -3
  70. package/packages/mcp-server/src/workflow-tools.test.ts +91 -1
  71. package/packages/mcp-server/src/workflow-tools.ts +128 -18
  72. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  73. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
  74. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +24 -20
  75. package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +158 -0
  76. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  77. package/src/resources/extensions/gsd/bootstrap/system-context.ts +20 -4
  78. package/src/resources/extensions/gsd/commands/handlers/onboarding.ts +65 -98
  79. package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
  80. package/src/resources/extensions/gsd/commands-memory.ts +551 -0
  81. package/src/resources/extensions/gsd/gsd-db.ts +273 -4
  82. package/src/resources/extensions/gsd/memory-embeddings.ts +235 -0
  83. package/src/resources/extensions/gsd/memory-extractor.ts +100 -34
  84. package/src/resources/extensions/gsd/memory-ingest.ts +286 -0
  85. package/src/resources/extensions/gsd/memory-relations.ts +240 -0
  86. package/src/resources/extensions/gsd/memory-source-store.ts +138 -0
  87. package/src/resources/extensions/gsd/memory-store.ts +351 -7
  88. package/src/resources/extensions/gsd/model-router.ts +10 -5
  89. package/src/resources/extensions/gsd/notification-overlay.ts +9 -19
  90. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
  91. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  92. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  93. package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -2
  94. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
  95. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  96. package/src/resources/extensions/gsd/tests/memory-embeddings.test.ts +213 -0
  97. package/src/resources/extensions/gsd/tests/memory-ingest.test.ts +153 -0
  98. package/src/resources/extensions/gsd/tests/memory-maintenance.test.ts +107 -0
  99. package/src/resources/extensions/gsd/tests/memory-relations.test.ts +175 -0
  100. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  101. package/src/resources/extensions/gsd/tests/memory-tools.test.ts +295 -0
  102. package/src/resources/extensions/gsd/tests/model-router.test.ts +50 -0
  103. package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +56 -37
  104. package/src/resources/extensions/gsd/tests/skip-slice-cascades-tasks.test.ts +125 -0
  105. package/src/resources/extensions/gsd/tools/memory-tools.ts +380 -0
  106. package/src/resources/extensions/gsd/tools/skip-slice.ts +133 -0
  107. package/src/resources/extensions/gsd/workflow-logger.ts +3 -1
  108. /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → o61X3klsB6C0UE0X1x3PA}/_buildManifest.js +0 -0
  109. /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → o61X3klsB6C0UE0X1x3PA}/_ssgManifest.js +0 -0
@@ -0,0 +1,306 @@
1
+ // GSD Memory Tools — Phase 1 executors for capture_thought, memory_query, gsd_graph
2
+ //
3
+ // These executors back the three memory-layer tools the LLM can call at any
4
+ // point in a session. They build on the existing `memory-store.ts` layer
5
+ // (SQLite memories table) and degrade gracefully when the DB is unavailable.
6
+ //
7
+ // Phase 1 scope:
8
+ // - capture_thought → create a memory with the caller-supplied category/content
9
+ // - memory_query → keyword-filtered, score-ranked listing of active memories
10
+ // - gsd_graph → returns a memory and its supersedes edges only (Phase 4 adds memory_relations)
11
+ import { _getAdapter, isDbAvailable } from "../gsd-db.js";
12
+ import { createMemory, getActiveMemoriesRanked, queryMemoriesRanked, reinforceMemory, } from "../memory-store.js";
13
+ import { traverseGraph } from "../memory-relations.js";
14
+ function dbUnavailable(operation) {
15
+ return {
16
+ content: [
17
+ {
18
+ type: "text",
19
+ text: "Error: GSD database is not available. Memory tools require an initialized .gsd/ project.",
20
+ },
21
+ ],
22
+ details: { operation, error: "db_unavailable" },
23
+ isError: true,
24
+ };
25
+ }
26
+ const VALID_CATEGORIES = new Set([
27
+ "architecture",
28
+ "convention",
29
+ "gotcha",
30
+ "preference",
31
+ "environment",
32
+ "pattern",
33
+ ]);
34
+ export function executeMemoryCapture(params) {
35
+ if (!isDbAvailable())
36
+ return dbUnavailable("memory_capture");
37
+ const category = (params.category ?? "").trim().toLowerCase();
38
+ const content = (params.content ?? "").trim();
39
+ if (!category || !content) {
40
+ return {
41
+ content: [{ type: "text", text: "Error: category and content are required." }],
42
+ details: { operation: "memory_capture", error: "missing_fields" },
43
+ isError: true,
44
+ };
45
+ }
46
+ if (!VALID_CATEGORIES.has(category)) {
47
+ return {
48
+ content: [
49
+ {
50
+ type: "text",
51
+ text: `Error: invalid category "${category}". Must be one of: ${[...VALID_CATEGORIES].join(", ")}.`,
52
+ },
53
+ ],
54
+ details: { operation: "memory_capture", error: "invalid_category" },
55
+ isError: true,
56
+ };
57
+ }
58
+ const confidence = clampConfidence(params.confidence);
59
+ const scope = normalizeScope(params.scope);
60
+ const tags = normalizeTags(params.tags);
61
+ const id = createMemory({ category, content, confidence, scope, tags });
62
+ if (!id) {
63
+ return {
64
+ content: [{ type: "text", text: "Error: failed to create memory." }],
65
+ details: { operation: "memory_capture", error: "create_failed" },
66
+ isError: true,
67
+ };
68
+ }
69
+ return {
70
+ content: [{ type: "text", text: `Captured ${id} (${category}): ${content}` }],
71
+ details: { operation: "memory_capture", id, category, confidence, scope, tags },
72
+ };
73
+ }
74
+ function normalizeScope(value) {
75
+ if (typeof value !== "string")
76
+ return "project";
77
+ const trimmed = value.trim();
78
+ return trimmed.length === 0 ? "project" : trimmed;
79
+ }
80
+ function normalizeTags(value) {
81
+ if (!Array.isArray(value))
82
+ return [];
83
+ return value.filter((t) => typeof t === "string" && t.trim().length > 0).slice(0, 10);
84
+ }
85
+ function clampConfidence(value) {
86
+ if (typeof value !== "number" || !Number.isFinite(value))
87
+ return 0.8;
88
+ if (value < 0.1)
89
+ return 0.1;
90
+ if (value > 0.99)
91
+ return 0.99;
92
+ return value;
93
+ }
94
+ export function executeMemoryQuery(params) {
95
+ if (!isDbAvailable())
96
+ return dbUnavailable("memory_query");
97
+ const query = (params.query ?? "").trim();
98
+ const k = clampTopK(params.k, 10);
99
+ const includeSuperseded = params.include_superseded === true;
100
+ const category = params.category?.trim().toLowerCase() || undefined;
101
+ const scopeFilter = params.scope?.trim() || undefined;
102
+ const tagFilter = params.tag?.trim().toLowerCase() || undefined;
103
+ try {
104
+ let ranked = [];
105
+ if (query) {
106
+ ranked = queryMemoriesRanked({
107
+ query,
108
+ k,
109
+ category,
110
+ scope: scopeFilter,
111
+ tag: tagFilter,
112
+ include_superseded: includeSuperseded,
113
+ });
114
+ }
115
+ else {
116
+ const candidates = includeSuperseded
117
+ ? includeSupersededMemories(getActiveMemoriesRanked(200))
118
+ : getActiveMemoriesRanked(200);
119
+ ranked = candidates
120
+ .filter((m) => {
121
+ if (category && m.category.toLowerCase() !== category)
122
+ return false;
123
+ if (scopeFilter && m.scope !== scopeFilter)
124
+ return false;
125
+ if (tagFilter && !m.tags.map((t) => t.toLowerCase()).includes(tagFilter))
126
+ return false;
127
+ return true;
128
+ })
129
+ .slice(0, k)
130
+ .map((memory) => ({
131
+ memory,
132
+ score: memory.confidence * (1 + memory.hit_count * 0.1),
133
+ keywordRank: null,
134
+ semanticRank: null,
135
+ confidenceBoost: memory.confidence * (1 + memory.hit_count * 0.1),
136
+ reason: "ranked",
137
+ }));
138
+ }
139
+ const hits = ranked.map((r) => ({
140
+ id: r.memory.id,
141
+ category: r.memory.category,
142
+ content: r.memory.content,
143
+ confidence: r.memory.confidence,
144
+ hit_count: r.memory.hit_count,
145
+ score: r.score,
146
+ reason: r.reason,
147
+ keyword_rank: r.keywordRank,
148
+ semantic_rank: r.semanticRank,
149
+ }));
150
+ if (params.reinforce_hits) {
151
+ for (const h of hits)
152
+ reinforceMemory(h.id);
153
+ }
154
+ const summary = hits.length === 0
155
+ ? "No matching memories."
156
+ : hits.map((h) => `- [${h.id}] (${h.category}) ${h.content}`).join("\n");
157
+ return {
158
+ content: [{ type: "text", text: summary }],
159
+ details: {
160
+ operation: "memory_query",
161
+ query,
162
+ k,
163
+ returned: hits.length,
164
+ hits,
165
+ },
166
+ };
167
+ }
168
+ catch (err) {
169
+ return {
170
+ content: [{ type: "text", text: `Error: memory query failed: ${err.message}` }],
171
+ details: { operation: "memory_query", error: err.message },
172
+ isError: true,
173
+ };
174
+ }
175
+ }
176
+ function clampTopK(value, fallback) {
177
+ if (typeof value !== "number" || !Number.isFinite(value))
178
+ return fallback;
179
+ if (value < 1)
180
+ return 1;
181
+ if (value > 50)
182
+ return 50;
183
+ return Math.floor(value);
184
+ }
185
+ function includeSupersededMemories(rankedActive) {
186
+ const adapter = _getAdapter();
187
+ if (!adapter)
188
+ return rankedActive;
189
+ try {
190
+ const rows = adapter.prepare("SELECT * FROM memories").all();
191
+ return rows.map((row) => {
192
+ let tags = [];
193
+ if (typeof row["tags"] === "string") {
194
+ try {
195
+ const parsed = JSON.parse(row["tags"]);
196
+ if (Array.isArray(parsed))
197
+ tags = parsed.filter((t) => typeof t === "string");
198
+ }
199
+ catch {
200
+ /* leave empty */
201
+ }
202
+ }
203
+ return {
204
+ seq: row["seq"],
205
+ id: row["id"],
206
+ category: row["category"],
207
+ content: row["content"],
208
+ confidence: row["confidence"],
209
+ source_unit_type: row["source_unit_type"] ?? null,
210
+ source_unit_id: row["source_unit_id"] ?? null,
211
+ created_at: row["created_at"],
212
+ updated_at: row["updated_at"],
213
+ superseded_by: row["superseded_by"] ?? null,
214
+ hit_count: row["hit_count"],
215
+ scope: row["scope"] ?? "project",
216
+ tags,
217
+ };
218
+ });
219
+ }
220
+ catch {
221
+ return rankedActive;
222
+ }
223
+ }
224
+ export function executeGsdGraph(params) {
225
+ if (!isDbAvailable())
226
+ return dbUnavailable("gsd_graph");
227
+ if (params.mode === "build") {
228
+ // The extractor emits LINK actions incrementally (Phase 4). There is no
229
+ // batch rebuild step to run today — ingest artifacts via `/gsd memory
230
+ // extract <SRC-...>` and the next extraction turn will add edges.
231
+ return {
232
+ content: [
233
+ {
234
+ type: "text",
235
+ text: "gsd_graph build acknowledged. Graph edges are populated incrementally by memory " +
236
+ "extraction (including LINK actions). Use `/gsd memory extract <SRC-...>` to trigger " +
237
+ "extraction against a specific ingested source.",
238
+ },
239
+ ],
240
+ details: { operation: "gsd_graph", mode: "build", built: 0 },
241
+ };
242
+ }
243
+ if (params.mode !== "query") {
244
+ return {
245
+ content: [{ type: "text", text: `Error: unknown mode "${params.mode}". Must be "build" or "query".` }],
246
+ details: { operation: "gsd_graph", error: "invalid_mode" },
247
+ isError: true,
248
+ };
249
+ }
250
+ const memoryId = params.memoryId?.trim();
251
+ if (!memoryId) {
252
+ return {
253
+ content: [{ type: "text", text: "Error: memoryId is required for mode=query." }],
254
+ details: { operation: "gsd_graph", error: "missing_memory_id" },
255
+ isError: true,
256
+ };
257
+ }
258
+ try {
259
+ const graph = traverseGraph(memoryId, clampDepth(params.depth));
260
+ const rel = params.rel?.trim().toLowerCase() || null;
261
+ const edges = rel ? graph.edges.filter((e) => e.rel === rel) : graph.edges;
262
+ const relevantIds = new Set([memoryId]);
263
+ for (const e of edges) {
264
+ relevantIds.add(e.from);
265
+ relevantIds.add(e.to);
266
+ }
267
+ const nodes = graph.nodes.filter((n) => relevantIds.has(n.id));
268
+ if (nodes.length === 0) {
269
+ return {
270
+ content: [{ type: "text", text: `No memory found with id ${memoryId}.` }],
271
+ details: { operation: "gsd_graph", mode: "query", memoryId, nodes: [], edges: [] },
272
+ };
273
+ }
274
+ const summary = [
275
+ `Memory ${memoryId} — ${nodes.length} node(s), ${edges.length} edge(s).`,
276
+ ...nodes.map((n) => ` [${n.id}] (${n.category}) ${n.content}`),
277
+ ...edges.map((e) => ` ${e.from} --${e.rel}-> ${e.to}`),
278
+ ].join("\n");
279
+ return {
280
+ content: [{ type: "text", text: summary }],
281
+ details: {
282
+ operation: "gsd_graph",
283
+ mode: "query",
284
+ memoryId,
285
+ nodes: nodes.map((n) => ({ id: n.id, category: n.category, content: n.content })),
286
+ edges: edges.map((e) => ({ from: e.from, to: e.to, rel: e.rel })),
287
+ },
288
+ };
289
+ }
290
+ catch (err) {
291
+ return {
292
+ content: [{ type: "text", text: `Error: graph query failed: ${err.message}` }],
293
+ details: { operation: "gsd_graph", error: err.message },
294
+ isError: true,
295
+ };
296
+ }
297
+ }
298
+ function clampDepth(value) {
299
+ if (typeof value !== "number" || !Number.isFinite(value))
300
+ return 1;
301
+ if (value < 0)
302
+ return 0;
303
+ if (value > 5)
304
+ return 5;
305
+ return Math.floor(value);
306
+ }
@@ -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
+ }