opencodekit 0.20.2 → 0.20.4
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/index.js +1 -1
- package/dist/template/.opencode/agent/build.md +4 -0
- package/dist/template/.opencode/agent/explore.md +4 -0
- package/dist/template/.opencode/agent/general.md +4 -0
- package/dist/template/.opencode/agent/plan.md +4 -0
- package/dist/template/.opencode/agent/review.md +4 -0
- package/dist/template/.opencode/agent/scout.md +4 -0
- package/dist/template/.opencode/command/create.md +119 -25
- package/dist/template/.opencode/command/design.md +1 -2
- package/dist/template/.opencode/command/health.md +234 -0
- package/dist/template/.opencode/command/init-user.md +15 -0
- package/dist/template/.opencode/command/plan.md +3 -4
- package/dist/template/.opencode/command/pr.md +13 -0
- package/dist/template/.opencode/command/research.md +15 -3
- package/dist/template/.opencode/command/review-codebase.md +11 -1
- package/dist/template/.opencode/command/ship.md +72 -8
- package/dist/template/.opencode/command/status.md +1 -1
- package/dist/template/.opencode/command/ui-review.md +0 -1
- package/dist/template/.opencode/command/ui-slop-check.md +1 -1
- package/dist/template/.opencode/command/verify.md +11 -1
- package/dist/template/.opencode/memory.db +0 -0
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/opencode.json +1678 -1677
- package/dist/template/.opencode/plugin/README.md +1 -1
- package/dist/template/.opencode/plugin/lib/compact.ts +194 -0
- package/dist/template/.opencode/plugin/lib/compile.ts +253 -0
- package/dist/template/.opencode/plugin/lib/db/graph.ts +253 -0
- package/dist/template/.opencode/plugin/lib/db/observations.ts +8 -3
- package/dist/template/.opencode/plugin/lib/db/schema.ts +96 -5
- package/dist/template/.opencode/plugin/lib/db/types.ts +73 -0
- package/dist/template/.opencode/plugin/lib/index-generator.ts +170 -0
- package/dist/template/.opencode/plugin/lib/lint.ts +359 -0
- package/dist/template/.opencode/plugin/lib/memory-admin-tools.ts +78 -4
- package/dist/template/.opencode/plugin/lib/memory-db.ts +19 -1
- package/dist/template/.opencode/plugin/lib/memory-helpers.ts +30 -0
- package/dist/template/.opencode/plugin/lib/memory-hooks.ts +10 -0
- package/dist/template/.opencode/plugin/lib/memory-tools.ts +167 -2
- package/dist/template/.opencode/plugin/lib/operation-log.ts +109 -0
- package/dist/template/.opencode/plugin/lib/validate.ts +243 -0
- package/dist/template/.opencode/plugin/memory.ts +2 -1
- package/dist/template/.opencode/skill/design-taste-frontend/SKILL.md +13 -1
- package/dist/template/.opencode/skill/figma-go/SKILL.md +1 -1
- package/dist/template/.opencode/skill/full-output-enforcement/SKILL.md +13 -0
- package/dist/template/.opencode/skill/high-end-visual-design/SKILL.md +13 -0
- package/dist/template/.opencode/skill/industrial-brutalist-ui/SKILL.md +13 -0
- package/dist/template/.opencode/skill/memory-system/SKILL.md +65 -1
- package/dist/template/.opencode/skill/minimalist-ui/SKILL.md +13 -0
- package/dist/template/.opencode/skill/redesign-existing-projects/SKILL.md +13 -0
- package/dist/template/.opencode/skill/requesting-code-review/SKILL.md +48 -2
- package/dist/template/.opencode/skill/requesting-code-review/references/specialist-profiles.md +108 -0
- package/dist/template/.opencode/skill/skill-creator/SKILL.md +25 -0
- package/dist/template/.opencode/skill/stitch-design-taste/SKILL.md +13 -0
- package/dist/template/.opencode/skill/verification-before-completion/SKILL.md +46 -0
- package/package.json +1 -1
- package/dist/template/.opencode/agent/runner.md +0 -79
- package/dist/template/.opencode/command/start.md +0 -156
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Memory Plugin — Core Tools
|
|
3
3
|
*
|
|
4
|
-
* observation, memory-search, memory-get, memory-read, memory-update, memory-timeline
|
|
4
|
+
* observation, memory-search, memory-get, memory-read, memory-update, memory-timeline,
|
|
5
|
+
* memory-graph-add, memory-graph-query, memory-graph-invalidate, memory-compact
|
|
5
6
|
*
|
|
6
7
|
* Uses factory pattern: createCoreTools(deps) returns tool definitions
|
|
7
8
|
* that can be spread into plugin's tool:{} export.
|
|
@@ -11,17 +12,23 @@ import { readdir } from "node:fs/promises";
|
|
|
11
12
|
import path from "node:path";
|
|
12
13
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
13
14
|
import {
|
|
15
|
+
addEntityTriple,
|
|
14
16
|
type ConfidenceLevel,
|
|
15
17
|
checkFTS5Available,
|
|
18
|
+
getMemoryDB,
|
|
16
19
|
getMemoryFile,
|
|
17
20
|
getObservationsByIds,
|
|
18
21
|
getTimelineAroundObservation,
|
|
22
|
+
invalidateTriple,
|
|
19
23
|
type ObservationSource,
|
|
20
24
|
type ObservationType,
|
|
25
|
+
queryEntity,
|
|
21
26
|
searchDistillationsFTS,
|
|
22
27
|
searchObservationsFTS,
|
|
23
28
|
storeObservation,
|
|
24
29
|
upsertMemoryFile,
|
|
30
|
+
type HallType,
|
|
31
|
+
VALID_HALLS,
|
|
25
32
|
} from "./memory-db.js";
|
|
26
33
|
import {
|
|
27
34
|
autoDetectFiles,
|
|
@@ -31,6 +38,8 @@ import {
|
|
|
31
38
|
TYPE_ICONS,
|
|
32
39
|
VALID_TYPES,
|
|
33
40
|
} from "./memory-helpers.js";
|
|
41
|
+
import { validateObservation } from "./validate.js";
|
|
42
|
+
import { compactObservations } from "./compact.js";
|
|
34
43
|
|
|
35
44
|
/**
|
|
36
45
|
* Wrap a memory tool execute function with DB error handling.
|
|
@@ -117,6 +126,22 @@ export function createCoreTools(deps: CoreToolDeps) {
|
|
|
117
126
|
.string()
|
|
118
127
|
.optional()
|
|
119
128
|
.describe("manual, curator, imported"),
|
|
129
|
+
wing: tool.schema
|
|
130
|
+
.string()
|
|
131
|
+
.optional()
|
|
132
|
+
.describe("Navigation wing (project or person name)"),
|
|
133
|
+
hall: tool.schema
|
|
134
|
+
.string()
|
|
135
|
+
.optional()
|
|
136
|
+
.describe("Navigation hall: facts, events, discoveries, preferences, advice"),
|
|
137
|
+
room: tool.schema
|
|
138
|
+
.string()
|
|
139
|
+
.optional()
|
|
140
|
+
.describe("Navigation room (topic name, e.g. auth-migration)"),
|
|
141
|
+
raw_source: tool.schema
|
|
142
|
+
.string()
|
|
143
|
+
.optional()
|
|
144
|
+
.describe("Verbatim source text to preserve losslessly alongside narrative"),
|
|
120
145
|
},
|
|
121
146
|
execute: withDBErrorHandling(async (args) => {
|
|
122
147
|
const obsType = args.type as ObservationType;
|
|
@@ -156,6 +181,37 @@ export function createCoreTools(deps: CoreToolDeps) {
|
|
|
156
181
|
}
|
|
157
182
|
|
|
158
183
|
const source = (args.source ?? "manual") as ObservationSource;
|
|
184
|
+
const hall = args.hall as HallType | undefined;
|
|
185
|
+
if (hall && !VALID_HALLS.includes(hall)) {
|
|
186
|
+
return `Error: Invalid hall "${args.hall}". Valid: ${VALID_HALLS.join(", ")}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Validation gate: check for duplicates, contradictions, low quality
|
|
190
|
+
const validation = validateObservation({
|
|
191
|
+
type: obsType,
|
|
192
|
+
title: args.title,
|
|
193
|
+
subtitle: args.subtitle,
|
|
194
|
+
facts,
|
|
195
|
+
narrative,
|
|
196
|
+
concepts,
|
|
197
|
+
files_read: filesRead,
|
|
198
|
+
files_modified: filesModified,
|
|
199
|
+
confidence,
|
|
200
|
+
bead_id: args.bead_id,
|
|
201
|
+
supersedes,
|
|
202
|
+
source,
|
|
203
|
+
wing: args.wing,
|
|
204
|
+
hall,
|
|
205
|
+
room: args.room,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (validation.verdict === "reject") {
|
|
209
|
+
const reasons = validation.issues.map(i => i.message).join("; ");
|
|
210
|
+
const dupHint = validation.duplicateOf
|
|
211
|
+
? ` Use \`observation({ supersedes: "${validation.duplicateOf}", ... })\` to update it.`
|
|
212
|
+
: "";
|
|
213
|
+
return `Rejected: ${reasons}.${dupHint}`;
|
|
214
|
+
}
|
|
159
215
|
|
|
160
216
|
const id = storeObservation({
|
|
161
217
|
type: obsType,
|
|
@@ -163,6 +219,7 @@ export function createCoreTools(deps: CoreToolDeps) {
|
|
|
163
219
|
subtitle: args.subtitle,
|
|
164
220
|
facts,
|
|
165
221
|
narrative,
|
|
222
|
+
raw_source: args.raw_source,
|
|
166
223
|
concepts,
|
|
167
224
|
files_read: filesRead,
|
|
168
225
|
files_modified: filesModified,
|
|
@@ -170,9 +227,16 @@ export function createCoreTools(deps: CoreToolDeps) {
|
|
|
170
227
|
bead_id: args.bead_id,
|
|
171
228
|
supersedes,
|
|
172
229
|
source,
|
|
230
|
+
wing: args.wing,
|
|
231
|
+
hall,
|
|
232
|
+
room: args.room,
|
|
173
233
|
});
|
|
174
234
|
|
|
175
|
-
|
|
235
|
+
const warnings = validation.issues.length > 0
|
|
236
|
+
? `\n⚠️ Warnings: ${validation.issues.map(i => i.message).join("; ")}`
|
|
237
|
+
: "";
|
|
238
|
+
|
|
239
|
+
return `${TYPE_ICONS[obsType] ?? "\uD83D\uDCCC"} Observation #${id} stored [${obsType}] "${args.title}" (confidence: ${confidence}, source: ${source})${warnings}`;
|
|
176
240
|
}),
|
|
177
241
|
}),
|
|
178
242
|
|
|
@@ -366,5 +430,106 @@ export function createCoreTools(deps: CoreToolDeps) {
|
|
|
366
430
|
return lines.join("\n");
|
|
367
431
|
}),
|
|
368
432
|
}),
|
|
433
|
+
|
|
434
|
+
"memory-graph-add": tool({
|
|
435
|
+
description: `Add a triple to the entity knowledge graph.\n\nStores a subject-predicate-object relationship with optional temporal bounds and confidence.\nUse for structured facts like "project uses typescript" or "user prefers dark-mode".\n\nExample:\nmemory-graph-add({ subject: "project", predicate: "uses", object: "typescript" })\nmemory-graph-add({ subject: "auth", predicate: "depends-on", object: "jwt", confidence: 0.9, source_observation_id: 42 })`,
|
|
436
|
+
args: {
|
|
437
|
+
subject: tool.schema.string().describe("Entity subject"),
|
|
438
|
+
predicate: tool.schema.string().describe("Relationship type (e.g. uses, depends-on, prefers)"),
|
|
439
|
+
object: tool.schema.string().describe("Entity object"),
|
|
440
|
+
valid_from: tool.schema.string().optional().describe("Start date (ISO, default: today)"),
|
|
441
|
+
valid_to: tool.schema.string().optional().describe("End date (ISO, null = still active)"),
|
|
442
|
+
confidence: tool.schema.number().optional().describe("Confidence 0.0-1.0 (default: 1.0)"),
|
|
443
|
+
source_observation_id: tool.schema.number().optional().describe("Link to source observation"),
|
|
444
|
+
},
|
|
445
|
+
execute: withDBErrorHandling(async (args) => {
|
|
446
|
+
if (!args.subject?.trim() || !args.predicate?.trim() || !args.object?.trim()) {
|
|
447
|
+
return "Error: subject, predicate, and object are required.";
|
|
448
|
+
}
|
|
449
|
+
const id = addEntityTriple({
|
|
450
|
+
subject: args.subject,
|
|
451
|
+
predicate: args.predicate,
|
|
452
|
+
object: args.object,
|
|
453
|
+
valid_from: args.valid_from,
|
|
454
|
+
valid_to: args.valid_to,
|
|
455
|
+
confidence: args.confidence,
|
|
456
|
+
source_observation_id: args.source_observation_id,
|
|
457
|
+
});
|
|
458
|
+
return `Triple #${id} added: ${args.subject} —[${args.predicate}]→ ${args.object}`;
|
|
459
|
+
}),
|
|
460
|
+
}),
|
|
461
|
+
|
|
462
|
+
"memory-graph-query": tool({
|
|
463
|
+
description: `Query the entity knowledge graph.\n\nFind relationships for an entity with optional time and direction filters.\nReturns triples where the entity appears as subject, object, or both.\n\nExample:\nmemory-graph-query({ entity: "typescript" })\nmemory-graph-query({ entity: "auth", direction: "out", active_only: true })\nmemory-graph-query({ entity: "project", as_of: "2025-06-01" })`,
|
|
464
|
+
args: {
|
|
465
|
+
entity: tool.schema.string().describe("Entity to query"),
|
|
466
|
+
direction: tool.schema.string().optional().describe("out (subject), in (object), both (default)"),
|
|
467
|
+
predicate: tool.schema.string().optional().describe("Filter by predicate"),
|
|
468
|
+
as_of: tool.schema.string().optional().describe("ISO date for point-in-time query"),
|
|
469
|
+
active_only: tool.schema.boolean().optional().describe("Only active triples (default: false)"),
|
|
470
|
+
limit: tool.schema.number().optional().describe("Max results (default: 50)"),
|
|
471
|
+
},
|
|
472
|
+
execute: withDBErrorHandling(async (args) => {
|
|
473
|
+
if (!args.entity?.trim()) return "Error: entity is required.";
|
|
474
|
+
const results = queryEntity(args.entity, {
|
|
475
|
+
direction: args.direction as "out" | "in" | "both" | undefined,
|
|
476
|
+
predicate: args.predicate,
|
|
477
|
+
as_of: args.as_of,
|
|
478
|
+
activeOnly: args.active_only,
|
|
479
|
+
limit: args.limit,
|
|
480
|
+
});
|
|
481
|
+
if (results.length === 0) return `No triples found for entity "${args.entity}".`;
|
|
482
|
+
const lines: string[] = [
|
|
483
|
+
`## Entity Graph: ${args.entity} (${results.length} triples)\n`,
|
|
484
|
+
"| ID | Subject | Predicate | Object | Active | Confidence |",
|
|
485
|
+
"|---|---|---|---|---|---|",
|
|
486
|
+
];
|
|
487
|
+
for (const r of results) {
|
|
488
|
+
lines.push(`| ${r.id} | ${r.subject} | ${r.predicate} | ${r.object} | ${r.is_active ? "\u2705" : "\u274C"} | ${(r.confidence * 100).toFixed(0)}% |`);
|
|
489
|
+
}
|
|
490
|
+
return lines.join("\n");
|
|
491
|
+
}),
|
|
492
|
+
}),
|
|
493
|
+
|
|
494
|
+
"memory-graph-invalidate": tool({
|
|
495
|
+
description: `Invalidate (close) entity triples in the knowledge graph.\n\nMarks active triples matching subject+predicate+object as no longer valid by setting valid_to.\nUse when a fact is no longer true (e.g. project stopped using a library).\n\nExample:\nmemory-graph-invalidate({ subject: "project", predicate: "uses", object: "moment.js" })`,
|
|
496
|
+
args: {
|
|
497
|
+
subject: tool.schema.string().describe("Entity subject"),
|
|
498
|
+
predicate: tool.schema.string().describe("Relationship type"),
|
|
499
|
+
object: tool.schema.string().describe("Entity object"),
|
|
500
|
+
end_date: tool.schema.string().optional().describe("End date (ISO, default: today)"),
|
|
501
|
+
},
|
|
502
|
+
execute: withDBErrorHandling(async (args) => {
|
|
503
|
+
if (!args.subject?.trim() || !args.predicate?.trim() || !args.object?.trim()) {
|
|
504
|
+
return "Error: subject, predicate, and object are required.";
|
|
505
|
+
}
|
|
506
|
+
const count = invalidateTriple(args.subject, args.predicate, args.object, args.end_date);
|
|
507
|
+
return count > 0
|
|
508
|
+
? `Invalidated ${count} triple(s): ${args.subject} —[${args.predicate}]→ ${args.object}`
|
|
509
|
+
: `No active triples found matching ${args.subject} —[${args.predicate}]→ ${args.object}.`;
|
|
510
|
+
}),
|
|
511
|
+
}),
|
|
512
|
+
|
|
513
|
+
"memory-compact": tool({
|
|
514
|
+
description: `Compact observations into a dense, token-efficient format.\n\nUses AAAK-inspired pipe-separated compression achieving ~3-5x token reduction.\nUseful for injecting memory into prompts with minimal token cost.\n\nExample:\nmemory-compact({})\nmemory-compact({ limit: 20 })`,
|
|
515
|
+
args: {
|
|
516
|
+
limit: tool.schema.number().optional().describe("Max observations to compact (default: all)"),
|
|
517
|
+
},
|
|
518
|
+
execute: withDBErrorHandling(async (args) => {
|
|
519
|
+
const db = getMemoryDB();
|
|
520
|
+
const limitClause = args.limit ? `LIMIT ${Number(args.limit)}` : "";
|
|
521
|
+
const observations = db.query(
|
|
522
|
+
`SELECT id, type, title, narrative, concepts, wing, hall, room, confidence, created_at
|
|
523
|
+
FROM observations
|
|
524
|
+
WHERE superseded_by IS NULL
|
|
525
|
+
ORDER BY created_at_epoch DESC ${limitClause}`
|
|
526
|
+
).all() as { id: number; type: string; title: string; narrative?: string | null; concepts?: string | null; wing?: string | null; hall?: string | null; room?: string | null; confidence?: string | null; created_at?: string | null }[];
|
|
527
|
+
if (observations.length === 0) return "No observations to compact.";
|
|
528
|
+
const result = compactObservations(observations);
|
|
529
|
+
if (!result.compressed) return "No observations to compact.";
|
|
530
|
+
const ratio = result.compression_ratio > 0 ? ` (${(result.compression_ratio * 100).toFixed(0)}% of original)` : "";
|
|
531
|
+
return `## Compact Memory (${observations.length} observations, ~${result.token_estimate} tokens${ratio})\n\n${result.compressed}`;
|
|
532
|
+
}),
|
|
533
|
+
}),
|
|
369
534
|
};
|
|
370
535
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Operation Log — Append-Only Audit Trail
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Karpathy's LLM Wiki log.md:
|
|
5
|
+
* chronological record of all memory operations for provenance tracking.
|
|
6
|
+
*
|
|
7
|
+
* Stored in memory_files as "log" — append-only, never overwritten.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getMemoryFile, upsertMemoryFile } from "./db/maintenance.js";
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
export type OperationType =
|
|
17
|
+
| "observation-created"
|
|
18
|
+
| "observation-superseded"
|
|
19
|
+
| "observation-validated"
|
|
20
|
+
| "observation-rejected"
|
|
21
|
+
| "index-generated"
|
|
22
|
+
| "lint-run"
|
|
23
|
+
| "compile-run"
|
|
24
|
+
| "maintenance-run"
|
|
25
|
+
| "distillation-created"
|
|
26
|
+
| "curation-run";
|
|
27
|
+
|
|
28
|
+
export interface LogEntry {
|
|
29
|
+
timestamp: string;
|
|
30
|
+
operation: OperationType;
|
|
31
|
+
targets: string[];
|
|
32
|
+
summary: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Log Operations
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Append an entry to the operation log.
|
|
41
|
+
* The log is append-only and stored in memory_files.
|
|
42
|
+
*/
|
|
43
|
+
export function appendOperationLog(entry: Omit<LogEntry, "timestamp">): void {
|
|
44
|
+
const timestamp = new Date().toISOString().slice(0, 19);
|
|
45
|
+
const targets = entry.targets.length > 0 ? entry.targets.join(", ") : "-";
|
|
46
|
+
const line = `[${timestamp}] ${entry.operation} | ${targets} | ${entry.summary}`;
|
|
47
|
+
|
|
48
|
+
// Check if log exists
|
|
49
|
+
const existing = getMemoryFile("log");
|
|
50
|
+
if (existing) {
|
|
51
|
+
// Append with newline
|
|
52
|
+
upsertMemoryFile("log", line, "append");
|
|
53
|
+
} else {
|
|
54
|
+
// Create with header
|
|
55
|
+
const header = [
|
|
56
|
+
"# Memory Operation Log",
|
|
57
|
+
"",
|
|
58
|
+
"> Append-only chronological record of all memory operations.",
|
|
59
|
+
"> Format: [timestamp] operation | targets | summary",
|
|
60
|
+
"",
|
|
61
|
+
"---",
|
|
62
|
+
"",
|
|
63
|
+
line,
|
|
64
|
+
].join("\n");
|
|
65
|
+
upsertMemoryFile("log", header, "replace");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get recent log entries (parsed from the log file).
|
|
71
|
+
*/
|
|
72
|
+
export function getRecentLogEntries(limit = 20): LogEntry[] {
|
|
73
|
+
const existing = getMemoryFile("log");
|
|
74
|
+
if (!existing) return [];
|
|
75
|
+
|
|
76
|
+
const lines = existing.content
|
|
77
|
+
.split("\n")
|
|
78
|
+
.filter((line) => line.startsWith("["))
|
|
79
|
+
.slice(-limit);
|
|
80
|
+
|
|
81
|
+
return lines.map((line) => {
|
|
82
|
+
const match = line.match(/^\[(.+?)\] (.+?) \| (.+?) \| (.+)$/);
|
|
83
|
+
if (!match) {
|
|
84
|
+
return {
|
|
85
|
+
timestamp: "",
|
|
86
|
+
operation: "observation-created" as OperationType,
|
|
87
|
+
targets: [],
|
|
88
|
+
summary: line,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
timestamp: match[1],
|
|
93
|
+
operation: match[2] as OperationType,
|
|
94
|
+
targets: match[3].split(",").map((t) => t.trim()),
|
|
95
|
+
summary: match[4],
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get the full log content as a string.
|
|
102
|
+
*/
|
|
103
|
+
export function getLogContent(): string {
|
|
104
|
+
const existing = getMemoryFile("log");
|
|
105
|
+
return (
|
|
106
|
+
existing?.content ??
|
|
107
|
+
"No operation log found. Run a memory operation to start logging."
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observation Validation Gate
|
|
3
|
+
*
|
|
4
|
+
* Inspired by jumperz's Secondmate Hermes validation:
|
|
5
|
+
* checks for duplicates and contradictions BEFORE storing an observation.
|
|
6
|
+
*
|
|
7
|
+
* Unlike Secondmate's approach (separate LLM model), this uses
|
|
8
|
+
* FTS5 similarity search + heuristic contradiction detection.
|
|
9
|
+
*
|
|
10
|
+
* Returns a validation result that can:
|
|
11
|
+
* - PASS: store normally
|
|
12
|
+
* - WARN: store but flag issues
|
|
13
|
+
* - REJECT: don't store, return reason
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ObservationInput, ObservationRow } from "./db/types.js";
|
|
17
|
+
import { getMemoryDB } from "./memory-db.js";
|
|
18
|
+
import { hasWord } from "./memory-helpers.js";
|
|
19
|
+
import { appendOperationLog } from "./operation-log.js";
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Types
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
export type ValidationVerdict = "pass" | "warn" | "reject";
|
|
26
|
+
|
|
27
|
+
export interface ValidationResult {
|
|
28
|
+
verdict: ValidationVerdict;
|
|
29
|
+
issues: ValidationIssue[];
|
|
30
|
+
/** If a near-duplicate was found, its ID */
|
|
31
|
+
duplicateOf?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ValidationIssue {
|
|
35
|
+
type: "duplicate" | "near-duplicate" | "contradiction" | "low-quality";
|
|
36
|
+
severity: "high" | "medium" | "low";
|
|
37
|
+
message: string;
|
|
38
|
+
relatedId?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Validation
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validate an observation before storing.
|
|
47
|
+
* Returns verdict (pass/warn/reject) with issues.
|
|
48
|
+
*/
|
|
49
|
+
export function validateObservation(input: ObservationInput): ValidationResult {
|
|
50
|
+
const issues: ValidationIssue[] = [];
|
|
51
|
+
|
|
52
|
+
// Check 1: Exact title duplicate
|
|
53
|
+
const exactDup = findExactDuplicate(input);
|
|
54
|
+
if (exactDup) {
|
|
55
|
+
// If it's explicitly superseding, that's fine
|
|
56
|
+
if (input.supersedes === exactDup.id) {
|
|
57
|
+
// Intentional supersede — pass
|
|
58
|
+
} else {
|
|
59
|
+
issues.push({
|
|
60
|
+
type: "duplicate",
|
|
61
|
+
severity: "high",
|
|
62
|
+
message: `Exact duplicate of #${exactDup.id}: "${exactDup.title}"`,
|
|
63
|
+
relatedId: exactDup.id,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
appendOperationLog({
|
|
67
|
+
operation: "observation-duplicate-warning",
|
|
68
|
+
targets: [`#${exactDup.id}`],
|
|
69
|
+
summary: `Duplicate warning: "${input.title}" (matches #${exactDup.id})`,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
verdict: "warn",
|
|
74
|
+
issues,
|
|
75
|
+
duplicateOf: exactDup.id,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check 2: Near-duplicate via FTS5
|
|
81
|
+
const nearDup = findNearDuplicate(input);
|
|
82
|
+
if (nearDup) {
|
|
83
|
+
issues.push({
|
|
84
|
+
type: "near-duplicate",
|
|
85
|
+
severity: "medium",
|
|
86
|
+
message: `Similar to #${nearDup.id}: "${nearDup.title}" (consider superseding)`,
|
|
87
|
+
relatedId: nearDup.id,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check 3: Contradiction with existing decisions
|
|
92
|
+
if (input.type === "decision") {
|
|
93
|
+
const contradiction = findContradiction(input);
|
|
94
|
+
if (contradiction) {
|
|
95
|
+
issues.push({
|
|
96
|
+
type: "contradiction",
|
|
97
|
+
severity: "medium",
|
|
98
|
+
message: `May contradict #${contradiction.id}: "${contradiction.title}"`,
|
|
99
|
+
relatedId: contradiction.id,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check 4: Low quality (no narrative, no concepts)
|
|
105
|
+
if (!input.narrative && !input.concepts) {
|
|
106
|
+
issues.push({
|
|
107
|
+
type: "low-quality",
|
|
108
|
+
severity: "low",
|
|
109
|
+
message:
|
|
110
|
+
"Observation has no narrative and no concepts — low knowledge value",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Determine verdict
|
|
115
|
+
const hasHigh = issues.some((i) => i.severity === "high");
|
|
116
|
+
const verdict: ValidationVerdict = hasHigh
|
|
117
|
+
? "reject"
|
|
118
|
+
: issues.length > 0
|
|
119
|
+
? "warn"
|
|
120
|
+
: "pass";
|
|
121
|
+
|
|
122
|
+
if (verdict === "pass" || verdict === "warn") {
|
|
123
|
+
appendOperationLog({
|
|
124
|
+
operation: "observation-validated",
|
|
125
|
+
targets: [input.title],
|
|
126
|
+
summary: `Validated [${input.type}] "${input.title}" — ${verdict}${issues.length > 0 ? ` (${issues.length} issues)` : ""}`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { verdict, issues };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// Internal Checks
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check for exact title match (same type, active).
|
|
139
|
+
*/
|
|
140
|
+
function findExactDuplicate(input: ObservationInput): ObservationRow | null {
|
|
141
|
+
const db = getMemoryDB();
|
|
142
|
+
return db
|
|
143
|
+
.query(
|
|
144
|
+
`SELECT * FROM observations
|
|
145
|
+
WHERE type = ? AND LOWER(title) = LOWER(?) AND superseded_by IS NULL
|
|
146
|
+
LIMIT 1`,
|
|
147
|
+
)
|
|
148
|
+
.get(input.type, input.title) as ObservationRow | null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check for near-duplicate via FTS5 title search.
|
|
153
|
+
*/
|
|
154
|
+
function findNearDuplicate(input: ObservationInput): ObservationRow | null {
|
|
155
|
+
const db = getMemoryDB();
|
|
156
|
+
|
|
157
|
+
// Build FTS query from title words
|
|
158
|
+
// Strip FTS5 special operators and characters to prevent query syntax errors
|
|
159
|
+
const FTS5_OPERATORS = /\b(AND|OR|NOT|NEAR)\b/gi;
|
|
160
|
+
const words = input.title
|
|
161
|
+
.replace(FTS5_OPERATORS, "")
|
|
162
|
+
.replace(/['"*^+(){}]/g, "")
|
|
163
|
+
.split(/\s+/)
|
|
164
|
+
.filter((w) => w.length > 2)
|
|
165
|
+
.map((w) => `"${w}"*`)
|
|
166
|
+
.join(" AND ");
|
|
167
|
+
|
|
168
|
+
if (!words) return null;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const result = db
|
|
172
|
+
.query(
|
|
173
|
+
`SELECT o.* FROM observations o
|
|
174
|
+
JOIN observations_fts fts ON fts.rowid = o.id
|
|
175
|
+
WHERE observations_fts MATCH ?
|
|
176
|
+
AND o.type = ?
|
|
177
|
+
AND o.superseded_by IS NULL
|
|
178
|
+
AND o.id != COALESCE(?, -1)
|
|
179
|
+
ORDER BY bm25(observations_fts) LIMIT 1`,
|
|
180
|
+
)
|
|
181
|
+
.get(
|
|
182
|
+
words,
|
|
183
|
+
input.type,
|
|
184
|
+
input.supersedes ?? null,
|
|
185
|
+
) as ObservationRow | null;
|
|
186
|
+
|
|
187
|
+
return result;
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Check for contradictions with existing decisions.
|
|
195
|
+
*/
|
|
196
|
+
function findContradiction(input: ObservationInput): ObservationRow | null {
|
|
197
|
+
if (!input.concepts || input.concepts.length === 0) return null;
|
|
198
|
+
|
|
199
|
+
const db = getMemoryDB();
|
|
200
|
+
|
|
201
|
+
// Search for decisions with overlapping concepts
|
|
202
|
+
const conceptQuery = input.concepts.map((c) => `"${c}"*`).join(" OR ");
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const candidates = db
|
|
206
|
+
.query(
|
|
207
|
+
`SELECT o.* FROM observations o
|
|
208
|
+
JOIN observations_fts fts ON fts.rowid = o.id
|
|
209
|
+
WHERE observations_fts MATCH ?
|
|
210
|
+
AND o.type = 'decision'
|
|
211
|
+
AND o.superseded_by IS NULL
|
|
212
|
+
ORDER BY bm25(observations_fts) LIMIT 5`,
|
|
213
|
+
)
|
|
214
|
+
.all(conceptQuery) as ObservationRow[];
|
|
215
|
+
|
|
216
|
+
// Check for opposing signals
|
|
217
|
+
const inputText = `${input.title} ${input.narrative ?? ""}`.toLowerCase();
|
|
218
|
+
const contradictionPairs = [
|
|
219
|
+
["use", "don't use"],
|
|
220
|
+
["enable", "disable"],
|
|
221
|
+
["add", "remove"],
|
|
222
|
+
["prefer", "avoid"],
|
|
223
|
+
["always", "never"],
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
for (const candidate of candidates) {
|
|
227
|
+
const candidateText =
|
|
228
|
+
`${candidate.title} ${candidate.narrative ?? ""}`.toLowerCase();
|
|
229
|
+
for (const [wordA, wordB] of contradictionPairs) {
|
|
230
|
+
if (
|
|
231
|
+
(hasWord(inputText, wordA) && hasWord(candidateText, wordB)) ||
|
|
232
|
+
(hasWord(inputText, wordB) && hasWord(candidateText, wordA))
|
|
233
|
+
) {
|
|
234
|
+
return candidate;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
// FTS5 query failed
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* 5. Context Management — messages.transform → token budget enforcement
|
|
12
12
|
*
|
|
13
13
|
* Tools: observation, memory-search, memory-get, memory-read,
|
|
14
|
-
* memory-update, memory-timeline, memory-
|
|
14
|
+
* memory-update, memory-timeline, memory-graph-add, memory-graph-query,
|
|
15
|
+
* memory-graph-invalidate, memory-compact, memory-admin
|
|
15
16
|
*
|
|
16
17
|
* Module structure:
|
|
17
18
|
* memory.ts — Plugin entry (this file)
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: design-taste-frontend
|
|
3
|
-
description: Use as the BASE aesthetic
|
|
3
|
+
description: Use when building any web UI as the BASE aesthetic layer to override default LLM design biases. Enforces strict typography, color, spacing, and component architecture rules. Load BEFORE frontend-design when premium visual quality is required.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
## When to Use
|
|
7
|
+
|
|
8
|
+
- When building any web UI that needs to override default LLM design biases
|
|
9
|
+
- Before loading `frontend-design` skill when premium visual quality is required
|
|
10
|
+
- As the base aesthetic layer for any UI implementation task
|
|
11
|
+
|
|
12
|
+
## When NOT to Use
|
|
13
|
+
|
|
14
|
+
- When a more specific aesthetic overlay is loaded (high-end-visual-design, minimalist-ui, industrial-brutalist-ui)
|
|
15
|
+
- For non-UI tasks (backend, CLI, data processing)
|
|
16
|
+
|
|
17
|
+
|
|
6
18
|
# High-Agency Frontend Skill
|
|
7
19
|
|
|
8
20
|
## 1. ACTIVE BASELINE CONFIGURATION
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: figma-go
|
|
3
|
-
description: Use
|
|
3
|
+
description: Use when you need Figma read/write access WITHOUT an API token, via figma-mcp-go plugin bridge. Prefer over figma skill when no API token is available. MUST load when user needs Figma data and has the desktop plugin installed.
|
|
4
4
|
mcp:
|
|
5
5
|
figma-mcp-go:
|
|
6
6
|
command: npx
|
|
@@ -3,6 +3,19 @@ name: full-output-enforcement
|
|
|
3
3
|
description: MUST load when generating complete files, long code blocks, or any output where truncation or placeholder comments like '// ... rest of code' would be harmful. Bans all placeholder patterns and enforces exhaustive, unabridged output.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
## When to Use
|
|
7
|
+
|
|
8
|
+
- When generating complete files, long code blocks, or full configuration outputs
|
|
9
|
+
- When truncation or placeholder comments like `// ... rest of code` would be harmful
|
|
10
|
+
- When the user needs exhaustive, unabridged output
|
|
11
|
+
|
|
12
|
+
## When NOT to Use
|
|
13
|
+
|
|
14
|
+
- For short code snippets or single-function responses
|
|
15
|
+
- When summarization is explicitly requested
|
|
16
|
+
- For conversational responses that don't involve code generation
|
|
17
|
+
|
|
18
|
+
|
|
6
19
|
# Full-Output Enforcement
|
|
7
20
|
|
|
8
21
|
## Baseline
|
|
@@ -3,6 +3,19 @@ name: high-end-visual-design
|
|
|
3
3
|
description: Use INSTEAD OF design-taste-frontend when user explicitly requests premium, agency-quality, or luxury visual design. Defines exact fonts, spacing, shadows, and animations that make websites feel expensive. Blocks cheap AI defaults.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
## When to Use
|
|
7
|
+
|
|
8
|
+
- When the user explicitly requests premium, agency-quality, or luxury visual design
|
|
9
|
+
- Instead of design-taste-frontend when high-end aesthetics are the priority
|
|
10
|
+
- For landing pages, marketing sites, or portfolio projects that need to feel expensive
|
|
11
|
+
|
|
12
|
+
## When NOT to Use
|
|
13
|
+
|
|
14
|
+
- For internal tools, admin panels, or dashboards where function > form
|
|
15
|
+
- When minimalist-ui or industrial-brutalist-ui aesthetics are requested instead
|
|
16
|
+
- For rapid prototyping where visual polish is not yet needed
|
|
17
|
+
|
|
18
|
+
|
|
6
19
|
# Agent Skill: Principal UI/UX Architect & Motion Choreographer (Awwwards-Tier)
|
|
7
20
|
|
|
8
21
|
## 1. Meta Information & Core Directive
|
|
@@ -3,6 +3,19 @@ name: industrial-brutalist-ui
|
|
|
3
3
|
description: Use INSTEAD OF design-taste-frontend when user requests brutalist, military-terminal, or raw mechanical aesthetics. Swiss typographic print meets utilitarian color. For data-heavy dashboards or editorial sites needing declassified-blueprint energy.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
## When to Use
|
|
7
|
+
|
|
8
|
+
- When the user requests brutalist, military-terminal, or raw mechanical aesthetics
|
|
9
|
+
- For data-heavy dashboards or editorial sites needing declassified-blueprint energy
|
|
10
|
+
- Instead of design-taste-frontend when utilitarian aesthetics are requested
|
|
11
|
+
|
|
12
|
+
## When NOT to Use
|
|
13
|
+
|
|
14
|
+
- For consumer-facing marketing or e-commerce sites
|
|
15
|
+
- When clean, minimalist, or luxury aesthetics are requested
|
|
16
|
+
- For accessibility-critical applications where brutalist patterns may reduce readability
|
|
17
|
+
|
|
18
|
+
|
|
6
19
|
# SKILL: Industrial Brutalism & Tactical Telemetry UI
|
|
7
20
|
|
|
8
21
|
## 1. Skill Meta
|