@dzhechkov/harness-core 0.3.43 → 0.3.45
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/agentdb-index.d.ts +37 -0
- package/dist/agentdb-index.d.ts.map +1 -0
- package/dist/agentdb-index.js +96 -0
- package/dist/agentdb-index.js.map +1 -0
- package/dist/book-kb.d.ts +61 -0
- package/dist/book-kb.d.ts.map +1 -0
- package/dist/book-kb.js +125 -0
- package/dist/book-kb.js.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +16 -0
- package/dist/operations.js.map +1 -1
- package/dist/patterns.d.ts +23 -0
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +0 -0
- package/dist/patterns.js.map +1 -1
- package/dist/publish.d.ts +4 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +4 -0
- package/dist/publish.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +34 -8
- package/dist/registry.js.map +1 -1
- package/dist/setup.d.ts +6 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +26 -9
- package/dist/setup.js.map +1 -1
- package/package.json +2 -2
- package/src/agentdb-index.ts +128 -0
- package/src/book-kb.ts +170 -0
- package/src/index.ts +6 -2
- package/src/operations.ts +18 -0
- package/src/patterns.ts +0 -0
- package/src/publish.ts +4 -0
- package/src/registry.ts +33 -7
- package/src/setup.ts +28 -11
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic native AgentDB indexer — the reusable primitive behind Option C's learnings-mirror
|
|
3
|
+
* AND the book-knowledge-digitizer's KB indexer (ADR-001 v2, `features/book-knowledge-digitizer`).
|
|
4
|
+
*
|
|
5
|
+
* Writes rows NATIVELY via the project's `better-sqlite3`, replicating `ReasoningBank`'s exact
|
|
6
|
+
* schema (`reasoning_patterns` + `pattern_embeddings`, embed text `${taskType}: ${text}`) so the
|
|
7
|
+
* `agentdb` MCP server's `agentdb_pattern_search` reads what we write. `agentdb` is used ONLY for
|
|
8
|
+
* its `EmbeddingService` — never its `createDatabase` (hardwired to sql.js, whose whole-file save
|
|
9
|
+
* corrupts concurrent native-WAL writers; QE P1). Best-effort: never throws, returns an honest
|
|
10
|
+
* error string when the deps are absent.
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { join, dirname } from 'node:path';
|
|
16
|
+
import { pathToFileURL } from 'node:url';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
|
|
19
|
+
/** One record to index. `text` is stored as `approach` AND embedded (`${taskType}: ${text}`). */
|
|
20
|
+
export interface AgentdbRow {
|
|
21
|
+
readonly taskType: string;
|
|
22
|
+
readonly text: string;
|
|
23
|
+
/** Stored as `success_rate`; clamped to [0,1]. Use the REAL signal, never a fabricated 1.0. */
|
|
24
|
+
readonly score: number;
|
|
25
|
+
readonly tags?: readonly string[];
|
|
26
|
+
readonly metadata?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Outcome of {@link indexPatternsToAgentdb}. */
|
|
30
|
+
export interface AgentdbIndexResult {
|
|
31
|
+
readonly indexed: number;
|
|
32
|
+
readonly error?: string | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface NativeDb {
|
|
36
|
+
pragma: (s: string) => void;
|
|
37
|
+
exec: (s: string) => void;
|
|
38
|
+
prepare: (q: string) => { run: (...a: unknown[]) => { lastInsertRowid: number | bigint } };
|
|
39
|
+
transaction: <T>(fn: (...a: unknown[]) => T) => (...a: unknown[]) => T;
|
|
40
|
+
close: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Resolve the shared store path: explicit opt → AGENTDB_PATH env → `<project>/.dz/agentdb.db`. */
|
|
44
|
+
export function resolveAgentdbPath(projectRoot: string, dbPath?: string): string {
|
|
45
|
+
if (dbPath !== undefined && dbPath !== '') return dbPath;
|
|
46
|
+
const env = process.env['AGENTDB_PATH'];
|
|
47
|
+
return env !== undefined && env !== '' ? env : join(projectRoot, '.dz', 'agentdb.db');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** ReasoningBank's schema, verbatim — so the MCP server reads exactly what we insert. */
|
|
51
|
+
const REASONING_BANK_SCHEMA = `CREATE TABLE IF NOT EXISTS reasoning_patterns (
|
|
52
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
53
|
+
ts INTEGER DEFAULT (strftime('%s', 'now')),
|
|
54
|
+
task_type TEXT NOT NULL,
|
|
55
|
+
approach TEXT NOT NULL,
|
|
56
|
+
success_rate REAL NOT NULL DEFAULT 0.0,
|
|
57
|
+
uses INTEGER DEFAULT 0,
|
|
58
|
+
avg_reward REAL DEFAULT 0.0,
|
|
59
|
+
tags TEXT,
|
|
60
|
+
metadata TEXT
|
|
61
|
+
);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_patterns_task_type ON reasoning_patterns(task_type);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_patterns_success_rate ON reasoning_patterns(success_rate);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_patterns_uses ON reasoning_patterns(uses);
|
|
65
|
+
CREATE TABLE IF NOT EXISTS pattern_embeddings (
|
|
66
|
+
pattern_id INTEGER PRIMARY KEY,
|
|
67
|
+
embedding BLOB NOT NULL,
|
|
68
|
+
FOREIGN KEY (pattern_id) REFERENCES reasoning_patterns(id) ON DELETE CASCADE
|
|
69
|
+
);`;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Index `rows` into the shared AgentDB vector store. Returns `{indexed:0}` for an empty input and
|
|
73
|
+
* `{indexed:0, error}` when `agentdb`/`better-sqlite3` cannot be resolved from the project.
|
|
74
|
+
*/
|
|
75
|
+
export async function indexPatternsToAgentdb(
|
|
76
|
+
projectRoot: string,
|
|
77
|
+
rows: readonly AgentdbRow[],
|
|
78
|
+
opts: { dbPath?: string } = {},
|
|
79
|
+
): Promise<AgentdbIndexResult> {
|
|
80
|
+
if (rows.length === 0) return { indexed: 0 };
|
|
81
|
+
let sqliteUrl: string;
|
|
82
|
+
let agentdbDir: string;
|
|
83
|
+
try {
|
|
84
|
+
const req = createRequire(join(projectRoot, 'package.json'));
|
|
85
|
+
sqliteUrl = pathToFileURL(req.resolve('better-sqlite3')).href;
|
|
86
|
+
agentdbDir = dirname(req.resolve('agentdb'));
|
|
87
|
+
} catch {
|
|
88
|
+
return { indexed: 0, error: 'agentdb/better-sqlite3 not installed in project (run: dz setup --memory agentdb)' };
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const { default: Database } = (await import(sqliteUrl)) as { default: new (p: string) => NativeDb };
|
|
92
|
+
const { EmbeddingService } = (await import(pathToFileURL(join(agentdbDir, 'controllers', 'EmbeddingService.js')).href)) as {
|
|
93
|
+
EmbeddingService: new (o: object) => { initialize: () => Promise<void>; embed: (t: string) => Promise<Float32Array> };
|
|
94
|
+
};
|
|
95
|
+
const emb = new EmbeddingService({ model: 'Xenova/all-MiniLM-L6-v2', dimension: 384, provider: 'transformers' });
|
|
96
|
+
await emb.initialize();
|
|
97
|
+
|
|
98
|
+
const db = new Database(resolveAgentdbPath(projectRoot, opts.dbPath));
|
|
99
|
+
try {
|
|
100
|
+
db.pragma('journal_mode = WAL');
|
|
101
|
+
db.pragma('busy_timeout = 5000'); // wait out a brief MCP-server write lock instead of failing
|
|
102
|
+
db.exec(REASONING_BANK_SCHEMA);
|
|
103
|
+
const insPattern = db.prepare('INSERT INTO reasoning_patterns (task_type, approach, success_rate, uses, avg_reward, tags, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
|
104
|
+
const insEmb = db.prepare('INSERT OR REPLACE INTO pattern_embeddings (pattern_id, embedding) VALUES (?, ?)');
|
|
105
|
+
// Embeddings are async (can't run inside better-sqlite3's sync transaction) — compute them
|
|
106
|
+
// ALL first, then commit the writes atomically (QE P2: a mid-loop failure must not leave a
|
|
107
|
+
// partial batch reported as indexed:0).
|
|
108
|
+
const prepared: Array<{ row: AgentdbRow; vec: Float32Array }> = [];
|
|
109
|
+
for (const row of rows) prepared.push({ row, vec: await emb.embed(`${row.taskType}: ${row.text}`) });
|
|
110
|
+
const commit = db.transaction(() => {
|
|
111
|
+
for (const { row, vec } of prepared) {
|
|
112
|
+
const r = insPattern.run(
|
|
113
|
+
row.taskType, row.text, Math.max(0, Math.min(1, Number.isFinite(row.score) ? row.score : 0)), 0, 0.0,
|
|
114
|
+
row.tags ? JSON.stringify(row.tags) : null,
|
|
115
|
+
row.metadata ? JSON.stringify(row.metadata) : null,
|
|
116
|
+
);
|
|
117
|
+
insEmb.run(Number(r.lastInsertRowid), Buffer.from(vec.buffer));
|
|
118
|
+
}
|
|
119
|
+
return prepared.length;
|
|
120
|
+
});
|
|
121
|
+
return { indexed: commit() as number };
|
|
122
|
+
} finally {
|
|
123
|
+
db.close();
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
return { indexed: 0, error: `index failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/book-kb.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Book Knowledge Base — a lexical (FTS5) namespace for digitized-book Knowledge Units, kept
|
|
3
|
+
* **separate by construction** from the taught-pattern store (`.dz/memory/…`). Because it is its
|
|
4
|
+
* own file (`.dz/memory/books.sqlite`) with its own API, `loadPatterns`/`computePatternBoost`
|
|
5
|
+
* never see book KUs (ADR-001 v2 harness-integration P1: no boost pollution) and there is no
|
|
6
|
+
* retention expiry (books are permanent references, not decaying session patterns).
|
|
7
|
+
*
|
|
8
|
+
* The semantic (vector) layer lives in the shared agentdb store via {@link indexPatternsToAgentdb};
|
|
9
|
+
* this is the lexical layer for `dz recall --books`. Native better-sqlite3; best-effort.
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
16
|
+
import { pathToFileURL } from 'node:url';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
|
|
19
|
+
/** A digitized Knowledge Unit as stored in the book KB. */
|
|
20
|
+
export interface BookKU {
|
|
21
|
+
readonly book: string; // ISBN or book slug (the immutable upstream key)
|
|
22
|
+
readonly kuId: string;
|
|
23
|
+
readonly corpusVersion: string; // invalidation key: a re-ingest bumps this
|
|
24
|
+
readonly type: string;
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly problem: string;
|
|
27
|
+
readonly content: string;
|
|
28
|
+
readonly chapter?: string;
|
|
29
|
+
readonly pages?: readonly number[];
|
|
30
|
+
readonly metadata?: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A lexical hit from {@link queryBookKnowledge}. */
|
|
34
|
+
export interface BookKUHit {
|
|
35
|
+
readonly book: string;
|
|
36
|
+
readonly kuId: string;
|
|
37
|
+
readonly type: string;
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly problem: string;
|
|
40
|
+
readonly content: string;
|
|
41
|
+
readonly chapter?: string;
|
|
42
|
+
readonly pages?: readonly number[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface NativeDb {
|
|
46
|
+
pragma: (s: string) => void;
|
|
47
|
+
exec: (s: string) => void;
|
|
48
|
+
prepare: (q: string) => {
|
|
49
|
+
run: (...a: unknown[]) => unknown;
|
|
50
|
+
all: (...a: unknown[]) => unknown[];
|
|
51
|
+
};
|
|
52
|
+
transaction: <T>(fn: (...a: unknown[]) => T) => (...a: unknown[]) => T;
|
|
53
|
+
close: () => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Default lexical store path — a `memory/` sibling of the pattern store, never `agentdb.db`. */
|
|
57
|
+
export function bookKbPath(projectRoot: string): string {
|
|
58
|
+
return join(projectRoot, '.dz', 'memory', 'books.sqlite');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const SCHEMA = `CREATE VIRTUAL TABLE IF NOT EXISTS book_knowledge USING fts5(
|
|
62
|
+
book UNINDEXED, ku_id UNINDEXED, corpus_version UNINDEXED, type UNINDEXED,
|
|
63
|
+
name, problem, content, chapter UNINDEXED, pages UNINDEXED, metadata UNINDEXED
|
|
64
|
+
);`;
|
|
65
|
+
|
|
66
|
+
async function openDb(projectRoot: string, dbPath?: string): Promise<NativeDb | { error: string }> {
|
|
67
|
+
let sqliteUrl: string;
|
|
68
|
+
try {
|
|
69
|
+
const req = createRequire(join(projectRoot, 'package.json'));
|
|
70
|
+
sqliteUrl = pathToFileURL(req.resolve('better-sqlite3')).href;
|
|
71
|
+
} catch {
|
|
72
|
+
return { error: 'better-sqlite3 not installed in project (run: dz setup --memory agentdb)' };
|
|
73
|
+
}
|
|
74
|
+
const path = dbPath ?? bookKbPath(projectRoot);
|
|
75
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
76
|
+
const { default: Database } = (await import(sqliteUrl)) as { default: new (p: string) => NativeDb };
|
|
77
|
+
const db = new Database(path);
|
|
78
|
+
db.pragma('journal_mode = WAL');
|
|
79
|
+
db.pragma('busy_timeout = 5000');
|
|
80
|
+
db.exec(SCHEMA);
|
|
81
|
+
return db;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Upsert a batch of KUs for a book. Idempotent per (book, kuId): existing rows for the same
|
|
86
|
+
* kuId are replaced, and — since a re-ingest changes `corpusVersion` — every row for the book
|
|
87
|
+
* whose corpus_version differs from this batch's is evicted (stale-corpus cleanup), so the KB
|
|
88
|
+
* mirrors exactly the current ingest. Best-effort: returns an honest error, never throws.
|
|
89
|
+
*/
|
|
90
|
+
export async function putBookKnowledge(
|
|
91
|
+
projectRoot: string,
|
|
92
|
+
kus: readonly BookKU[],
|
|
93
|
+
opts: { dbPath?: string } = {},
|
|
94
|
+
): Promise<{ upserted: number; evicted: number; error?: string }> {
|
|
95
|
+
if (kus.length === 0) return { upserted: 0, evicted: 0 };
|
|
96
|
+
// Precondition (QE P3): a batch must be one book at one corpus_version — the eviction below is
|
|
97
|
+
// keyed on that. Reject a mixed batch instead of silently leaving stale rows.
|
|
98
|
+
const book = kus[0]!.book;
|
|
99
|
+
const corpusVersion = kus[0]!.corpusVersion;
|
|
100
|
+
if (kus.some((k) => k.book !== book || k.corpusVersion !== corpusVersion)) {
|
|
101
|
+
return { upserted: 0, evicted: 0, error: 'putBookKnowledge: batch must share one book + corpus_version' };
|
|
102
|
+
}
|
|
103
|
+
const opened = await openDb(projectRoot, opts.dbPath);
|
|
104
|
+
if ('error' in opened) return { upserted: 0, evicted: 0, error: opened.error };
|
|
105
|
+
const db = opened;
|
|
106
|
+
try {
|
|
107
|
+
// Atomic (QE P2): eviction + all inserts in ONE transaction — a mid-batch failure rolls the
|
|
108
|
+
// eviction back too, so we never lose rows and never leave a half-written book.
|
|
109
|
+
const del = db.prepare('DELETE FROM book_knowledge WHERE book = ? AND corpus_version != ?');
|
|
110
|
+
const delKu = db.prepare('DELETE FROM book_knowledge WHERE book = ? AND ku_id = ?');
|
|
111
|
+
const ins = db.prepare(`INSERT INTO book_knowledge
|
|
112
|
+
(book, ku_id, corpus_version, type, name, problem, content, chapter, pages, metadata)
|
|
113
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
114
|
+
const run = db.transaction(() => {
|
|
115
|
+
const evicted = (del.run(book, corpusVersion) as { changes?: number }).changes ?? 0;
|
|
116
|
+
for (const ku of kus) {
|
|
117
|
+
delKu.run(ku.book, ku.kuId);
|
|
118
|
+
ins.run(
|
|
119
|
+
ku.book, ku.kuId, ku.corpusVersion, ku.type, ku.name, ku.problem, ku.content,
|
|
120
|
+
ku.chapter ?? null, ku.pages ? JSON.stringify(ku.pages) : null,
|
|
121
|
+
ku.metadata ? JSON.stringify(ku.metadata) : null,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return { upserted: kus.length, evicted };
|
|
125
|
+
});
|
|
126
|
+
return run() as { upserted: number; evicted: number };
|
|
127
|
+
} catch (err) {
|
|
128
|
+
return { upserted: 0, evicted: 0, error: `book-kb upsert failed (rolled back): ${err instanceof Error ? err.message : String(err)}` };
|
|
129
|
+
} finally {
|
|
130
|
+
db.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Lexical (FTS5) search over the book KB. `book` filters to one book. Never throws. */
|
|
135
|
+
export async function queryBookKnowledge(
|
|
136
|
+
projectRoot: string,
|
|
137
|
+
query: string,
|
|
138
|
+
opts: { limit?: number; book?: string; dbPath?: string } = {},
|
|
139
|
+
): Promise<{ hits: BookKUHit[]; error?: string }> {
|
|
140
|
+
const path = opts.dbPath ?? bookKbPath(projectRoot);
|
|
141
|
+
if (!existsSync(path)) return { hits: [] };
|
|
142
|
+
const opened = await openDb(projectRoot, opts.dbPath);
|
|
143
|
+
if ('error' in opened) return { hits: [], error: opened.error };
|
|
144
|
+
const db = opened;
|
|
145
|
+
try {
|
|
146
|
+
const limit = opts.limit ?? 10;
|
|
147
|
+
// Build the FTS5 MATCH by AND-ing individually-quoted TERMS (QE P2): quoting the whole query as
|
|
148
|
+
// one phrase forced strict adjacency (a big recall regression); per-term quoting keeps
|
|
149
|
+
// injection safety (special chars stay literal inside each phrase) while allowing any-order
|
|
150
|
+
// matches. Empty/all-punctuation query → no MATCH (return no hits) rather than an FTS5 error.
|
|
151
|
+
const terms = query.split(/\s+/).map((t) => t.replace(/[^\p{L}\p{N}]+/gu, '')).filter(Boolean);
|
|
152
|
+
if (terms.length === 0) return { hits: [] };
|
|
153
|
+
const match = terms.map((t) => `"${t}"`).join(' AND ');
|
|
154
|
+
const rows = (opts.book !== undefined
|
|
155
|
+
? db.prepare('SELECT book, ku_id, type, name, problem, content, chapter, pages FROM book_knowledge WHERE book_knowledge MATCH ? AND book = ? ORDER BY rank LIMIT ?').all(match, opts.book, limit)
|
|
156
|
+
: db.prepare('SELECT book, ku_id, type, name, problem, content, chapter, pages FROM book_knowledge WHERE book_knowledge MATCH ? ORDER BY rank LIMIT ?').all(match, limit)
|
|
157
|
+
) as Array<{ book: string; ku_id: string; type: string; name: string; problem: string; content: string; chapter: string | null; pages: string | null }>;
|
|
158
|
+
return {
|
|
159
|
+
hits: rows.map((r) => ({
|
|
160
|
+
book: r.book, kuId: r.ku_id, type: r.type, name: r.name, problem: r.problem, content: r.content,
|
|
161
|
+
...(r.chapter !== null ? { chapter: r.chapter } : {}),
|
|
162
|
+
...(r.pages !== null ? { pages: JSON.parse(r.pages) as number[] } : {}),
|
|
163
|
+
})),
|
|
164
|
+
};
|
|
165
|
+
} catch (err) {
|
|
166
|
+
return { hits: [], error: `book-kb query failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
167
|
+
} finally {
|
|
168
|
+
db.close();
|
|
169
|
+
}
|
|
170
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -25,9 +25,13 @@ export { benchmarkSkill, benchmarkSkills, compareSkills } from './benchmark.js';
|
|
|
25
25
|
export { buildRegistry, searchRegistry, filterByCategory, skillPackBaseDirs, discoverSkillPackDirs } from './registry.js';
|
|
26
26
|
export { recommend } from './recommend.js';
|
|
27
27
|
export { pretrain } from './pretrain.js';
|
|
28
|
-
export { loadPatterns, loadSessions, computePatternBoost, readLearningConfig, BOOST_CAP, recordPattern, loadStorePatternsSync, patternToRecord, recordToPattern, consolidateSessions, recallPatterns } from './patterns.js';
|
|
29
|
-
export type { PatternRecord, SessionRecord, LearningConfig, LoadOptions, ConsolidateResult, ConsolidateOptions, SqliteBackendMode, RecallHit, SessionsSource } from './patterns.js';
|
|
28
|
+
export { loadPatterns, loadSessions, computePatternBoost, readLearningConfig, BOOST_CAP, recordPattern, loadStorePatternsSync, patternToRecord, recordToPattern, consolidateSessions, recallPatterns, pruneNoisePatterns } from './patterns.js';
|
|
29
|
+
export type { PatternRecord, SessionRecord, LearningConfig, LoadOptions, ConsolidateResult, ConsolidateOptions, SqliteBackendMode, RecallHit, SessionsSource, PruneNoiseResult } from './patterns.js';
|
|
30
30
|
export { runSetup, generateHooksConfig, generateAgentdbWriter, writerVersionOf, AGENTDB_WRITER_VERSION } from './setup.js';
|
|
31
|
+
export { indexPatternsToAgentdb, resolveAgentdbPath } from './agentdb-index.js';
|
|
32
|
+
export { putBookKnowledge, queryBookKnowledge, bookKbPath } from './book-kb.js';
|
|
33
|
+
export type { BookKU, BookKUHit } from './book-kb.js';
|
|
34
|
+
export type { AgentdbRow, AgentdbIndexResult } from './agentdb-index.js';
|
|
31
35
|
export { generatePlugin } from './plugin.js';
|
|
32
36
|
export type { PluginManifest } from './plugin.js';
|
|
33
37
|
export type { SetupOptions, SetupResult, SetupStep } from './setup.js';
|
package/src/operations.ts
CHANGED
|
@@ -526,6 +526,24 @@ export async function runDoctor(options: { projectRoot: string }): Promise<Docto
|
|
|
526
526
|
detail: '.claude/mcp.json is NOT loaded by Claude Code — re-run dz setup to register agentdb in .mcp.json (project root)',
|
|
527
527
|
});
|
|
528
528
|
}
|
|
529
|
+
// Flat dz hook entries (dz ≤0.3.43 emitted `{type,command}` without the matcher-group wrapper)
|
|
530
|
+
// are silently IGNORED by Claude Code — the writer never fires. Detect and point at the fix.
|
|
531
|
+
try {
|
|
532
|
+
const { commandsOf } = await import('./setup.js');
|
|
533
|
+
const settings = JSON.parse(readFileSync(join(root, '.claude', 'settings.json'), 'utf-8')) as {
|
|
534
|
+
hooks?: Record<string, unknown[]>;
|
|
535
|
+
};
|
|
536
|
+
const flatDz = Object.values(settings.hooks ?? {}).flat().filter((entry) =>
|
|
537
|
+
!Array.isArray((entry as { hooks?: unknown[] })?.hooks) &&
|
|
538
|
+
commandsOf(entry).some((cmd) => cmd.includes('agentdb-writer.mjs') || cmd.includes('sessions.jsonl')));
|
|
539
|
+
if (flatDz.length > 0) {
|
|
540
|
+
checks.push({
|
|
541
|
+
name: 'agentdb hooks shape',
|
|
542
|
+
ok: false,
|
|
543
|
+
detail: `${flatDz.length} dz hook entr(ies) use the legacy flat shape Claude Code ignores (writer never fires) — re-run dz setup to migrate`,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
} catch { /* settings absent/unreadable — covered by other checks */ }
|
|
529
547
|
const writerPath = join(root, '.dz', 'agentdb-writer.mjs');
|
|
530
548
|
if (existsSync(writerPath)) {
|
|
531
549
|
const { writerVersionOf, AGENTDB_WRITER_VERSION } = await import('./setup.js');
|
package/src/patterns.ts
CHANGED
|
Binary file
|
package/src/publish.ts
CHANGED
|
@@ -180,6 +180,10 @@ export function orderByDependencies<T extends { name: string; dir: string }>(pkg
|
|
|
180
180
|
* pre-setting the future version by hand). Exact-old-token matching keeps every other version
|
|
181
181
|
* string (dependency pins, historical notes, examples citing other releases) untouched.
|
|
182
182
|
* Returns the pre-sync README text for failure restore, or undefined when nothing was rewritten.
|
|
183
|
+
*
|
|
184
|
+
* Bootstrap invariant: exact-token matching MAINTAINS sync but cannot REPAIR pre-existing drift
|
|
185
|
+
* (a footer already one release behind contains a token != oldVersion and is skipped). Bring the
|
|
186
|
+
* footer to the current package.json version once; the mechanism owns it from then on.
|
|
183
187
|
*/
|
|
184
188
|
export function syncReadmeVersion(dir: string, oldVersion: string, newVersion: string): string | undefined {
|
|
185
189
|
const readmePath = join(dir, 'README.md');
|
package/src/registry.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
11
|
-
import { basename, dirname, join } from 'node:path';
|
|
11
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -31,16 +31,24 @@ export function skillPackBaseDirs(cwd: string): string[] {
|
|
|
31
31
|
if (!bases.includes(dir)) bases.push(dir);
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// Extra @scopes and local pack roots from `.dz/config.json` (ADR-001 v2): makes packs in other
|
|
35
|
+
// npm scopes (e.g. a private @mybooks registry) OR arbitrary local dirs first-class for
|
|
36
|
+
// discovery — the path for generated book packs distributed privately, not via public @dzhechkov.
|
|
37
|
+
const { skillScopes, skillDirs } = readSkillDiscoveryConfig(cwd);
|
|
38
|
+
const scopes = ['@dzhechkov', ...skillScopes];
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
for (const scope of scopes) add(join(cwd, 'packages', scope));
|
|
41
|
+
for (const scope of scopes) add(join(cwd, 'node_modules', scope));
|
|
42
|
+
|
|
43
|
+
// Self-location: harness-core is co-installed with the skill packs (siblings under the same
|
|
44
|
+
// scope dir, or one `node_modules` hop away). Walk up for each configured scope.
|
|
39
45
|
try {
|
|
40
46
|
let dir = dirname(fileURLToPath(import.meta.url)); // .../@dzhechkov/harness-core/dist
|
|
41
47
|
for (let depth = 0; depth < 8; depth += 1) {
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
for (const scope of scopes) {
|
|
49
|
+
if (basename(dir) === scope) add(dir);
|
|
50
|
+
add(join(dir, 'node_modules', scope));
|
|
51
|
+
}
|
|
44
52
|
const parent = dirname(dir);
|
|
45
53
|
if (parent === dir) break;
|
|
46
54
|
dir = parent;
|
|
@@ -49,9 +57,25 @@ export function skillPackBaseDirs(cwd: string): string[] {
|
|
|
49
57
|
// import.meta.url unavailable (unexpected in ESM) — cwd scans still apply.
|
|
50
58
|
}
|
|
51
59
|
|
|
60
|
+
// Arbitrary local pack roots (each directly contains `skills-*` dirs). Resolved against cwd.
|
|
61
|
+
for (const d of skillDirs) add(resolve(cwd, d));
|
|
62
|
+
|
|
52
63
|
return bases.filter((dir) => existsSync(dir));
|
|
53
64
|
}
|
|
54
65
|
|
|
66
|
+
/** Read `discovery.skillScopes[]` / `discovery.skillDirs[]` from `.dz/config.json` (empty on any error). */
|
|
67
|
+
function readSkillDiscoveryConfig(cwd: string): { skillScopes: string[]; skillDirs: string[] } {
|
|
68
|
+
try {
|
|
69
|
+
const cfg = JSON.parse(readFileSync(join(cwd, '.dz', 'config.json'), 'utf-8')) as {
|
|
70
|
+
discovery?: { skillScopes?: unknown; skillDirs?: unknown };
|
|
71
|
+
};
|
|
72
|
+
const arr = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : []);
|
|
73
|
+
return { skillScopes: arr(cfg.discovery?.skillScopes), skillDirs: arr(cfg.discovery?.skillDirs) };
|
|
74
|
+
} catch {
|
|
75
|
+
return { skillScopes: [], skillDirs: [] };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
55
79
|
/** List `skills-*` pack directories across all base dirs, de-duplicated by pack name (first wins). */
|
|
56
80
|
export function discoverSkillPackDirs(cwd: string): { pack: string; dir: string }[] {
|
|
57
81
|
const seen = new Set<string>();
|
|
@@ -155,6 +179,8 @@ function categoryFromPack(pack: string): string {
|
|
|
155
179
|
pack.includes('presentation')
|
|
156
180
|
)
|
|
157
181
|
return 'product';
|
|
182
|
+
// Digitized-book knowledge packs (ADR-001 book-knowledge-digitizer)
|
|
183
|
+
if (pack.includes('book')) return 'knowledge';
|
|
158
184
|
return 'other';
|
|
159
185
|
}
|
|
160
186
|
|
package/src/setup.ts
CHANGED
|
@@ -157,6 +157,17 @@ export function writerVersionOf(content: string): number {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/** Generate Claude Code hooks configuration for self-learning. */
|
|
160
|
+
/**
|
|
161
|
+
* Commands of a Claude Code hook entry in EITHER shape: the valid matcher-group form
|
|
162
|
+
* `{matcher?, hooks:[{type,command}]}` or the legacy flat `{type,command}` that dz ≤0.3.43
|
|
163
|
+
* emitted (Claude Code silently ignores flat entries — the writer-hooks bug; migrated on setup).
|
|
164
|
+
*/
|
|
165
|
+
export function commandsOf(entry: unknown): string[] {
|
|
166
|
+
const e = entry as { command?: unknown; hooks?: { command?: unknown }[] };
|
|
167
|
+
if (Array.isArray(e?.hooks)) return e.hooks.map((h) => String(h?.command ?? ''));
|
|
168
|
+
return [String(e?.command ?? '')];
|
|
169
|
+
}
|
|
170
|
+
|
|
160
171
|
export function generateHooksConfig(projectRoot: string, backend: MemoryBackend): string {
|
|
161
172
|
const dzDir = join(projectRoot, '.dz');
|
|
162
173
|
|
|
@@ -165,10 +176,13 @@ export function generateHooksConfig(projectRoot: string, backend: MemoryBackend)
|
|
|
165
176
|
// (ReasoningBank.storePattern) into the AGENTDB_PATH store the MCP server shares. The writer
|
|
166
177
|
// self-degrades to a sessions.jsonl marker on any failure, so no hook ever throws.
|
|
167
178
|
const writer = join(dzDir, 'agentdb-writer.mjs');
|
|
179
|
+
// Claude Code's hooks schema requires MATCHER-GROUP entries: `[{ hooks: [{type, command}] }]`.
|
|
180
|
+
// A flat `[{type, command}]` is silently ignored by Claude Code (QE find: writer hooks never
|
|
181
|
+
// fired), so wrap every entry. Session events take no matcher.
|
|
168
182
|
return JSON.stringify({
|
|
169
183
|
hooks: {
|
|
170
|
-
SessionStart: [{ type: 'command', command: `node ${JSON.stringify(writer)} start` }],
|
|
171
|
-
SessionEnd: [{ type: 'command', command: `node ${JSON.stringify(writer)} end` }],
|
|
184
|
+
SessionStart: [{ hooks: [{ type: 'command', command: `node ${JSON.stringify(writer)} start` }] }],
|
|
185
|
+
SessionEnd: [{ hooks: [{ type: 'command', command: `node ${JSON.stringify(writer)} end` }] }],
|
|
172
186
|
},
|
|
173
187
|
}, null, 2);
|
|
174
188
|
}
|
|
@@ -181,8 +195,8 @@ export function generateHooksConfig(projectRoot: string, backend: MemoryBackend)
|
|
|
181
195
|
`node -e "const fs=require('fs');const d=new Date().toISOString();fs.appendFileSync('.dz/sessions.jsonl',JSON.stringify({event:'${event}',ts:d,backend:'jsonl'})+'\\n')"`;
|
|
182
196
|
return JSON.stringify({
|
|
183
197
|
hooks: {
|
|
184
|
-
SessionStart: [{ type: 'command', command: jsonlCmd('start') }],
|
|
185
|
-
SessionEnd: [{ type: 'command', command: jsonlCmd('end') }],
|
|
198
|
+
SessionStart: [{ hooks: [{ type: 'command', command: jsonlCmd('start') }] }],
|
|
199
|
+
SessionEnd: [{ hooks: [{ type: 'command', command: jsonlCmd('end') }] }],
|
|
186
200
|
},
|
|
187
201
|
}, null, 2);
|
|
188
202
|
}
|
|
@@ -500,7 +514,7 @@ export function runSetup(opts: SetupOptions): SetupResult {
|
|
|
500
514
|
const settingsDir = join(opts.projectRoot, '.claude');
|
|
501
515
|
const settingsPath = join(settingsDir, 'settings.json');
|
|
502
516
|
const generated = JSON.parse(generateHooksConfig(opts.projectRoot, backend)) as {
|
|
503
|
-
hooks: Record<string, { type: string; command: string }[]>;
|
|
517
|
+
hooks: Record<string, { hooks: { type: string; command: string }[] }[]>;
|
|
504
518
|
};
|
|
505
519
|
|
|
506
520
|
if (!existsSync(settingsPath)) {
|
|
@@ -515,11 +529,14 @@ export function runSetup(opts: SetupOptions): SetupResult {
|
|
|
515
529
|
let replacedLegacy = false;
|
|
516
530
|
for (const event of Object.keys(generated.hooks)) {
|
|
517
531
|
const current = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
518
|
-
// Drop dz-generated entries (any vintage) — keep the user's own hooks
|
|
532
|
+
// Drop dz-generated entries (any vintage, either shape) — keep the user's own hooks
|
|
533
|
+
// untouched. Flat dz entries (≤0.3.43) are dropped too, migrating them to the valid
|
|
534
|
+
// matcher-group shape appended below.
|
|
519
535
|
const kept = current.filter((entry) => {
|
|
520
|
-
const
|
|
521
|
-
const isDz = cmd.includes('agentdb add') || cmd.includes('agentdb-writer.mjs') || cmd.includes('sessions.jsonl');
|
|
522
|
-
|
|
536
|
+
const cmds = commandsOf(entry);
|
|
537
|
+
const isDz = cmds.some((cmd) => cmd.includes('agentdb add') || cmd.includes('agentdb-writer.mjs') || cmd.includes('sessions.jsonl'));
|
|
538
|
+
const isFlat = !Array.isArray((entry as { hooks?: unknown[] })?.hooks);
|
|
539
|
+
if (isDz && (isFlat || cmds.some((cmd) => cmd.includes('agentdb add')))) replacedLegacy = true;
|
|
523
540
|
return !isDz;
|
|
524
541
|
});
|
|
525
542
|
const next = [...kept, ...generated.hooks[event]!];
|
|
@@ -605,10 +622,10 @@ export function runSetup(opts: SetupOptions): SetupResult {
|
|
|
605
622
|
if (!isAgentdbInstalledLocally(opts.projectRoot)) problems.push('deps missing (npm i agentdb better-sqlite3)');
|
|
606
623
|
try {
|
|
607
624
|
const settings = JSON.parse(readFileSync(join(opts.projectRoot, '.claude', 'settings.json'), 'utf-8')) as {
|
|
608
|
-
hooks?: Record<string,
|
|
625
|
+
hooks?: Record<string, unknown[]>;
|
|
609
626
|
};
|
|
610
627
|
const refs = ['SessionStart', 'SessionEnd'].every((ev) =>
|
|
611
|
-
(settings.hooks?.[ev] ?? []).some((h) =>
|
|
628
|
+
(settings.hooks?.[ev] ?? []).some((h) => commandsOf(h).some((cmd) => cmd.includes('agentdb-writer.mjs'))));
|
|
612
629
|
if (!refs) problems.push('hooks do not invoke the writer');
|
|
613
630
|
} catch {
|
|
614
631
|
problems.push('settings.json unreadable');
|