gsd-pi 2.75.0-dev.b6ad8c5f7 → 2.75.0-dev.e41b70b10
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/resources/extensions/gsd/auto/phases.js +2 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +22 -1
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +8 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +11 -11
- package/dist/resources/extensions/gsd/auto-model-selection.js +3 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +19 -9
- package/dist/resources/extensions/gsd/auto-worktree.js +16 -1
- 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 -92
- 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/doctor-git-checks.js +22 -2
- 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/pre-execution-checks.js +12 -8
- package/dist/resources/extensions/gsd/prompts/add-tests.md +1 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -0
- package/dist/resources/extensions/gsd/tools/memory-tools.js +306 -0
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +14 -0
- package/dist/resources/extensions/search-the-web/command-search-provider.js +4 -1
- package/dist/resources/extensions/search-the-web/native-search.js +13 -2
- 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 +10 -10
- 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 +10 -10
- 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 +207 -71
- 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 +346 -1
- package/packages/mcp-server/src/workflow-tools.ts +228 -75
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-ai/dist/index.d.ts +1 -0
- package/packages/pi-ai/dist/index.d.ts.map +1 -1
- package/packages/pi-ai/dist/index.js +1 -0
- package/packages/pi-ai/dist/index.js.map +1 -1
- package/packages/pi-ai/dist/providers/api-family.d.ts +27 -0
- package/packages/pi-ai/dist/providers/api-family.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/api-family.js +47 -0
- package/packages/pi-ai/dist/providers/api-family.js.map +1 -0
- package/packages/pi-ai/dist/providers/api-family.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/api-family.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/api-family.test.js +101 -0
- package/packages/pi-ai/dist/providers/api-family.test.js.map +1 -0
- package/packages/pi-ai/src/index.ts +1 -0
- package/packages/pi-ai/src/providers/api-family.test.ts +129 -0
- package/packages/pi-ai/src/providers/api-family.ts +57 -0
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/cli/args.d.ts +6 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +14 -4
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/cli/args.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/cli/args.test.js +38 -0
- package/packages/pi-coding-agent/dist/cli/args.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +4 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts +10 -0
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +7 -1
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +3 -0
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.test.ts +44 -0
- package/packages/pi-coding-agent/src/cli/args.ts +21 -6
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +4 -1
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -2
- package/packages/pi-coding-agent/src/core/retry-handler.ts +4 -1
- package/packages/pi-coding-agent/src/core/sdk.ts +17 -1
- package/packages/pi-coding-agent/src/main.ts +4 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -10
- package/src/resources/extensions/gsd/auto/phases.ts +3 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +25 -1
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +15 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +21 -7
- package/src/resources/extensions/gsd/auto-model-selection.ts +3 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +33 -9
- package/src/resources/extensions/gsd/auto-worktree.ts +16 -1
- 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 -131
- 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/doctor-git-checks.ts +23 -2
- 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/pre-execution-checks.ts +12 -8
- package/src/resources/extensions/gsd/prompts/add-tests.md +1 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -0
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +49 -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/integration/doctor-git-symlink-cwd.test.ts +79 -0
- 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/pre-execution-checks.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +132 -8
- package/src/resources/extensions/gsd/tests/prompts-no-gitignored-test-refs.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +97 -0
- package/src/resources/extensions/gsd/tools/memory-tools.ts +380 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +14 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +3 -1
- package/src/resources/extensions/search-the-web/command-search-provider.ts +4 -1
- package/src/resources/extensions/search-the-web/native-search.ts +13 -3
- package/src/resources/extensions/gsd/tests/onboarding-handler-loader.test.ts +0 -41
- /package/dist/web/standalone/.next/static/{J2z3GMC9QtSLr7gyoM38c → By_yegSJ-AA1OP0QjYbSl}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{J2z3GMC9QtSLr7gyoM38c → By_yegSJ-AA1OP0QjYbSl}/_ssgManifest.js +0 -0
|
@@ -15,7 +15,10 @@ import {
|
|
|
15
15
|
markMemoryUnitProcessed,
|
|
16
16
|
decayMemoriesBefore,
|
|
17
17
|
supersedeLowestRankedMemories,
|
|
18
|
+
deleteMemoryEmbedding,
|
|
19
|
+
deleteMemoryRelationsFor,
|
|
18
20
|
} from './gsd-db.js';
|
|
21
|
+
import { createMemoryRelation, isValidRelation } from './memory-relations.js';
|
|
19
22
|
|
|
20
23
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
21
24
|
|
|
@@ -31,6 +34,8 @@ export interface Memory {
|
|
|
31
34
|
updated_at: string;
|
|
32
35
|
superseded_by: string | null;
|
|
33
36
|
hit_count: number;
|
|
37
|
+
scope: string;
|
|
38
|
+
tags: string[];
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
export type MemoryActionCreate = {
|
|
@@ -38,6 +43,8 @@ export type MemoryActionCreate = {
|
|
|
38
43
|
category: string;
|
|
39
44
|
content: string;
|
|
40
45
|
confidence?: number;
|
|
46
|
+
scope?: string;
|
|
47
|
+
tags?: string[];
|
|
41
48
|
};
|
|
42
49
|
|
|
43
50
|
export type MemoryActionUpdate = {
|
|
@@ -58,11 +65,20 @@ export type MemoryActionSupersede = {
|
|
|
58
65
|
superseded_by: string;
|
|
59
66
|
};
|
|
60
67
|
|
|
68
|
+
export type MemoryActionLink = {
|
|
69
|
+
action: 'LINK';
|
|
70
|
+
from: string;
|
|
71
|
+
to: string;
|
|
72
|
+
rel: string;
|
|
73
|
+
confidence?: number;
|
|
74
|
+
};
|
|
75
|
+
|
|
61
76
|
export type MemoryAction =
|
|
62
77
|
| MemoryActionCreate
|
|
63
78
|
| MemoryActionUpdate
|
|
64
79
|
| MemoryActionReinforce
|
|
65
|
-
| MemoryActionSupersede
|
|
80
|
+
| MemoryActionSupersede
|
|
81
|
+
| MemoryActionLink;
|
|
66
82
|
|
|
67
83
|
// ─── Category Display Order ─────────────────────────────────────────────────
|
|
68
84
|
|
|
@@ -90,9 +106,21 @@ function rowToMemory(row: Record<string, unknown>): Memory {
|
|
|
90
106
|
updated_at: row['updated_at'] as string,
|
|
91
107
|
superseded_by: (row['superseded_by'] as string) ?? null,
|
|
92
108
|
hit_count: row['hit_count'] as number,
|
|
109
|
+
scope: (row['scope'] as string) ?? 'project',
|
|
110
|
+
tags: parseTags(row['tags']),
|
|
93
111
|
};
|
|
94
112
|
}
|
|
95
113
|
|
|
114
|
+
function parseTags(raw: unknown): string[] {
|
|
115
|
+
if (typeof raw !== 'string' || raw.length === 0) return [];
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(raw);
|
|
118
|
+
return Array.isArray(parsed) ? parsed.filter((t): t is string => typeof t === 'string') : [];
|
|
119
|
+
} catch {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
96
124
|
// ─── Query Functions ────────────────────────────────────────────────────────
|
|
97
125
|
|
|
98
126
|
/**
|
|
@@ -134,6 +162,280 @@ export function getActiveMemoriesRanked(limit = 30): Memory[] {
|
|
|
134
162
|
}
|
|
135
163
|
}
|
|
136
164
|
|
|
165
|
+
// ─── Hybrid query (keyword FTS + optional semantic) ─────────────────────────
|
|
166
|
+
|
|
167
|
+
export interface QueryMemoriesFilters {
|
|
168
|
+
category?: string;
|
|
169
|
+
scope?: string;
|
|
170
|
+
tag?: string;
|
|
171
|
+
include_superseded?: boolean;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface QueryMemoriesOptions extends QueryMemoriesFilters {
|
|
175
|
+
query: string;
|
|
176
|
+
k?: number;
|
|
177
|
+
/**
|
|
178
|
+
* Optional query-side embedding. When provided and embeddings exist in the
|
|
179
|
+
* DB, results are fused with cosine similarity via reciprocal-rank-fusion.
|
|
180
|
+
*/
|
|
181
|
+
queryVector?: Float32Array | null;
|
|
182
|
+
/** RRF fusion constant (default 60). */
|
|
183
|
+
rrfK?: number;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface RankedMemory {
|
|
187
|
+
memory: Memory;
|
|
188
|
+
score: number;
|
|
189
|
+
keywordRank: number | null;
|
|
190
|
+
semanticRank: number | null;
|
|
191
|
+
confidenceBoost: number;
|
|
192
|
+
reason: 'keyword' | 'semantic' | 'both' | 'ranked';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function queryMemoriesRanked(opts: QueryMemoriesOptions): RankedMemory[] {
|
|
196
|
+
if (!isDbAvailable()) return [];
|
|
197
|
+
const adapter = _getAdapter();
|
|
198
|
+
if (!adapter) return [];
|
|
199
|
+
|
|
200
|
+
const k = clampLimit(opts.k, 10);
|
|
201
|
+
const rrfK = opts.rrfK ?? 60;
|
|
202
|
+
const activeClause = opts.include_superseded === true ? '' : 'WHERE superseded_by IS NULL';
|
|
203
|
+
const trimmedQuery = (opts.query ?? '').trim();
|
|
204
|
+
|
|
205
|
+
// 1) Keyword hits — try FTS5 first, fall back to LIKE when unavailable.
|
|
206
|
+
const keywordHits = trimmedQuery ? keywordSearch(adapter, trimmedQuery, activeClause, 50) : [];
|
|
207
|
+
|
|
208
|
+
// 2) Semantic hits — cosine over memory_embeddings. Requires opts.queryVector.
|
|
209
|
+
const semanticHits = opts.queryVector
|
|
210
|
+
? semanticSearch(adapter, opts.queryVector, activeClause, 50)
|
|
211
|
+
: [];
|
|
212
|
+
|
|
213
|
+
if (keywordHits.length === 0 && semanticHits.length === 0 && !trimmedQuery) {
|
|
214
|
+
// No query at all — fall back to the existing ranked-by-score listing.
|
|
215
|
+
return getActiveMemoriesRanked(k).map((memory) => ({
|
|
216
|
+
memory,
|
|
217
|
+
score: memory.confidence * (1 + memory.hit_count * 0.1),
|
|
218
|
+
keywordRank: null,
|
|
219
|
+
semanticRank: null,
|
|
220
|
+
confidenceBoost: memory.confidence * (1 + memory.hit_count * 0.1),
|
|
221
|
+
reason: 'ranked' as const,
|
|
222
|
+
})).filter((hit) => passesFilters(hit.memory, opts));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 3) Reciprocal rank fusion — each hit contributes 1/(rrfK + rank).
|
|
226
|
+
const fused = new Map<string, { memory: Memory; kwRank: number | null; semRank: number | null; score: number }>();
|
|
227
|
+
|
|
228
|
+
for (let i = 0; i < keywordHits.length; i++) {
|
|
229
|
+
const hit = keywordHits[i];
|
|
230
|
+
const existing = fused.get(hit.id);
|
|
231
|
+
const rrf = 1 / (rrfK + i + 1);
|
|
232
|
+
if (existing) {
|
|
233
|
+
existing.kwRank = i + 1;
|
|
234
|
+
existing.score += rrf;
|
|
235
|
+
} else {
|
|
236
|
+
fused.set(hit.id, { memory: hit, kwRank: i + 1, semRank: null, score: rrf });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
for (let i = 0; i < semanticHits.length; i++) {
|
|
241
|
+
const hit = semanticHits[i];
|
|
242
|
+
const existing = fused.get(hit.id);
|
|
243
|
+
const rrf = 1 / (rrfK + i + 1);
|
|
244
|
+
if (existing) {
|
|
245
|
+
existing.semRank = i + 1;
|
|
246
|
+
existing.score += rrf;
|
|
247
|
+
} else {
|
|
248
|
+
fused.set(hit.id, { memory: hit, kwRank: null, semRank: i + 1, score: rrf });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 4) Apply filters + confidence boost, then sort.
|
|
253
|
+
const ranked: RankedMemory[] = [];
|
|
254
|
+
for (const entry of fused.values()) {
|
|
255
|
+
if (!passesFilters(entry.memory, opts)) continue;
|
|
256
|
+
const boost = entry.memory.confidence * (1 + entry.memory.hit_count * 0.1);
|
|
257
|
+
const reason: RankedMemory['reason'] =
|
|
258
|
+
entry.kwRank != null && entry.semRank != null
|
|
259
|
+
? 'both'
|
|
260
|
+
: entry.kwRank != null
|
|
261
|
+
? 'keyword'
|
|
262
|
+
: 'semantic';
|
|
263
|
+
ranked.push({
|
|
264
|
+
memory: entry.memory,
|
|
265
|
+
score: entry.score * boost,
|
|
266
|
+
keywordRank: entry.kwRank,
|
|
267
|
+
semanticRank: entry.semRank,
|
|
268
|
+
confidenceBoost: boost,
|
|
269
|
+
reason,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
ranked.sort((a, b) => b.score - a.score);
|
|
274
|
+
return ranked.slice(0, k);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function clampLimit(value: unknown, fallback: number): number {
|
|
278
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return fallback;
|
|
279
|
+
if (value < 1) return 1;
|
|
280
|
+
if (value > 100) return 100;
|
|
281
|
+
return Math.floor(value);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function passesFilters(memory: Memory, filters: QueryMemoriesFilters): boolean {
|
|
285
|
+
if (filters.category && memory.category.toLowerCase() !== filters.category.toLowerCase()) return false;
|
|
286
|
+
if (filters.scope && memory.scope !== filters.scope) return false;
|
|
287
|
+
if (filters.tag) {
|
|
288
|
+
const needle = filters.tag.toLowerCase();
|
|
289
|
+
if (!memory.tags.map((t) => t.toLowerCase()).includes(needle)) return false;
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function keywordSearch(
|
|
295
|
+
adapter: NonNullable<ReturnType<typeof _getAdapter>>,
|
|
296
|
+
rawQuery: string,
|
|
297
|
+
activeClause: string,
|
|
298
|
+
limit: number,
|
|
299
|
+
): Memory[] {
|
|
300
|
+
const ftsAvailable = isFtsAvailable(adapter);
|
|
301
|
+
if (ftsAvailable) {
|
|
302
|
+
try {
|
|
303
|
+
const matchExpr = toFtsMatchExpr(rawQuery);
|
|
304
|
+
if (!matchExpr) return [];
|
|
305
|
+
const activePart = activeClause ? `AND m.${activeClause.replace(/^WHERE\s+/i, '')}` : '';
|
|
306
|
+
const rows = adapter.prepare(
|
|
307
|
+
`SELECT m.*
|
|
308
|
+
FROM memories_fts f
|
|
309
|
+
JOIN memories m ON m.seq = f.rowid
|
|
310
|
+
WHERE memories_fts MATCH :match
|
|
311
|
+
${activePart}
|
|
312
|
+
ORDER BY bm25(memories_fts)
|
|
313
|
+
LIMIT :limit`,
|
|
314
|
+
).all({ ':match': matchExpr, ':limit': limit });
|
|
315
|
+
return rows.map(rowToMemory);
|
|
316
|
+
} catch {
|
|
317
|
+
// fall through to LIKE
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// LIKE fallback — scans the candidate pool.
|
|
322
|
+
const terms = rawQuery
|
|
323
|
+
.toLowerCase()
|
|
324
|
+
.split(/[^a-z0-9_]+/)
|
|
325
|
+
.filter((t) => t.length >= 2);
|
|
326
|
+
if (terms.length === 0) return [];
|
|
327
|
+
|
|
328
|
+
const rows = adapter.prepare(`SELECT * FROM memories ${activeClause}`).all();
|
|
329
|
+
const scored: Array<{ memory: Memory; score: number }> = [];
|
|
330
|
+
for (const row of rows) {
|
|
331
|
+
const memory = rowToMemory(row);
|
|
332
|
+
const lower = memory.content.toLowerCase();
|
|
333
|
+
let score = 0;
|
|
334
|
+
for (const term of terms) {
|
|
335
|
+
const idx = lower.indexOf(term);
|
|
336
|
+
if (idx === -1) continue;
|
|
337
|
+
score += 1 + (term.length >= 5 ? 0.5 : 0);
|
|
338
|
+
}
|
|
339
|
+
if (score > 0) scored.push({ memory, score });
|
|
340
|
+
}
|
|
341
|
+
scored.sort((a, b) => b.score - a.score);
|
|
342
|
+
return scored.slice(0, limit).map((s) => s.memory);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function isFtsAvailable(adapter: NonNullable<ReturnType<typeof _getAdapter>>): boolean {
|
|
346
|
+
try {
|
|
347
|
+
const row = adapter
|
|
348
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'")
|
|
349
|
+
.get();
|
|
350
|
+
return !!row;
|
|
351
|
+
} catch {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function toFtsMatchExpr(query: string): string | null {
|
|
357
|
+
// Build a tolerant AND expression: quote each bare term with a trailing *.
|
|
358
|
+
const tokens = query
|
|
359
|
+
.toLowerCase()
|
|
360
|
+
.split(/[^a-z0-9_]+/)
|
|
361
|
+
.filter((t) => t.length >= 2)
|
|
362
|
+
.slice(0, 8);
|
|
363
|
+
if (tokens.length === 0) return null;
|
|
364
|
+
return tokens.map((t) => `"${t.replace(/"/g, '""')}"*`).join(' OR ');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function semanticSearch(
|
|
368
|
+
adapter: NonNullable<ReturnType<typeof _getAdapter>>,
|
|
369
|
+
queryVector: Float32Array,
|
|
370
|
+
activeClause: string,
|
|
371
|
+
limit: number,
|
|
372
|
+
): Memory[] {
|
|
373
|
+
try {
|
|
374
|
+
const rows = adapter
|
|
375
|
+
.prepare(
|
|
376
|
+
`SELECT m.*, e.vector as embedding_vector, e.dim as embedding_dim
|
|
377
|
+
FROM memories m
|
|
378
|
+
JOIN memory_embeddings e ON e.memory_id = m.id
|
|
379
|
+
${activeClause}`,
|
|
380
|
+
)
|
|
381
|
+
.all();
|
|
382
|
+
|
|
383
|
+
const scored: Array<{ memory: Memory; sim: number }> = [];
|
|
384
|
+
for (const row of rows) {
|
|
385
|
+
const dim = row['embedding_dim'] as number;
|
|
386
|
+
if (dim !== queryVector.length) continue;
|
|
387
|
+
const vector = unpackVector(row['embedding_vector'], dim);
|
|
388
|
+
if (!vector) continue;
|
|
389
|
+
const sim = cosine(queryVector, vector);
|
|
390
|
+
if (sim <= 0) continue;
|
|
391
|
+
scored.push({ memory: rowToMemory(row), sim });
|
|
392
|
+
}
|
|
393
|
+
scored.sort((a, b) => b.sim - a.sim);
|
|
394
|
+
return scored.slice(0, limit).map((s) => s.memory);
|
|
395
|
+
} catch {
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function unpackVector(blob: unknown, dim: number): Float32Array | null {
|
|
401
|
+
if (!blob) return null;
|
|
402
|
+
try {
|
|
403
|
+
let view: Uint8Array | null = null;
|
|
404
|
+
if (blob instanceof Float32Array) return blob;
|
|
405
|
+
if (blob instanceof Uint8Array) view = blob;
|
|
406
|
+
else if (blob instanceof ArrayBuffer) view = new Uint8Array(blob);
|
|
407
|
+
else if ((blob as Buffer).buffer && (blob as Buffer).byteLength != null) {
|
|
408
|
+
const buf = blob as Buffer;
|
|
409
|
+
view = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
410
|
+
} else if (Array.isArray(blob)) {
|
|
411
|
+
return new Float32Array(blob as number[]);
|
|
412
|
+
}
|
|
413
|
+
if (!view || view.byteLength % 4 !== 0) return null;
|
|
414
|
+
const aligned = new ArrayBuffer(view.byteLength);
|
|
415
|
+
new Uint8Array(aligned).set(view);
|
|
416
|
+
const f32 = new Float32Array(aligned);
|
|
417
|
+
return f32.length === dim ? f32 : null;
|
|
418
|
+
} catch {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function cosine(a: Float32Array, b: Float32Array): number {
|
|
424
|
+
if (a.length === 0 || a.length !== b.length) return 0;
|
|
425
|
+
let dot = 0;
|
|
426
|
+
let na = 0;
|
|
427
|
+
let nb = 0;
|
|
428
|
+
for (let i = 0; i < a.length; i++) {
|
|
429
|
+
const x = a[i];
|
|
430
|
+
const y = b[i];
|
|
431
|
+
dot += x * y;
|
|
432
|
+
na += x * x;
|
|
433
|
+
nb += y * y;
|
|
434
|
+
}
|
|
435
|
+
if (na === 0 || nb === 0) return 0;
|
|
436
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
437
|
+
}
|
|
438
|
+
|
|
137
439
|
/**
|
|
138
440
|
* Generate the next memory ID: MEM + zero-padded 3-digit from MAX(seq).
|
|
139
441
|
* Returns MEM001 if no memories exist.
|
|
@@ -173,6 +475,8 @@ export function createMemory(fields: {
|
|
|
173
475
|
confidence?: number;
|
|
174
476
|
source_unit_type?: string;
|
|
175
477
|
source_unit_id?: string;
|
|
478
|
+
scope?: string;
|
|
479
|
+
tags?: string[];
|
|
176
480
|
}): string | null {
|
|
177
481
|
if (!isDbAvailable()) return null;
|
|
178
482
|
const adapter = _getAdapter();
|
|
@@ -191,6 +495,8 @@ export function createMemory(fields: {
|
|
|
191
495
|
sourceUnitId: fields.source_unit_id ?? null,
|
|
192
496
|
createdAt: now,
|
|
193
497
|
updatedAt: now,
|
|
498
|
+
scope: fields.scope ?? 'project',
|
|
499
|
+
tags: fields.tags ?? [],
|
|
194
500
|
});
|
|
195
501
|
// Derive the real ID from the assigned seq (SELECT is still fine via adapter)
|
|
196
502
|
const row = adapter.prepare('SELECT seq FROM memories WHERE id = :id').get({ ':id': placeholder });
|
|
@@ -285,11 +591,12 @@ export function markUnitProcessed(unitKey: string, activityFile: string): boolea
|
|
|
285
591
|
/**
|
|
286
592
|
* Reduce confidence for memories not updated within the last N processed units.
|
|
287
593
|
* "Stale" = updated_at is older than the Nth most recent processed_at.
|
|
594
|
+
* Returns the number of decayed memory IDs for observability.
|
|
288
595
|
*/
|
|
289
|
-
export function decayStaleMemories(thresholdUnits = 20):
|
|
290
|
-
if (!isDbAvailable()) return;
|
|
596
|
+
export function decayStaleMemories(thresholdUnits = 20): string[] {
|
|
597
|
+
if (!isDbAvailable()) return [];
|
|
291
598
|
const adapter = _getAdapter();
|
|
292
|
-
if (!adapter) return;
|
|
599
|
+
if (!adapter) return [];
|
|
293
600
|
|
|
294
601
|
try {
|
|
295
602
|
// Find the timestamp of the Nth most recent processed unit (read-only SELECT)
|
|
@@ -299,17 +606,24 @@ export function decayStaleMemories(thresholdUnits = 20): void {
|
|
|
299
606
|
LIMIT 1 OFFSET :offset`,
|
|
300
607
|
).get({ ':offset': thresholdUnits - 1 });
|
|
301
608
|
|
|
302
|
-
if (!row) return; // not enough processed units yet
|
|
609
|
+
if (!row) return []; // not enough processed units yet
|
|
303
610
|
|
|
304
611
|
const cutoff = row['processed_at'] as string;
|
|
612
|
+
const affected = adapter.prepare(
|
|
613
|
+
`SELECT id FROM memories
|
|
614
|
+
WHERE superseded_by IS NULL AND updated_at < :cutoff AND confidence > 0.1`,
|
|
615
|
+
).all({ ':cutoff': cutoff }).map((r) => r['id'] as string);
|
|
616
|
+
|
|
305
617
|
decayMemoriesBefore(cutoff, new Date().toISOString());
|
|
618
|
+
return affected;
|
|
306
619
|
} catch {
|
|
307
|
-
|
|
620
|
+
return [];
|
|
308
621
|
}
|
|
309
622
|
}
|
|
310
623
|
|
|
311
624
|
/**
|
|
312
|
-
* Supersede lowest-ranked memories when count exceeds cap.
|
|
625
|
+
* Supersede lowest-ranked memories when count exceeds cap. Cascades to the
|
|
626
|
+
* embedding and relation rows so those tables don't grow unboundedly.
|
|
313
627
|
*/
|
|
314
628
|
export function enforceMemoryCap(max = 50): void {
|
|
315
629
|
if (!isDbAvailable()) return;
|
|
@@ -324,7 +638,21 @@ export function enforceMemoryCap(max = 50): void {
|
|
|
324
638
|
if (count <= max) return;
|
|
325
639
|
|
|
326
640
|
const excess = count - max;
|
|
641
|
+
// Capture the about-to-be-superseded IDs first so we can cascade cleanup.
|
|
642
|
+
const victims = adapter.prepare(
|
|
643
|
+
`SELECT id FROM memories
|
|
644
|
+
WHERE superseded_by IS NULL
|
|
645
|
+
ORDER BY (confidence * (1.0 + hit_count * 0.1)) ASC
|
|
646
|
+
LIMIT :limit`,
|
|
647
|
+
).all({ ':limit': excess }).map((row) => row['id'] as string);
|
|
648
|
+
|
|
327
649
|
supersedeLowestRankedMemories(excess, new Date().toISOString());
|
|
650
|
+
|
|
651
|
+
if (victims.length === 0) return;
|
|
652
|
+
for (const id of victims) {
|
|
653
|
+
try { deleteMemoryEmbedding(id); } catch { /* non-fatal */ }
|
|
654
|
+
try { deleteMemoryRelationsFor(id); } catch { /* non-fatal */ }
|
|
655
|
+
}
|
|
328
656
|
} catch {
|
|
329
657
|
// non-fatal
|
|
330
658
|
}
|
|
@@ -354,6 +682,8 @@ export function applyMemoryActions(
|
|
|
354
682
|
confidence: action.confidence,
|
|
355
683
|
source_unit_type: unitType,
|
|
356
684
|
source_unit_id: unitId,
|
|
685
|
+
scope: action.scope,
|
|
686
|
+
tags: action.tags,
|
|
357
687
|
});
|
|
358
688
|
break;
|
|
359
689
|
case 'UPDATE':
|
|
@@ -365,6 +695,9 @@ export function applyMemoryActions(
|
|
|
365
695
|
case 'SUPERSEDE':
|
|
366
696
|
supersedeMemory(action.id, action.superseded_by);
|
|
367
697
|
break;
|
|
698
|
+
case 'LINK':
|
|
699
|
+
applyLinkAction(action);
|
|
700
|
+
break;
|
|
368
701
|
}
|
|
369
702
|
}
|
|
370
703
|
enforceMemoryCap();
|
|
@@ -374,6 +707,17 @@ export function applyMemoryActions(
|
|
|
374
707
|
}
|
|
375
708
|
}
|
|
376
709
|
|
|
710
|
+
// ─── LINK action ────────────────────────────────────────────────────────────
|
|
711
|
+
|
|
712
|
+
function applyLinkAction(action: MemoryActionLink): void {
|
|
713
|
+
try {
|
|
714
|
+
if (!isValidRelation(action.rel)) return;
|
|
715
|
+
createMemoryRelation(action.from, action.to, action.rel, action.confidence);
|
|
716
|
+
} catch {
|
|
717
|
+
// Link failures should never break memory extraction.
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
377
721
|
// ─── Prompt Formatting ──────────────────────────────────────────────────────
|
|
378
722
|
|
|
379
723
|
/**
|
|
@@ -403,10 +403,13 @@ function getExpectedOutputsUpTo(tasks: TaskRow[], taskIndex: number): Set<string
|
|
|
403
403
|
/**
|
|
404
404
|
* Check that all files referenced in task.inputs either:
|
|
405
405
|
* 1. Exist on disk, OR
|
|
406
|
-
* 2. Are in a prior task's expected_output
|
|
406
|
+
* 2. Are in a prior task's expected_output, OR
|
|
407
|
+
* 3. Are in the current task's own expected_output — the task produces them,
|
|
408
|
+
* so they don't need to pre-exist (#4459, mirroring the exemption #3626
|
|
409
|
+
* introduced for task.files).
|
|
407
410
|
*
|
|
408
|
-
* task.files ("files likely touched") is excluded
|
|
409
|
-
* files the task will create
|
|
411
|
+
* task.files ("files likely touched") is excluded entirely from this check —
|
|
412
|
+
* it intentionally includes files the task will create (#3626).
|
|
410
413
|
*
|
|
411
414
|
* All paths are normalized before comparison to ensure ./src/a.ts matches src/a.ts.
|
|
412
415
|
*/
|
|
@@ -419,6 +422,7 @@ export function checkFilePathConsistency(
|
|
|
419
422
|
for (let i = 0; i < tasks.length; i++) {
|
|
420
423
|
const task = tasks[i];
|
|
421
424
|
const priorOutputs = getExpectedOutputsUpTo(tasks, i);
|
|
425
|
+
const ownOutputs = new Set<string>(task.expected_output.map(normalizeFilePath));
|
|
422
426
|
const filesToCheck = [...task.inputs];
|
|
423
427
|
|
|
424
428
|
for (const file of filesToCheck) {
|
|
@@ -436,23 +440,23 @@ export function checkFilePathConsistency(
|
|
|
436
440
|
|
|
437
441
|
// Check if file is in prior expected outputs (priorOutputs already normalized)
|
|
438
442
|
const inPriorOutputs = priorOutputs.has(normalizedFile);
|
|
443
|
+
const inOwnOutputs = ownOutputs.has(normalizedFile);
|
|
439
444
|
|
|
440
445
|
// Directory inputs are satisfied when something produces a file beneath
|
|
441
446
|
// them — either a prior task or the current task itself.
|
|
442
447
|
let directorySatisfied = false;
|
|
443
|
-
if (!existsOnDisk && !inPriorOutputs && isDirectoryReference(file)) {
|
|
444
|
-
const sameTaskOutputs = task.expected_output.map(normalizeFilePath);
|
|
448
|
+
if (!existsOnDisk && !inPriorOutputs && !inOwnOutputs && isDirectoryReference(file)) {
|
|
445
449
|
directorySatisfied =
|
|
446
450
|
anyOutputUnderDirectory(normalizedFile, priorOutputs) ||
|
|
447
|
-
anyOutputUnderDirectory(normalizedFile,
|
|
451
|
+
anyOutputUnderDirectory(normalizedFile, ownOutputs);
|
|
448
452
|
}
|
|
449
453
|
|
|
450
|
-
if (!existsOnDisk && !inPriorOutputs && !directorySatisfied) {
|
|
454
|
+
if (!existsOnDisk && !inPriorOutputs && !inOwnOutputs && !directorySatisfied) {
|
|
451
455
|
results.push({
|
|
452
456
|
category: "file",
|
|
453
457
|
target: file,
|
|
454
458
|
passed: false,
|
|
455
|
-
message: `Task ${task.id} references '${file}' which doesn't exist and isn't created by prior
|
|
459
|
+
message: `Task ${task.id} references '${file}' which doesn't exist and isn't created by prior or same-task outputs`,
|
|
456
460
|
blocking: true,
|
|
457
461
|
});
|
|
458
462
|
}
|
|
@@ -31,5 +31,6 @@ You are generating tests for recently completed GSD work.
|
|
|
31
31
|
- Do NOT modify implementation files — only create or update test files
|
|
32
32
|
- Name test files consistently with the project's conventions
|
|
33
33
|
- Keep tests focused and readable
|
|
34
|
+
- Tests must only reference files that are tracked in git. Do NOT import, read, or depend on paths listed in `.gitignore` — in particular GSD-local state such as `.gsd/`, `.planning/`, and `.audits/`. If a test seems to need one of those files, replace it with an inline fixture or a tracked sample; otherwise the test will fail for everyone but the author.
|
|
34
35
|
|
|
35
36
|
{{skillActivation}}
|
|
@@ -36,7 +36,7 @@ Then:
|
|
|
36
36
|
2. Execute the steps in the inlined task plan, adapting minor local mismatches when the surrounding code differs from the planner's snapshot
|
|
37
37
|
3. Before any `Write` that creates an artifact or output file, check whether that path already exists. If it does, read it first and decide whether the work is already done, should be extended, or truly needs replacement. "Create" in the plan does **not** mean the file is missing — a prior session may already have started it.
|
|
38
38
|
4. Build the real thing. If the task plan says "create login endpoint", build an endpoint that actually authenticates against a real store, not one that returns a hardcoded success response. If the task plan says "create dashboard page", build a page that renders real data from the API, not a component with hardcoded props. Stubs and mocks are for tests, not for the shipped feature.
|
|
39
|
-
5. Write or update tests as part of execution — tests are verification, not an afterthought. If the slice plan defines test files in its Verification section and this is the first task, create them (they should initially fail).
|
|
39
|
+
5. Write or update tests as part of execution — tests are verification, not an afterthought. If the slice plan defines test files in its Verification section and this is the first task, create them (they should initially fail). Tests must only reference files tracked in git; never import, read, or assert on paths listed in `.gitignore` (e.g. `.gsd/`, `.planning/`, `.audits/`) — those files are local-only and the test will fail for anyone else. Use inline fixtures or tracked samples instead.
|
|
40
40
|
6. When implementing non-trivial runtime behavior (async flows, API boundaries, background processes, error paths), add or preserve agent-usable observability. Skip this for simple changes where it doesn't apply.
|
|
41
41
|
|
|
42
42
|
**Background process rule:** Never use bare `command &` to run background processes. The shell's `&` operator leaves stdout/stderr attached to the parent, which causes the Bash tool to hang indefinitely waiting for those streams to close. Always redirect output before backgrounding:
|
|
@@ -53,6 +53,7 @@ Then:
|
|
|
53
53
|
- For simple slices: executable commands or script assertions are fine.
|
|
54
54
|
- If the project is non-trivial and has no test framework, the first task should set one up.
|
|
55
55
|
- If this slice establishes a boundary contract, verification must exercise that contract.
|
|
56
|
+
- Planned test files must only read from or import paths that are tracked in git. Do NOT plan tests whose inputs or fixtures are paths listed in `.gitignore` (e.g. `.gsd/`, `.planning/`, `.audits/`). If the scenario seems to require such a file, plan an inline fixture or a tracked sample instead.
|
|
56
57
|
4. **For non-trivial slices only** — plan observability, proof level, and integration closure:
|
|
57
58
|
- Include `Observability / Diagnostics` for backend, integration, async, stateful, or UI slices where failure diagnosis matters.
|
|
58
59
|
- Fill `Proof Level` and `Integration Closure` when the slice crosses runtime boundaries or has meaningful integration concerns.
|
|
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
6
7
|
|
|
7
8
|
import {
|
|
8
9
|
unitVerb,
|
|
@@ -15,6 +16,10 @@ import {
|
|
|
15
16
|
getWidgetMode,
|
|
16
17
|
cycleWidgetMode,
|
|
17
18
|
_resetWidgetModeForTests,
|
|
19
|
+
_resetLastCommitCacheForTests,
|
|
20
|
+
_refreshLastCommitForTests,
|
|
21
|
+
_getLastCommitForTests,
|
|
22
|
+
_getLastCommitFetchedAtForTests,
|
|
18
23
|
} from "../auto-dashboard.ts";
|
|
19
24
|
|
|
20
25
|
const autoSource = readFileSync(join(process.cwd(), "src", "resources", "extensions", "gsd", "auto.ts"), "utf-8");
|
|
@@ -215,6 +220,50 @@ test("auto progress widget renders RTK savings under the footer stats line", ()
|
|
|
215
220
|
assert.match(dashboardSource, /lines\.push\(rightAlign\("", theme\.fg\("dim", cachedRtkLabel\), width\)\);/);
|
|
216
221
|
});
|
|
217
222
|
|
|
223
|
+
test("last commit refresh backs off cleanly when base path is not a git repo", (t) => {
|
|
224
|
+
const dir = makeTempDir("non-git");
|
|
225
|
+
mkdirSync(dir, { recursive: true });
|
|
226
|
+
|
|
227
|
+
t.after(() => {
|
|
228
|
+
cleanup(dir);
|
|
229
|
+
_resetLastCommitCacheForTests();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
_resetLastCommitCacheForTests();
|
|
233
|
+
_refreshLastCommitForTests(dir);
|
|
234
|
+
|
|
235
|
+
assert.equal(_getLastCommitForTests(dir), null);
|
|
236
|
+
assert.ok(
|
|
237
|
+
_getLastCommitFetchedAtForTests() > 0,
|
|
238
|
+
"non-git refresh should still advance fetchedAt to avoid render-loop retries",
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("last commit refresh still returns commit info for a valid git repo", (t) => {
|
|
243
|
+
const dir = makeTempDir("git");
|
|
244
|
+
mkdirSync(dir, { recursive: true });
|
|
245
|
+
|
|
246
|
+
execFileSync("git", ["init", "-b", "main"], { cwd: dir, stdio: "pipe" });
|
|
247
|
+
execFileSync("git", ["config", "user.name", "GSD Test"], { cwd: dir, stdio: "pipe" });
|
|
248
|
+
execFileSync("git", ["config", "user.email", "gsd@example.com"], { cwd: dir, stdio: "pipe" });
|
|
249
|
+
writeFileSync(join(dir, "README.md"), "hello\n", "utf-8");
|
|
250
|
+
execFileSync("git", ["add", "README.md"], { cwd: dir, stdio: "pipe" });
|
|
251
|
+
execFileSync("git", ["commit", "-m", "test: seed dashboard repo"], { cwd: dir, stdio: "pipe" });
|
|
252
|
+
|
|
253
|
+
t.after(() => {
|
|
254
|
+
cleanup(dir);
|
|
255
|
+
_resetLastCommitCacheForTests();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
_resetLastCommitCacheForTests();
|
|
259
|
+
_refreshLastCommitForTests(dir);
|
|
260
|
+
|
|
261
|
+
const lastCommit = _getLastCommitForTests(dir);
|
|
262
|
+
assert.ok(lastCommit, "git repo should produce last commit metadata");
|
|
263
|
+
assert.match(lastCommit!.message, /test: seed dashboard repo/);
|
|
264
|
+
assert.ok(lastCommit!.timeAgo.length > 0, "relative time should be populated");
|
|
265
|
+
});
|
|
266
|
+
|
|
218
267
|
// ─── extractUatSliceId ───────────────────────────────────────────────────
|
|
219
268
|
|
|
220
269
|
test("extractUatSliceId extracts slice ID from M001/S01 format", () => {
|
|
@@ -125,9 +125,9 @@ console.log('\n=== complete-slice: schema v6 migration ===');
|
|
|
125
125
|
|
|
126
126
|
const adapter = _getAdapter()!;
|
|
127
127
|
|
|
128
|
-
// Verify schema version is current (
|
|
128
|
+
// Verify schema version is current (v20 — memory system Phase 4 relations)
|
|
129
129
|
const versionRow = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
|
|
130
|
-
assertEq(versionRow?.['v'],
|
|
130
|
+
assertEq(versionRow?.['v'], 20, 'schema version should be 20');
|
|
131
131
|
|
|
132
132
|
// Verify slices table has full_summary_md and full_uat_md columns
|
|
133
133
|
const cols = adapter.prepare("PRAGMA table_info(slices)").all();
|
|
@@ -109,9 +109,9 @@ console.log('\n=== complete-task: schema v5 migration ===');
|
|
|
109
109
|
|
|
110
110
|
const adapter = _getAdapter()!;
|
|
111
111
|
|
|
112
|
-
// Verify schema version is current (
|
|
112
|
+
// Verify schema version is current (v20 — memory system Phase 4 relations)
|
|
113
113
|
const versionRow = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
|
|
114
|
-
assertEq(versionRow?.['v'],
|
|
114
|
+
assertEq(versionRow?.['v'], 20, 'schema version should be 20');
|
|
115
115
|
|
|
116
116
|
// Verify all 4 new tables exist
|
|
117
117
|
const tables = adapter.prepare(
|
|
@@ -327,7 +327,7 @@ test("ADR-011 P2: listEscalationArtifacts filters to actionable by default", (t)
|
|
|
327
327
|
assert.equal(all.length, 2, "both surface with --all");
|
|
328
328
|
});
|
|
329
329
|
|
|
330
|
-
test("ADR-011 P2: schema
|
|
330
|
+
test("ADR-011 P2: schema v20 fresh DB has all escalation columns on tasks + source on decisions", (t) => {
|
|
331
331
|
const base = makeBase();
|
|
332
332
|
t.after(() => cleanup(base));
|
|
333
333
|
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
@@ -348,7 +348,7 @@ test("ADR-011 P2: schema v17 fresh DB has all escalation columns on tasks + sour
|
|
|
348
348
|
assert.ok(decCols.includes("source"), "decisions table must have source column");
|
|
349
349
|
|
|
350
350
|
const version = adapter.prepare("SELECT MAX(version) as v FROM schema_version").get();
|
|
351
|
-
assert.equal(version?.["v"],
|
|
351
|
+
assert.equal(version?.["v"], 20);
|
|
352
352
|
});
|
|
353
353
|
|
|
354
354
|
test("ADR-011 P2: findUnappliedEscalationOverride returns null when escalation_pending=1 (still pending)", (t) => {
|
|
@@ -81,7 +81,7 @@ describe('gsd-db', () => {
|
|
|
81
81
|
// Check schema_version table
|
|
82
82
|
const adapter = _getAdapter()!;
|
|
83
83
|
const version = adapter.prepare('SELECT MAX(version) as version FROM schema_version').get();
|
|
84
|
-
assert.deepStrictEqual(version?.['version'],
|
|
84
|
+
assert.deepStrictEqual(version?.['version'], 20, 'schema version should be 20');
|
|
85
85
|
|
|
86
86
|
// Check tables exist by querying them
|
|
87
87
|
const dRows = adapter.prepare('SELECT count(*) as cnt FROM decisions').get();
|