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.
- 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/bootstrap/memory-tools.js +128 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +17 -4
- package/dist/resources/extensions/gsd/commands/handlers/onboarding.js +52 -68
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
- package/dist/resources/extensions/gsd/commands-memory.js +462 -0
- package/dist/resources/extensions/gsd/gsd-db.js +237 -4
- package/dist/resources/extensions/gsd/memory-embeddings.js +219 -0
- package/dist/resources/extensions/gsd/memory-extractor.js +78 -27
- package/dist/resources/extensions/gsd/memory-ingest.js +218 -0
- package/dist/resources/extensions/gsd/memory-relations.js +189 -0
- package/dist/resources/extensions/gsd/memory-source-store.js +113 -0
- package/dist/resources/extensions/gsd/memory-store.js +299 -6
- 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/memory-tools.js +306 -0
- 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 +14 -14
- 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 +14 -14
- 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/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +12 -10
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/session-manager.d.ts.map +1 -1
- package/packages/mcp-server/dist/session-manager.js +8 -1
- package/packages/mcp-server/dist/session-manager.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts +1 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +113 -14
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +40 -4
- package/packages/mcp-server/src/server.ts +12 -10
- package/packages/mcp-server/src/session-manager.ts +10 -3
- package/packages/mcp-server/src/workflow-tools.test.ts +91 -1
- package/packages/mcp-server/src/workflow-tools.ts +128 -18
- package/packages/mcp-server/tsconfig.tsbuildinfo +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/bootstrap/memory-tools.ts +158 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +20 -4
- package/src/resources/extensions/gsd/commands/handlers/onboarding.ts +65 -98
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
- package/src/resources/extensions/gsd/commands-memory.ts +551 -0
- package/src/resources/extensions/gsd/gsd-db.ts +273 -4
- package/src/resources/extensions/gsd/memory-embeddings.ts +235 -0
- package/src/resources/extensions/gsd/memory-extractor.ts +100 -34
- package/src/resources/extensions/gsd/memory-ingest.ts +286 -0
- package/src/resources/extensions/gsd/memory-relations.ts +240 -0
- package/src/resources/extensions/gsd/memory-source-store.ts +138 -0
- package/src/resources/extensions/gsd/memory-store.ts +351 -7
- 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/complete-slice.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/memory-embeddings.test.ts +213 -0
- package/src/resources/extensions/gsd/tests/memory-ingest.test.ts +153 -0
- package/src/resources/extensions/gsd/tests/memory-maintenance.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/memory-relations.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/memory-tools.test.ts +295 -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/skip-slice-cascades-tasks.test.ts +125 -0
- package/src/resources/extensions/gsd/tools/memory-tools.ts +380 -0
- package/src/resources/extensions/gsd/tools/skip-slice.ts +133 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +3 -1
- /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → o61X3klsB6C0UE0X1x3PA}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → o61X3klsB6C0UE0X1x3PA}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,380 @@
|
|
|
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
|
+
|
|
12
|
+
import { _getAdapter, isDbAvailable } from "../gsd-db.js";
|
|
13
|
+
import {
|
|
14
|
+
createMemory,
|
|
15
|
+
getActiveMemoriesRanked,
|
|
16
|
+
queryMemoriesRanked,
|
|
17
|
+
reinforceMemory,
|
|
18
|
+
} from "../memory-store.js";
|
|
19
|
+
import type { Memory, RankedMemory } from "../memory-store.js";
|
|
20
|
+
import { traverseGraph } from "../memory-relations.js";
|
|
21
|
+
|
|
22
|
+
// ─── Shared result shape (matches tools/workflow-tool-executors.ts) ─────────
|
|
23
|
+
|
|
24
|
+
export interface ToolExecutionResult {
|
|
25
|
+
content: Array<{ type: "text"; text: string }>;
|
|
26
|
+
details: Record<string, unknown>;
|
|
27
|
+
isError?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function dbUnavailable(operation: string): ToolExecutionResult {
|
|
31
|
+
return {
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: "text",
|
|
35
|
+
text: "Error: GSD database is not available. Memory tools require an initialized .gsd/ project.",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
details: { operation, error: "db_unavailable" },
|
|
39
|
+
isError: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─── capture_thought ────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export interface MemoryCaptureParams {
|
|
46
|
+
category: string;
|
|
47
|
+
content: string;
|
|
48
|
+
confidence?: number;
|
|
49
|
+
tags?: string[];
|
|
50
|
+
scope?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const VALID_CATEGORIES = new Set([
|
|
54
|
+
"architecture",
|
|
55
|
+
"convention",
|
|
56
|
+
"gotcha",
|
|
57
|
+
"preference",
|
|
58
|
+
"environment",
|
|
59
|
+
"pattern",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
export function executeMemoryCapture(params: MemoryCaptureParams): ToolExecutionResult {
|
|
63
|
+
if (!isDbAvailable()) return dbUnavailable("memory_capture");
|
|
64
|
+
|
|
65
|
+
const category = (params.category ?? "").trim().toLowerCase();
|
|
66
|
+
const content = (params.content ?? "").trim();
|
|
67
|
+
if (!category || !content) {
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: "text", text: "Error: category and content are required." }],
|
|
70
|
+
details: { operation: "memory_capture", error: "missing_fields" },
|
|
71
|
+
isError: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (!VALID_CATEGORIES.has(category)) {
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: "text",
|
|
79
|
+
text: `Error: invalid category "${category}". Must be one of: ${[...VALID_CATEGORIES].join(", ")}.`,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
details: { operation: "memory_capture", error: "invalid_category" },
|
|
83
|
+
isError: true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const confidence = clampConfidence(params.confidence);
|
|
87
|
+
const scope = normalizeScope(params.scope);
|
|
88
|
+
const tags = normalizeTags(params.tags);
|
|
89
|
+
|
|
90
|
+
const id = createMemory({ category, content, confidence, scope, tags });
|
|
91
|
+
if (!id) {
|
|
92
|
+
return {
|
|
93
|
+
content: [{ type: "text", text: "Error: failed to create memory." }],
|
|
94
|
+
details: { operation: "memory_capture", error: "create_failed" },
|
|
95
|
+
isError: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text", text: `Captured ${id} (${category}): ${content}` }],
|
|
101
|
+
details: { operation: "memory_capture", id, category, confidence, scope, tags },
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeScope(value: unknown): string {
|
|
106
|
+
if (typeof value !== "string") return "project";
|
|
107
|
+
const trimmed = value.trim();
|
|
108
|
+
return trimmed.length === 0 ? "project" : trimmed;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeTags(value: unknown): string[] {
|
|
112
|
+
if (!Array.isArray(value)) return [];
|
|
113
|
+
return value.filter((t): t is string => typeof t === "string" && t.trim().length > 0).slice(0, 10);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function clampConfidence(value: unknown): number {
|
|
117
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 0.8;
|
|
118
|
+
if (value < 0.1) return 0.1;
|
|
119
|
+
if (value > 0.99) return 0.99;
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── memory_query ───────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
export interface MemoryQueryParams {
|
|
126
|
+
query: string;
|
|
127
|
+
k?: number;
|
|
128
|
+
category?: string;
|
|
129
|
+
scope?: string;
|
|
130
|
+
tag?: string;
|
|
131
|
+
include_superseded?: boolean;
|
|
132
|
+
reinforce_hits?: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface MemoryQueryHit {
|
|
136
|
+
id: string;
|
|
137
|
+
category: string;
|
|
138
|
+
content: string;
|
|
139
|
+
confidence: number;
|
|
140
|
+
hit_count: number;
|
|
141
|
+
score: number;
|
|
142
|
+
reason: "keyword" | "semantic" | "both" | "ranked";
|
|
143
|
+
keyword_rank: number | null;
|
|
144
|
+
semantic_rank: number | null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function executeMemoryQuery(params: MemoryQueryParams): ToolExecutionResult {
|
|
148
|
+
if (!isDbAvailable()) return dbUnavailable("memory_query");
|
|
149
|
+
|
|
150
|
+
const query = (params.query ?? "").trim();
|
|
151
|
+
const k = clampTopK(params.k, 10);
|
|
152
|
+
const includeSuperseded = params.include_superseded === true;
|
|
153
|
+
const category = params.category?.trim().toLowerCase() || undefined;
|
|
154
|
+
const scopeFilter = params.scope?.trim() || undefined;
|
|
155
|
+
const tagFilter = params.tag?.trim().toLowerCase() || undefined;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
let ranked: RankedMemory[] = [];
|
|
159
|
+
if (query) {
|
|
160
|
+
ranked = queryMemoriesRanked({
|
|
161
|
+
query,
|
|
162
|
+
k,
|
|
163
|
+
category,
|
|
164
|
+
scope: scopeFilter,
|
|
165
|
+
tag: tagFilter,
|
|
166
|
+
include_superseded: includeSuperseded,
|
|
167
|
+
});
|
|
168
|
+
} else {
|
|
169
|
+
const candidates: Memory[] = includeSuperseded
|
|
170
|
+
? includeSupersededMemories(getActiveMemoriesRanked(200))
|
|
171
|
+
: getActiveMemoriesRanked(200);
|
|
172
|
+
ranked = candidates
|
|
173
|
+
.filter((m) => {
|
|
174
|
+
if (category && m.category.toLowerCase() !== category) return false;
|
|
175
|
+
if (scopeFilter && m.scope !== scopeFilter) return false;
|
|
176
|
+
if (tagFilter && !m.tags.map((t) => t.toLowerCase()).includes(tagFilter)) return false;
|
|
177
|
+
return true;
|
|
178
|
+
})
|
|
179
|
+
.slice(0, k)
|
|
180
|
+
.map((memory) => ({
|
|
181
|
+
memory,
|
|
182
|
+
score: memory.confidence * (1 + memory.hit_count * 0.1),
|
|
183
|
+
keywordRank: null,
|
|
184
|
+
semanticRank: null,
|
|
185
|
+
confidenceBoost: memory.confidence * (1 + memory.hit_count * 0.1),
|
|
186
|
+
reason: "ranked" as const,
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const hits: MemoryQueryHit[] = ranked.map((r) => ({
|
|
191
|
+
id: r.memory.id,
|
|
192
|
+
category: r.memory.category,
|
|
193
|
+
content: r.memory.content,
|
|
194
|
+
confidence: r.memory.confidence,
|
|
195
|
+
hit_count: r.memory.hit_count,
|
|
196
|
+
score: r.score,
|
|
197
|
+
reason: r.reason,
|
|
198
|
+
keyword_rank: r.keywordRank,
|
|
199
|
+
semantic_rank: r.semanticRank,
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
if (params.reinforce_hits) {
|
|
203
|
+
for (const h of hits) reinforceMemory(h.id);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const summary = hits.length === 0
|
|
207
|
+
? "No matching memories."
|
|
208
|
+
: hits.map((h) => `- [${h.id}] (${h.category}) ${h.content}`).join("\n");
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
content: [{ type: "text", text: summary }],
|
|
212
|
+
details: {
|
|
213
|
+
operation: "memory_query",
|
|
214
|
+
query,
|
|
215
|
+
k,
|
|
216
|
+
returned: hits.length,
|
|
217
|
+
hits,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
} catch (err) {
|
|
221
|
+
return {
|
|
222
|
+
content: [{ type: "text", text: `Error: memory query failed: ${(err as Error).message}` }],
|
|
223
|
+
details: { operation: "memory_query", error: (err as Error).message },
|
|
224
|
+
isError: true,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function clampTopK(value: unknown, fallback: number): number {
|
|
230
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
231
|
+
if (value < 1) return 1;
|
|
232
|
+
if (value > 50) return 50;
|
|
233
|
+
return Math.floor(value);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function includeSupersededMemories(rankedActive: Memory[]): Memory[] {
|
|
237
|
+
const adapter = _getAdapter();
|
|
238
|
+
if (!adapter) return rankedActive;
|
|
239
|
+
try {
|
|
240
|
+
const rows = adapter.prepare("SELECT * FROM memories").all();
|
|
241
|
+
return rows.map((row) => {
|
|
242
|
+
let tags: string[] = [];
|
|
243
|
+
if (typeof row["tags"] === "string") {
|
|
244
|
+
try {
|
|
245
|
+
const parsed = JSON.parse(row["tags"] as string);
|
|
246
|
+
if (Array.isArray(parsed)) tags = parsed.filter((t): t is string => typeof t === "string");
|
|
247
|
+
} catch {
|
|
248
|
+
/* leave empty */
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
seq: row["seq"] as number,
|
|
253
|
+
id: row["id"] as string,
|
|
254
|
+
category: row["category"] as string,
|
|
255
|
+
content: row["content"] as string,
|
|
256
|
+
confidence: row["confidence"] as number,
|
|
257
|
+
source_unit_type: (row["source_unit_type"] as string) ?? null,
|
|
258
|
+
source_unit_id: (row["source_unit_id"] as string) ?? null,
|
|
259
|
+
created_at: row["created_at"] as string,
|
|
260
|
+
updated_at: row["updated_at"] as string,
|
|
261
|
+
superseded_by: (row["superseded_by"] as string) ?? null,
|
|
262
|
+
hit_count: row["hit_count"] as number,
|
|
263
|
+
scope: (row["scope"] as string) ?? "project",
|
|
264
|
+
tags,
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
} catch {
|
|
268
|
+
return rankedActive;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ─── gsd_graph ──────────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
export interface GsdGraphParams {
|
|
275
|
+
mode: "build" | "query";
|
|
276
|
+
memoryId?: string;
|
|
277
|
+
depth?: number;
|
|
278
|
+
rel?: string;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export interface GraphNode {
|
|
282
|
+
id: string;
|
|
283
|
+
category: string;
|
|
284
|
+
content: string;
|
|
285
|
+
confidence: number;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export interface GraphEdge {
|
|
289
|
+
from: string;
|
|
290
|
+
to: string;
|
|
291
|
+
rel: string;
|
|
292
|
+
confidence: number;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function executeGsdGraph(params: GsdGraphParams): ToolExecutionResult {
|
|
296
|
+
if (!isDbAvailable()) return dbUnavailable("gsd_graph");
|
|
297
|
+
|
|
298
|
+
if (params.mode === "build") {
|
|
299
|
+
// The extractor emits LINK actions incrementally (Phase 4). There is no
|
|
300
|
+
// batch rebuild step to run today — ingest artifacts via `/gsd memory
|
|
301
|
+
// extract <SRC-...>` and the next extraction turn will add edges.
|
|
302
|
+
return {
|
|
303
|
+
content: [
|
|
304
|
+
{
|
|
305
|
+
type: "text",
|
|
306
|
+
text:
|
|
307
|
+
"gsd_graph build acknowledged. Graph edges are populated incrementally by memory " +
|
|
308
|
+
"extraction (including LINK actions). Use `/gsd memory extract <SRC-...>` to trigger " +
|
|
309
|
+
"extraction against a specific ingested source.",
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
details: { operation: "gsd_graph", mode: "build", built: 0 },
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (params.mode !== "query") {
|
|
317
|
+
return {
|
|
318
|
+
content: [{ type: "text", text: `Error: unknown mode "${params.mode}". Must be "build" or "query".` }],
|
|
319
|
+
details: { operation: "gsd_graph", error: "invalid_mode" },
|
|
320
|
+
isError: true,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const memoryId = params.memoryId?.trim();
|
|
325
|
+
if (!memoryId) {
|
|
326
|
+
return {
|
|
327
|
+
content: [{ type: "text", text: "Error: memoryId is required for mode=query." }],
|
|
328
|
+
details: { operation: "gsd_graph", error: "missing_memory_id" },
|
|
329
|
+
isError: true,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const graph = traverseGraph(memoryId, clampDepth(params.depth));
|
|
335
|
+
const rel = params.rel?.trim().toLowerCase() || null;
|
|
336
|
+
const edges = rel ? graph.edges.filter((e) => e.rel === rel) : graph.edges;
|
|
337
|
+
const relevantIds = new Set<string>([memoryId]);
|
|
338
|
+
for (const e of edges) {
|
|
339
|
+
relevantIds.add(e.from);
|
|
340
|
+
relevantIds.add(e.to);
|
|
341
|
+
}
|
|
342
|
+
const nodes = graph.nodes.filter((n) => relevantIds.has(n.id));
|
|
343
|
+
|
|
344
|
+
if (nodes.length === 0) {
|
|
345
|
+
return {
|
|
346
|
+
content: [{ type: "text", text: `No memory found with id ${memoryId}.` }],
|
|
347
|
+
details: { operation: "gsd_graph", mode: "query", memoryId, nodes: [], edges: [] },
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const summary = [
|
|
352
|
+
`Memory ${memoryId} — ${nodes.length} node(s), ${edges.length} edge(s).`,
|
|
353
|
+
...nodes.map((n) => ` [${n.id}] (${n.category}) ${n.content}`),
|
|
354
|
+
...edges.map((e) => ` ${e.from} --${e.rel}-> ${e.to}`),
|
|
355
|
+
].join("\n");
|
|
356
|
+
return {
|
|
357
|
+
content: [{ type: "text", text: summary }],
|
|
358
|
+
details: {
|
|
359
|
+
operation: "gsd_graph",
|
|
360
|
+
mode: "query",
|
|
361
|
+
memoryId,
|
|
362
|
+
nodes: nodes.map((n) => ({ id: n.id, category: n.category, content: n.content })),
|
|
363
|
+
edges: edges.map((e) => ({ from: e.from, to: e.to, rel: e.rel })),
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
} catch (err) {
|
|
367
|
+
return {
|
|
368
|
+
content: [{ type: "text", text: `Error: graph query failed: ${(err as Error).message}` }],
|
|
369
|
+
details: { operation: "gsd_graph", error: (err as Error).message },
|
|
370
|
+
isError: true,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function clampDepth(value: unknown): number {
|
|
376
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 1;
|
|
377
|
+
if (value < 0) return 0;
|
|
378
|
+
if (value > 5) return 5;
|
|
379
|
+
return Math.floor(value);
|
|
380
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
|
|
13
|
+
import {
|
|
14
|
+
getSlice,
|
|
15
|
+
getSliceTasks,
|
|
16
|
+
isDbAvailable,
|
|
17
|
+
transaction,
|
|
18
|
+
updateSliceStatus,
|
|
19
|
+
updateTaskStatus,
|
|
20
|
+
} from "../gsd-db.js";
|
|
21
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Input parameters for {@link handleSkipSlice}.
|
|
25
|
+
*
|
|
26
|
+
* - `milestoneId` / `sliceId` identify the target slice.
|
|
27
|
+
* - `reason` is a free-form note surfaced in the MCP response; optional
|
|
28
|
+
* because the caller (e.g. rethink flow) may not have a structured reason.
|
|
29
|
+
*/
|
|
30
|
+
export interface SkipSliceParams {
|
|
31
|
+
milestoneId: string;
|
|
32
|
+
sliceId: string;
|
|
33
|
+
reason?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Stable machine-readable error codes for {@link SkipSliceResult.error}.
|
|
38
|
+
* Keep in sync with the wrapper in bootstrap/db-tools.ts.
|
|
39
|
+
*/
|
|
40
|
+
export type SkipSliceErrorCode = "slice_not_found" | "already_complete";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Result of a {@link handleSkipSlice} call.
|
|
44
|
+
*
|
|
45
|
+
* - `tasksSkipped` — count of tasks whose status was cascaded to "skipped".
|
|
46
|
+
* Zero is a valid success (slice had no non-closed tasks).
|
|
47
|
+
* - `wasAlreadySkipped` — true when the slice was in "skipped" status on
|
|
48
|
+
* entry; callers can use this to distinguish first-skip from re-skip.
|
|
49
|
+
* - `error` / `errorCode` — set together for recoverable validation failures
|
|
50
|
+
* (unknown slice, slice already complete). Both absent on success. DB
|
|
51
|
+
* errors propagate as thrown exceptions and should be caught by the caller.
|
|
52
|
+
*/
|
|
53
|
+
export interface SkipSliceResult {
|
|
54
|
+
milestoneId: string;
|
|
55
|
+
sliceId: string;
|
|
56
|
+
tasksSkipped: number;
|
|
57
|
+
wasAlreadySkipped: boolean;
|
|
58
|
+
reason?: string;
|
|
59
|
+
error?: string;
|
|
60
|
+
errorCode?: SkipSliceErrorCode;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Mark a slice as "skipped" and cascade the skip to every non-closed task in
|
|
65
|
+
* that slice. Runs as a single transaction so slice status and task statuses
|
|
66
|
+
* are always consistent.
|
|
67
|
+
*
|
|
68
|
+
* Behaviour summary:
|
|
69
|
+
* - Unknown slice → returns {@link SkipSliceResult} with `error`.
|
|
70
|
+
* - Slice already complete/done → returns `error` (cannot un-complete).
|
|
71
|
+
* - Slice already skipped → still cascades leftover non-closed tasks
|
|
72
|
+
* (heals inconsistent historical state from projects that ran older
|
|
73
|
+
* versions before the #4375 cascade fix).
|
|
74
|
+
* - Tasks in closed status (complete/done/skipped) are never downgraded.
|
|
75
|
+
*/
|
|
76
|
+
export function handleSkipSlice(params: SkipSliceParams): SkipSliceResult {
|
|
77
|
+
const base: SkipSliceResult = {
|
|
78
|
+
milestoneId: params.milestoneId,
|
|
79
|
+
sliceId: params.sliceId,
|
|
80
|
+
tasksSkipped: 0,
|
|
81
|
+
wasAlreadySkipped: false,
|
|
82
|
+
reason: params.reason,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Fail loudly on a closed DB so a `null` from getSlice() inside the
|
|
86
|
+
// transaction unambiguously means "slice not found", never "DB unavailable".
|
|
87
|
+
// The MCP wrapper in bootstrap/db-tools.ts runs ensureDbOpen() before calling
|
|
88
|
+
// this helper; this guard protects direct callers (tests, future code).
|
|
89
|
+
if (!isDbAvailable()) {
|
|
90
|
+
throw new Error("handleSkipSlice: GSD database is not available");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ────
|
|
94
|
+
let guardError: string | null = null;
|
|
95
|
+
let guardCode: SkipSliceErrorCode | null = null;
|
|
96
|
+
let wasAlreadySkipped = false;
|
|
97
|
+
let tasksSkipped = 0;
|
|
98
|
+
|
|
99
|
+
transaction(() => {
|
|
100
|
+
const slice = getSlice(params.milestoneId, params.sliceId);
|
|
101
|
+
if (!slice) {
|
|
102
|
+
guardError = `Slice ${params.sliceId} not found in milestone ${params.milestoneId}`;
|
|
103
|
+
guardCode = "slice_not_found";
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (slice.status === "complete" || slice.status === "done") {
|
|
107
|
+
guardError = `Slice ${params.sliceId} is already complete — cannot skip.`;
|
|
108
|
+
guardCode = "already_complete";
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
wasAlreadySkipped = slice.status === "skipped";
|
|
113
|
+
if (!wasAlreadySkipped) {
|
|
114
|
+
updateSliceStatus(params.milestoneId, params.sliceId, "skipped");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Cascade: mark every non-closed task as skipped so milestone completion
|
|
118
|
+
// doesn't trip the deep-task guard (#4375). Closed tasks (complete/done/
|
|
119
|
+
// skipped) are left untouched — we never downgrade.
|
|
120
|
+
const tasks = getSliceTasks(params.milestoneId, params.sliceId);
|
|
121
|
+
for (const task of tasks) {
|
|
122
|
+
if (!isClosedStatus(task.status)) {
|
|
123
|
+
updateTaskStatus(params.milestoneId, params.sliceId, task.id, "skipped");
|
|
124
|
+
tasksSkipped++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (guardError) {
|
|
130
|
+
return { ...base, error: guardError, errorCode: guardCode ?? undefined };
|
|
131
|
+
}
|
|
132
|
+
return { ...base, tasksSkipped, wasAlreadySkipped };
|
|
133
|
+
}
|
|
@@ -54,7 +54,9 @@ export type LogComponent =
|
|
|
54
54
|
| "registry" // Rule registry hook state
|
|
55
55
|
| "renderer" // Markdown renderer and projections
|
|
56
56
|
| "safety" // LLM safety harness
|
|
57
|
-
| "ecosystem"
|
|
57
|
+
| "ecosystem" // GSD ecosystem extension loader and dispatch
|
|
58
|
+
| "memory-embeddings" // Memory layer embedding generation
|
|
59
|
+
| "memory-ingest"; // Memory layer ingestion pipeline
|
|
58
60
|
|
|
59
61
|
export interface LogEntry {
|
|
60
62
|
ts: string;
|
|
File without changes
|
|
File without changes
|