@dzhechkov/harness-core 0.3.39 → 0.3.41
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.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +52 -0
- package/dist/operations.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +38 -2
- package/dist/registry.js.map +1 -1
- package/dist/setup.d.ts +31 -2
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +305 -87
- package/dist/setup.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +1 -1
- package/src/operations.ts +52 -0
- package/src/registry.ts +39 -2
- package/src/setup.ts +310 -86
package/src/operations.ts
CHANGED
|
@@ -500,5 +500,57 @@ export async function runDoctor(options: { projectRoot: string }): Promise<Docto
|
|
|
500
500
|
});
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
// 8. agentdb brownfield health (gap G2) — only when agentdb artifacts are present, to keep
|
|
504
|
+
// non-agentdb projects noise-free. Detects installs from BEFORE the real-write fix.
|
|
505
|
+
const settingsPath = join(root, '.claude', 'settings.json');
|
|
506
|
+
const settingsRaw = existsSync(settingsPath) ? readFileSync(settingsPath, 'utf-8') : '';
|
|
507
|
+
if (settingsRaw.includes('agentdb add')) {
|
|
508
|
+
checks.push({
|
|
509
|
+
name: 'agentdb hooks',
|
|
510
|
+
ok: false,
|
|
511
|
+
detail: 'LEGACY broken hooks call non-existent `agentdb add` (they never wrote anything) — re-run: dz setup --target <t> --memory agentdb',
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
if (existsSync(join(root, '.dz', 'memory.rvf'))) {
|
|
515
|
+
checks.push({
|
|
516
|
+
name: 'agentdb store',
|
|
517
|
+
ok: false,
|
|
518
|
+
detail: 'orphan .dz/memory.rvf placeholder from an old setup (nothing reads it) — delete it; the real store is .dz/agentdb.db',
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
const legacyMcp = join(root, '.claude', 'mcp.json');
|
|
522
|
+
if (existsSync(legacyMcp) && readFileSync(legacyMcp, 'utf-8').includes('agentdb')) {
|
|
523
|
+
checks.push({
|
|
524
|
+
name: 'agentdb mcp location',
|
|
525
|
+
ok: false,
|
|
526
|
+
detail: '.claude/mcp.json is NOT loaded by Claude Code — re-run dz setup to register agentdb in .mcp.json (project root)',
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
const writerPath = join(root, '.dz', 'agentdb-writer.mjs');
|
|
530
|
+
if (existsSync(writerPath)) {
|
|
531
|
+
const { writerVersionOf, AGENTDB_WRITER_VERSION } = await import('./setup.js');
|
|
532
|
+
const deployed = writerVersionOf(readFileSync(writerPath, 'utf-8'));
|
|
533
|
+
if (deployed < AGENTDB_WRITER_VERSION) {
|
|
534
|
+
checks.push({
|
|
535
|
+
name: 'agentdb writer version',
|
|
536
|
+
ok: false,
|
|
537
|
+
detail: `deployed writer v${deployed} < current v${AGENTDB_WRITER_VERSION} — re-run dz setup to upgrade (no --force needed)`,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
// Version drift between the local agentdb copy and the .mcp.json pin (gap G7)
|
|
541
|
+
try {
|
|
542
|
+
const localVer = (JSON.parse(readFileSync(join(root, 'node_modules', 'agentdb', 'package.json'), 'utf-8')) as { version?: string }).version;
|
|
543
|
+
const mcp = JSON.parse(readFileSync(join(root, '.mcp.json'), 'utf-8')) as { mcpServers?: Record<string, { args?: string[] }> };
|
|
544
|
+
const pinned = mcp.mcpServers?.['agentdb']?.args?.[0];
|
|
545
|
+
if (localVer && pinned && pinned !== `agentdb@${localVer}`) {
|
|
546
|
+
checks.push({
|
|
547
|
+
name: 'agentdb version drift',
|
|
548
|
+
ok: false,
|
|
549
|
+
detail: `local agentdb@${localVer} != MCP pin ${pinned} (alpha schema drift risk on the shared DB) — re-run dz setup`,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
} catch { /* either side absent — covered by other checks */ }
|
|
553
|
+
}
|
|
554
|
+
|
|
503
555
|
return { node: process.version, checks, ok: checks.every((check) => check.ok) };
|
|
504
556
|
}
|
package/src/registry.ts
CHANGED
|
@@ -89,14 +89,51 @@ export interface Registry {
|
|
|
89
89
|
|
|
90
90
|
/** Extract description from SKILL.md frontmatter. */
|
|
91
91
|
function extractFrontmatter(content: string): { description: string; trustTier: number } {
|
|
92
|
-
const descMatch = /^description:\s*"?(.+?)"?\s*$/m.exec(content);
|
|
93
92
|
const tierMatch = /^trust_tier:\s*(\d)/m.exec(content);
|
|
94
93
|
return {
|
|
95
|
-
description:
|
|
94
|
+
description: extractDescription(content),
|
|
96
95
|
trustTier: tierMatch ? parseInt(tierMatch[1] ?? '1', 10) : 1,
|
|
97
96
|
};
|
|
98
97
|
}
|
|
99
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Read the `description:` value from YAML frontmatter. Handles two shapes:
|
|
101
|
+
* description: plain or "quoted" text → inline value on the same line
|
|
102
|
+
* description: > (also >- >+ | |- |+ and an → YAML block scalar; the text
|
|
103
|
+
* optional indent digit) lives on the following, more-
|
|
104
|
+
* indented lines
|
|
105
|
+
* Block scalars are folded to a single space-joined line for registry display,
|
|
106
|
+
* so the previous regex-only reader no longer captures the bare `>`/`|` marker.
|
|
107
|
+
*/
|
|
108
|
+
function extractDescription(content: string): string {
|
|
109
|
+
const lines = content.split(/\r?\n/);
|
|
110
|
+
const idx = lines.findIndex((l) => /^\s*description:/.test(l));
|
|
111
|
+
if (idx === -1) return '';
|
|
112
|
+
const keyLine = lines[idx] ?? '';
|
|
113
|
+
const keyIndent = keyLine.length - keyLine.trimStart().length;
|
|
114
|
+
const inline = keyLine.slice(keyLine.indexOf(':') + 1).trim();
|
|
115
|
+
|
|
116
|
+
// Block scalar indicator (`>`/`|` + optional chomping/indent chars): gather the
|
|
117
|
+
// following lines that are indented deeper than the key, until the block dedents.
|
|
118
|
+
if (/^[|>][+-]?\d?$/.test(inline)) {
|
|
119
|
+
const body: string[] = [];
|
|
120
|
+
for (let i = idx + 1; i < lines.length; i += 1) {
|
|
121
|
+
const line = lines[i] ?? '';
|
|
122
|
+
if (line.trim() === '') {
|
|
123
|
+
body.push('');
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const indent = line.length - line.trimStart().length;
|
|
127
|
+
if (indent <= keyIndent) break; // dedent → next key or end of frontmatter
|
|
128
|
+
body.push(line.trim());
|
|
129
|
+
}
|
|
130
|
+
return body.join(' ').replace(/\s+/g, ' ').trim();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Inline value — strip a single pair of surrounding quotes if present.
|
|
134
|
+
return inline.replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1');
|
|
135
|
+
}
|
|
136
|
+
|
|
100
137
|
/** Infer category from pack name. */
|
|
101
138
|
function categoryFromPack(pack: string): string {
|
|
102
139
|
if (pack.includes('devops')) return 'devops';
|
package/src/setup.ts
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Unlike `dz init` (skills only), `dz setup` configures the complete
|
|
5
5
|
* self-learning environment:
|
|
6
6
|
* 1. Skills installation (via init)
|
|
7
|
-
* 2. Claude Code hooks (
|
|
8
|
-
*
|
|
7
|
+
* 2. Claude Code session hooks (start/end) — with `--memory agentdb`, a real vector-store
|
|
8
|
+
* write via `.dz/agentdb-writer.mjs`; otherwise a `.dz/sessions.jsonl` marker
|
|
9
|
+
* 3. Memory store: `.dz/agentdb.db` (agentdb, shared with the MCP server via AGENTDB_PATH)
|
|
10
|
+
* or `.dz/sessions.jsonl` + `.dz/patterns.jsonl` (jsonl default)
|
|
9
11
|
* 4. Pretrain (project analysis → auto-recommend)
|
|
10
12
|
*
|
|
11
13
|
* All operations are additive — never overwrites existing files.
|
|
@@ -13,8 +15,9 @@
|
|
|
13
15
|
* @packageDocumentation
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
18
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
|
17
19
|
import { join } from 'node:path';
|
|
20
|
+
import { execSync } from 'node:child_process';
|
|
18
21
|
|
|
19
22
|
/** Memory backend type. */
|
|
20
23
|
export type MemoryBackend = 'jsonl' | 'agentdb';
|
|
@@ -48,38 +51,111 @@ export interface SetupStep {
|
|
|
48
51
|
readonly detail: string;
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
/** Absolute path to the agentdb store shared by the hook writer and the MCP server. */
|
|
55
|
+
function agentdbStorePath(projectRoot: string): string {
|
|
56
|
+
return join(projectRoot, '.dz', 'agentdb.db');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Version of the generated `.dz/agentdb-writer.mjs`. Bump on ANY change to
|
|
61
|
+
* {@link generateAgentdbWriter}'s output — setup regenerates deployed writers whose
|
|
62
|
+
* `dz-writer-version` stamp is older, WITHOUT requiring `--force` (audit gap G4: generated code
|
|
63
|
+
* must not fossilize outside the package lifecycle).
|
|
64
|
+
*/
|
|
65
|
+
export const AGENTDB_WRITER_VERSION = 2;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generate the `.dz/agentdb-writer.mjs` helper invoked by the session hooks.
|
|
69
|
+
*
|
|
70
|
+
* v2 (ADR-002, audit gaps G3 + code#1 + code#4): session markers are **metadata-only telemetry** —
|
|
71
|
+
* a plain row in the `dz_session_events` table inside the SAME shared `.dz/agentdb.db` the MCP
|
|
72
|
+
* server is pinned to (`AGENTDB_PATH`). No embedding, no model load, no `successRate`:
|
|
73
|
+
* - ~ms latency (v1 loaded a ~90 MB transformers model → 12 s cold-timeout losing the marker);
|
|
74
|
+
* - zero pollution of the HNSW index real learnings live in (those enter via `agentdb_*` MCP tools);
|
|
75
|
+
* - uses better-sqlite3 DIRECTLY (WAL + busy_timeout) — if it is unavailable the writer falls back
|
|
76
|
+
* to the jsonl marker instead of ever touching the sql.js in-memory backend, whose
|
|
77
|
+
* last-write-wins semantics were the design's one real corruption vector.
|
|
78
|
+
*
|
|
79
|
+
* Best-effort and never throws: any failure degrades to a `.dz/sessions.jsonl` line labelled
|
|
80
|
+
* `jsonl-fallback` (with the error), so the hook always exits 0 and never blocks a session.
|
|
81
|
+
*/
|
|
82
|
+
export function generateAgentdbWriter(projectRoot: string): string {
|
|
83
|
+
const dbPath = agentdbStorePath(projectRoot);
|
|
84
|
+
const sessionsPath = join(projectRoot, '.dz', 'sessions.jsonl');
|
|
85
|
+
return `#!/usr/bin/env node
|
|
86
|
+
// dz-writer-version: ${AGENTDB_WRITER_VERSION}
|
|
87
|
+
// Auto-generated by \`dz setup --memory agentdb\`. Do not edit — re-run \`dz setup\` to upgrade
|
|
88
|
+
// (setup regenerates automatically when this version stamp is outdated; --force not required).
|
|
89
|
+
// Writes a metadata-only session-event row into the shared AgentDB store (.dz/agentdb.db — the
|
|
90
|
+
// same file the agentdb MCP server is pinned to via AGENTDB_PATH). Real learnings go into the
|
|
91
|
+
// vector index via the agentdb_* MCP tools; this is deliberately non-semantic telemetry.
|
|
92
|
+
// Best-effort: on ANY error it appends a .dz/sessions.jsonl marker and exits 0 — never throws,
|
|
93
|
+
// never blocks the session.
|
|
94
|
+
import { appendFileSync } from 'node:fs';
|
|
95
|
+
|
|
96
|
+
const event = process.argv[2] === 'end' ? 'end' : 'start';
|
|
97
|
+
const ts = new Date().toISOString();
|
|
98
|
+
const DB = process.env.AGENTDB_PATH || ${JSON.stringify(dbPath)};
|
|
99
|
+
const SESSIONS = ${JSON.stringify(sessionsPath)};
|
|
100
|
+
|
|
101
|
+
function fallback(err) {
|
|
102
|
+
try {
|
|
103
|
+
appendFileSync(
|
|
104
|
+
SESSIONS,
|
|
105
|
+
JSON.stringify({ event, ts, backend: 'jsonl-fallback', error: String((err && err.message) || err) }) + '\\n',
|
|
106
|
+
);
|
|
107
|
+
} catch { /* last resort: swallow */ }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
// Native better-sqlite3 ONLY (prebuilt; synchronous; WAL). Never the sql.js fallback — its
|
|
112
|
+
// whole-file-in-memory persistence is unsafe against a concurrently-writing MCP server.
|
|
113
|
+
const { default: Database } = await import('better-sqlite3');
|
|
114
|
+
const db = new Database(DB);
|
|
115
|
+
db.pragma('journal_mode = WAL');
|
|
116
|
+
db.pragma('busy_timeout = 5000'); // wait out a brief MCP-server write lock instead of failing
|
|
117
|
+
db.exec('CREATE TABLE IF NOT EXISTS dz_session_events (id INTEGER PRIMARY KEY AUTOINCREMENT, event TEXT NOT NULL, ts TEXT NOT NULL, source TEXT NOT NULL DEFAULT \\'dz-session-hook\\')');
|
|
118
|
+
db.prepare('INSERT INTO dz_session_events (event, ts) VALUES (?, ?)').run(event, ts);
|
|
119
|
+
db.close();
|
|
120
|
+
} catch (err) {
|
|
121
|
+
fallback(err);
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Parse the `dz-writer-version` stamp from a deployed writer file ('' or absent → 0). */
|
|
127
|
+
export function writerVersionOf(content: string): number {
|
|
128
|
+
const m = /^\/\/ dz-writer-version:\s*(\d+)/m.exec(content);
|
|
129
|
+
return m ? parseInt(m[1] ?? '0', 10) : 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
51
132
|
/** Generate Claude Code hooks configuration for self-learning. */
|
|
52
|
-
function generateHooksConfig(projectRoot: string, backend: MemoryBackend): string {
|
|
133
|
+
export function generateHooksConfig(projectRoot: string, backend: MemoryBackend): string {
|
|
53
134
|
const dzDir = join(projectRoot, '.dz');
|
|
54
135
|
|
|
55
136
|
if (backend === 'agentdb') {
|
|
56
|
-
// agentdb backend: hooks
|
|
57
|
-
|
|
137
|
+
// agentdb backend: hooks invoke the generated writer, which does a REAL vector-store write
|
|
138
|
+
// (ReasoningBank.storePattern) into the AGENTDB_PATH store the MCP server shares. The writer
|
|
139
|
+
// self-degrades to a sessions.jsonl marker on any failure, so no hook ever throws.
|
|
140
|
+
const writer = join(dzDir, 'agentdb-writer.mjs');
|
|
58
141
|
return JSON.stringify({
|
|
59
142
|
hooks: {
|
|
60
|
-
SessionStart: [{
|
|
61
|
-
|
|
62
|
-
command: `node -e "const fs=require('fs');const d=new Date().toISOString();try{require('child_process').execSync('npx agentdb add \\'${rvfPath}\\' \\'session-start: '+d+'\\'',{stdio:'pipe'})}catch{fs.appendFileSync('${dzDir}/sessions.jsonl',JSON.stringify({event:'start',ts:d,backend:'agentdb'})+'\\n')}"`,
|
|
63
|
-
}],
|
|
64
|
-
SessionEnd: [{
|
|
65
|
-
type: 'command',
|
|
66
|
-
command: `node -e "const fs=require('fs');const d=new Date().toISOString();try{require('child_process').execSync('npx agentdb add \\'${rvfPath}\\' \\'session-end: '+d+'\\'',{stdio:'pipe'})}catch{fs.appendFileSync('${dzDir}/sessions.jsonl',JSON.stringify({event:'end',ts:d,backend:'agentdb'})+'\\n')}"`,
|
|
67
|
-
}],
|
|
143
|
+
SessionStart: [{ type: 'command', command: `node ${JSON.stringify(writer)} start` }],
|
|
144
|
+
SessionEnd: [{ type: 'command', command: `node ${JSON.stringify(writer)} end` }],
|
|
68
145
|
},
|
|
69
146
|
}, null, 2);
|
|
70
147
|
}
|
|
71
148
|
|
|
72
|
-
// JSONL backend (default)
|
|
149
|
+
// JSONL backend (default) — honest session bookkeeping, no vector store involved.
|
|
150
|
+
// Use a RELATIVE path (Claude Code runs hooks from the project root): interpolating the absolute
|
|
151
|
+
// ${dzDir} into a single-quoted JS literal inside shell double-quotes breaks on Windows backslash
|
|
152
|
+
// paths (\U, \b…) and on any path containing a quote. `.dz/sessions.jsonl` sidesteps all of it.
|
|
153
|
+
const jsonlCmd = (event: 'start' | 'end'): string =>
|
|
154
|
+
`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')"`;
|
|
73
155
|
return JSON.stringify({
|
|
74
156
|
hooks: {
|
|
75
|
-
SessionStart: [{
|
|
76
|
-
|
|
77
|
-
command: `node -e "const fs=require('fs');const d=new Date().toISOString();fs.appendFileSync('${dzDir}/sessions.jsonl',JSON.stringify({event:'start',ts:d})+'\\n')"`,
|
|
78
|
-
}],
|
|
79
|
-
SessionEnd: [{
|
|
80
|
-
type: 'command',
|
|
81
|
-
command: `node -e "const fs=require('fs');const d=new Date().toISOString();fs.appendFileSync('${dzDir}/sessions.jsonl',JSON.stringify({event:'end',ts:d})+'\\n')"`,
|
|
82
|
-
}],
|
|
157
|
+
SessionStart: [{ type: 'command', command: jsonlCmd('start') }],
|
|
158
|
+
SessionEnd: [{ type: 'command', command: jsonlCmd('end') }],
|
|
83
159
|
},
|
|
84
160
|
}, null, 2);
|
|
85
161
|
}
|
|
@@ -107,12 +183,18 @@ function generateDzConfig(target: string, preset: string | undefined, backend: M
|
|
|
107
183
|
},
|
|
108
184
|
memory: {
|
|
109
185
|
backend,
|
|
110
|
-
|
|
186
|
+
// agentdb: the native SQLite vector store shared by the session-hook writer and the
|
|
187
|
+
// agentdb MCP server (both pinned via AGENTDB_PATH). Real ReasoningBank patterns land here.
|
|
188
|
+
path: backend === 'agentdb' ? '.dz/agentdb.db' : '.dz/sessions.jsonl',
|
|
111
189
|
maxSizeMb: backend === 'agentdb' ? 100 : 10,
|
|
112
190
|
agentdb: backend === 'agentdb' ? {
|
|
113
191
|
learning: true,
|
|
114
192
|
vectorDim: 384,
|
|
115
193
|
mcpServer: 'agentdb',
|
|
194
|
+
// Both the hook writer and the MCP server read/write THIS path (env AGENTDB_PATH).
|
|
195
|
+
storePath: '.dz/agentdb.db',
|
|
196
|
+
embeddingModel: 'Xenova/all-MiniLM-L6-v2',
|
|
197
|
+
sessionHookWrites: true,
|
|
116
198
|
} : undefined,
|
|
117
199
|
},
|
|
118
200
|
hooks: {
|
|
@@ -122,11 +204,52 @@ function generateDzConfig(target: string, preset: string | undefined, backend: M
|
|
|
122
204
|
}, null, 2);
|
|
123
205
|
}
|
|
124
206
|
|
|
125
|
-
/**
|
|
126
|
-
function
|
|
207
|
+
/** True if `agentdb` resolves from the project's node_modules (the hook writer needs it there). */
|
|
208
|
+
function isAgentdbInstalledLocally(projectRoot: string): boolean {
|
|
209
|
+
return existsSync(join(projectRoot, 'node_modules', 'agentdb', 'package.json'));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The exact agentdb version installed in the project, or `'latest'` as a fallback. Used to pin the
|
|
214
|
+
* MCP server spec (`agentdb@<version>`) so the long-running MCP server and the hook writer — which
|
|
215
|
+
* imports the on-disk local copy — run the SAME schema against the shared DB (agentdb is alpha;
|
|
216
|
+
* `@latest` could drift the MCP server's schema away from what the hook wrote).
|
|
217
|
+
*/
|
|
218
|
+
function installedAgentdbSpec(projectRoot: string): string {
|
|
219
|
+
try {
|
|
220
|
+
const pkg = JSON.parse(readFileSync(join(projectRoot, 'node_modules', 'agentdb', 'package.json'), 'utf-8')) as { version?: string };
|
|
221
|
+
return pkg.version ? `agentdb@${pkg.version}` : 'agentdb@latest';
|
|
222
|
+
} catch {
|
|
223
|
+
return 'agentdb@latest';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Install `agentdb` + `better-sqlite3` as LOCAL project deps so the session-hook writer can
|
|
229
|
+
* `import('agentdb')` and get a native SQLite store (better-sqlite3 ships prebuilt binaries — no
|
|
230
|
+
* build tools — and gives true cross-process WAL concurrency so the hook and the MCP server share
|
|
231
|
+
* one live store). Best-effort: returns false (caller degrades to jsonl) if install fails.
|
|
232
|
+
*/
|
|
233
|
+
function installAgentdbLocally(projectRoot: string): boolean {
|
|
234
|
+
if (isAgentdbInstalledLocally(projectRoot)) return true;
|
|
127
235
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
236
|
+
// Anchor npm to THIS project: without a package.json here, npm's prefix walk-up would
|
|
237
|
+
// install into (and mutate the lockfile of) the nearest ANCESTOR project (audit code#2).
|
|
238
|
+
const pkgJsonPath = join(projectRoot, 'package.json');
|
|
239
|
+
if (!existsSync(pkgJsonPath)) {
|
|
240
|
+
writeFileSync(pkgJsonPath, JSON.stringify({ name: 'dz-harness-project', private: true, version: '0.0.0' }, null, 2) + '\n');
|
|
241
|
+
}
|
|
242
|
+
// NB: use the ESM-imported execSync — `require()` is undefined in this ESM module (the
|
|
243
|
+
// original agentdb hooks failed silently for exactly this reason). stdio:'ignore' (not
|
|
244
|
+
// 'pipe') avoids execSync's 1 MB maxBuffer aborting the child on npm's verbose output.
|
|
245
|
+
// --save-exact: agentdb is alpha; a semver range would let a later `npm update` drift the
|
|
246
|
+
// local copy away from the version the MCP registration pins (audit gap G7).
|
|
247
|
+
execSync('npm install agentdb better-sqlite3 --save-exact --no-audit --no-fund --loglevel=error', {
|
|
248
|
+
cwd: projectRoot,
|
|
249
|
+
stdio: 'ignore',
|
|
250
|
+
timeout: 300000,
|
|
251
|
+
});
|
|
252
|
+
return isAgentdbInstalledLocally(projectRoot);
|
|
130
253
|
} catch {
|
|
131
254
|
return false;
|
|
132
255
|
}
|
|
@@ -257,14 +380,19 @@ export function runSetup(opts: SetupOptions): SetupResult {
|
|
|
257
380
|
const dzDir = join(opts.projectRoot, '.dz');
|
|
258
381
|
const backend: MemoryBackend = opts.memory ?? 'jsonl';
|
|
259
382
|
|
|
260
|
-
// Step 0:
|
|
383
|
+
// Step 0: Install agentdb + better-sqlite3 locally so the session-hook writer can import them
|
|
384
|
+
// and share a native store with the MCP server. Best-effort — the writer self-degrades to a
|
|
385
|
+
// jsonl marker (and self-heals once the deps exist) if this fails.
|
|
261
386
|
if (backend === 'agentdb') {
|
|
262
|
-
const
|
|
263
|
-
if (
|
|
264
|
-
steps.push({ name: '
|
|
387
|
+
const ready = installAgentdbLocally(opts.projectRoot);
|
|
388
|
+
if (ready) {
|
|
389
|
+
steps.push({ name: 'Install agentdb + better-sqlite3', status: 'done', detail: 'local deps for real vector writes' });
|
|
265
390
|
} else {
|
|
266
|
-
steps.push({
|
|
267
|
-
|
|
391
|
+
steps.push({
|
|
392
|
+
name: 'Install agentdb + better-sqlite3',
|
|
393
|
+
status: 'error',
|
|
394
|
+
detail: 'install failed — hooks log to sessions.jsonl until you run: npm i agentdb better-sqlite3',
|
|
395
|
+
});
|
|
268
396
|
}
|
|
269
397
|
}
|
|
270
398
|
|
|
@@ -296,22 +424,26 @@ export function runSetup(opts: SetupOptions): SetupResult {
|
|
|
296
424
|
|
|
297
425
|
// Step 4: Initialize memory store
|
|
298
426
|
if (backend === 'agentdb') {
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
427
|
+
// Write the session-hook writer. The agentdb.db store itself is auto-created on first write
|
|
428
|
+
// by createDatabase() (both the writer and the MCP server init the schema), so there is no
|
|
429
|
+
// orphan placeholder file — the writer targets the real, shared native store.
|
|
430
|
+
const writerPath = join(dzDir, 'agentdb-writer.mjs');
|
|
431
|
+
// Regenerate when missing, forced, OR the deployed stamp is older than the current
|
|
432
|
+
// generator — deployed writers must not fossilize outside the package lifecycle (gap G4).
|
|
433
|
+
const deployedVersion = existsSync(writerPath) ? writerVersionOf(readFileSync(writerPath, 'utf-8')) : -1;
|
|
434
|
+
if (deployedVersion === -1 || opts.force || deployedVersion < AGENTDB_WRITER_VERSION) {
|
|
435
|
+
writeFileSync(writerPath, generateAgentdbWriter(opts.projectRoot));
|
|
436
|
+
steps.push({
|
|
437
|
+
name: 'Write agentdb-writer.mjs',
|
|
438
|
+
status: 'done',
|
|
439
|
+
detail: deployedVersion > -1 && deployedVersion < AGENTDB_WRITER_VERSION
|
|
440
|
+
? `upgraded v${deployedVersion} → v${AGENTDB_WRITER_VERSION}`
|
|
441
|
+
: `session telemetry writer v${AGENTDB_WRITER_VERSION}`,
|
|
442
|
+
});
|
|
311
443
|
} else {
|
|
312
|
-
steps.push({ name: '
|
|
444
|
+
steps.push({ name: 'Write agentdb-writer.mjs', status: 'skipped', detail: `current (v${deployedVersion})` });
|
|
313
445
|
}
|
|
314
|
-
//
|
|
446
|
+
// Keep the jsonl fallback log available for the writer's degraded path.
|
|
315
447
|
const sessionsPath = join(dzDir, 'sessions.jsonl');
|
|
316
448
|
if (!existsSync(sessionsPath)) writeFileSync(sessionsPath, '');
|
|
317
449
|
} else {
|
|
@@ -332,70 +464,162 @@ export function runSetup(opts: SetupOptions): SetupResult {
|
|
|
332
464
|
}
|
|
333
465
|
}
|
|
334
466
|
|
|
335
|
-
// Step 5: Configure hooks (write to .claude/settings.json)
|
|
467
|
+
// Step 5: Configure hooks (write to .claude/settings.json) — EVENT-LEVEL merge (gap G2):
|
|
468
|
+
// dz-generated entries (recognized by signature, incl. the broken legacy `agentdb add` hooks
|
|
469
|
+
// this feature fixes) are replaced in place WITHOUT --force; the user's own hooks and every
|
|
470
|
+
// other settings key are preserved. Full-file overwrite happens only when the file is absent.
|
|
336
471
|
if (!opts.noHooks) {
|
|
337
472
|
const settingsDir = join(opts.projectRoot, '.claude');
|
|
338
473
|
const settingsPath = join(settingsDir, 'settings.json');
|
|
474
|
+
const generated = JSON.parse(generateHooksConfig(opts.projectRoot, backend)) as {
|
|
475
|
+
hooks: Record<string, { type: string; command: string }[]>;
|
|
476
|
+
};
|
|
339
477
|
|
|
340
|
-
if (!existsSync(settingsPath)
|
|
478
|
+
if (!existsSync(settingsPath)) {
|
|
341
479
|
mkdirSync(settingsDir, { recursive: true });
|
|
342
|
-
writeFileSync(settingsPath,
|
|
480
|
+
writeFileSync(settingsPath, JSON.stringify({ hooks: generated.hooks }, null, 2));
|
|
343
481
|
steps.push({ name: 'Configure hooks', status: 'done', detail: `${backend} session hooks` });
|
|
344
482
|
} else {
|
|
345
483
|
try {
|
|
346
484
|
const existing = JSON.parse(readFileSync(settingsPath, 'utf-8')) as Record<string, unknown>;
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
485
|
+
const hooks = (existing['hooks'] ?? {}) as Record<string, unknown[]>;
|
|
486
|
+
let changed = false;
|
|
487
|
+
let replacedLegacy = false;
|
|
488
|
+
for (const event of Object.keys(generated.hooks)) {
|
|
489
|
+
const current = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
490
|
+
// Drop dz-generated entries (any vintage) — keep the user's own hooks untouched.
|
|
491
|
+
const kept = current.filter((entry) => {
|
|
492
|
+
const cmd = String((entry as { command?: unknown })?.command ?? '');
|
|
493
|
+
const isDz = cmd.includes('agentdb add') || cmd.includes('agentdb-writer.mjs') || cmd.includes('sessions.jsonl');
|
|
494
|
+
if (isDz && !cmd.includes('agentdb-writer.mjs')) replacedLegacy = true;
|
|
495
|
+
return !isDz;
|
|
496
|
+
});
|
|
497
|
+
const next = [...kept, ...generated.hooks[event]!];
|
|
498
|
+
if (JSON.stringify(next) !== JSON.stringify(current)) changed = true;
|
|
499
|
+
hooks[event] = next;
|
|
500
|
+
}
|
|
501
|
+
if (changed) {
|
|
502
|
+
existing['hooks'] = hooks;
|
|
350
503
|
writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
|
|
351
|
-
steps.push({
|
|
504
|
+
steps.push({
|
|
505
|
+
name: 'Configure hooks',
|
|
506
|
+
status: 'done',
|
|
507
|
+
detail: replacedLegacy ? `replaced legacy dz hooks with ${backend} hooks` : `merged ${backend} hooks (user hooks preserved)`,
|
|
508
|
+
});
|
|
352
509
|
} else {
|
|
353
|
-
steps.push({ name: 'Configure hooks', status: 'skipped', detail: 'hooks already
|
|
510
|
+
steps.push({ name: 'Configure hooks', status: 'skipped', detail: 'hooks already current' });
|
|
354
511
|
}
|
|
355
512
|
} catch {
|
|
356
|
-
steps.push({ name: 'Configure hooks', status: '
|
|
513
|
+
steps.push({ name: 'Configure hooks', status: 'error', detail: 'could not parse existing settings.json — fix it and re-run' });
|
|
357
514
|
}
|
|
358
515
|
}
|
|
359
516
|
} else {
|
|
360
517
|
steps.push({ name: 'Configure hooks', status: 'skipped', detail: '--no-hooks' });
|
|
361
518
|
}
|
|
362
519
|
|
|
363
|
-
// Step 5.5: Register agentdb MCP server (if agentdb backend)
|
|
520
|
+
// Step 5.5: Register agentdb MCP server (if agentdb backend) in **`.mcp.json` at the project
|
|
521
|
+
// root** — the project-scope location Claude Code actually loads (gap G5: the previously used
|
|
522
|
+
// `.claude/mcp.json` is NOT read by Claude Code, so the server never got AGENTDB_PATH and the
|
|
523
|
+
// shared-store invariant silently failed). READ-MERGE-WRITE: only the `agentdb` entry is
|
|
524
|
+
// managed; every other registered server is preserved (audit code#5 — never clobber).
|
|
364
525
|
if (backend === 'agentdb') {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
526
|
+
const agentdbEntry = {
|
|
527
|
+
command: 'npx',
|
|
528
|
+
// Pin to the INSTALLED agentdb version (not @latest) so the MCP server and the hook
|
|
529
|
+
// writer run the same alpha schema against one DB.
|
|
530
|
+
args: [installedAgentdbSpec(opts.projectRoot), 'mcp', 'start'],
|
|
531
|
+
// Pin the store to the SAME native DB the session-hook writer targets, so hook telemetry
|
|
532
|
+
// and agentdb_* pattern/reflexion data live in one shared store.
|
|
533
|
+
env: { AGENTDB_PATH: agentdbStorePath(opts.projectRoot) },
|
|
534
|
+
};
|
|
535
|
+
const mcpConfigPath = join(opts.projectRoot, '.mcp.json');
|
|
536
|
+
try {
|
|
537
|
+
const mcpConfig = (existsSync(mcpConfigPath)
|
|
538
|
+
? JSON.parse(readFileSync(mcpConfigPath, 'utf-8'))
|
|
539
|
+
: {}) as { mcpServers?: Record<string, unknown> };
|
|
540
|
+
const servers = mcpConfig.mcpServers ?? {};
|
|
541
|
+
const before = JSON.stringify(servers['agentdb']);
|
|
542
|
+
servers['agentdb'] = agentdbEntry;
|
|
543
|
+
mcpConfig.mcpServers = servers;
|
|
544
|
+
if (before !== JSON.stringify(agentdbEntry)) {
|
|
545
|
+
writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
|
|
546
|
+
steps.push({ name: 'Register agentdb MCP', status: 'done', detail: '.mcp.json: 41 tools, store pinned to .dz/agentdb.db' });
|
|
547
|
+
} else {
|
|
548
|
+
steps.push({ name: 'Register agentdb MCP', status: 'skipped', detail: 'already registered and current' });
|
|
549
|
+
}
|
|
550
|
+
} catch {
|
|
551
|
+
steps.push({ name: 'Register agentdb MCP', status: 'error', detail: '.mcp.json unparseable — fix it and re-run' });
|
|
552
|
+
}
|
|
553
|
+
// Migrate off the legacy location: `.claude/mcp.json` is not loaded by Claude Code. If it
|
|
554
|
+
// holds ONLY our old agentdb registration, remove the file; otherwise leave it and warn.
|
|
555
|
+
const legacyPath = join(opts.projectRoot, '.claude', 'mcp.json');
|
|
556
|
+
if (existsSync(legacyPath)) {
|
|
557
|
+
try {
|
|
558
|
+
const legacy = JSON.parse(readFileSync(legacyPath, 'utf-8')) as { mcpServers?: Record<string, unknown> };
|
|
559
|
+
const keys = Object.keys(legacy.mcpServers ?? {});
|
|
560
|
+
if (keys.length === 1 && keys[0] === 'agentdb') {
|
|
561
|
+
rmSync(legacyPath);
|
|
562
|
+
steps.push({ name: 'Migrate legacy .claude/mcp.json', status: 'done', detail: 'removed (not loaded by Claude Code); registration now in .mcp.json' });
|
|
563
|
+
} else {
|
|
564
|
+
steps.push({ name: 'Migrate legacy .claude/mcp.json', status: 'error', detail: 'contains other servers — Claude Code does NOT load this file; move them to .mcp.json' });
|
|
565
|
+
}
|
|
566
|
+
} catch {
|
|
567
|
+
steps.push({ name: 'Migrate legacy .claude/mcp.json', status: 'error', detail: 'unparseable legacy file — Claude Code does not load it; review manually' });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Step 5.9: agentdb wiring invariant check (audit code#3). Skip-branches across repeated runs
|
|
573
|
+
// can leave inconsistent combinations (e.g. writer+MCP present but hooks still jsonl). Verify
|
|
574
|
+
// the three-way invariant explicitly and surface a loud error step instead of silent "skipped"s.
|
|
575
|
+
if (backend === 'agentdb' && !opts.noHooks) {
|
|
576
|
+
const problems: string[] = [];
|
|
577
|
+
if (!isAgentdbInstalledLocally(opts.projectRoot)) problems.push('deps missing (npm i agentdb better-sqlite3)');
|
|
578
|
+
try {
|
|
579
|
+
const settings = JSON.parse(readFileSync(join(opts.projectRoot, '.claude', 'settings.json'), 'utf-8')) as {
|
|
580
|
+
hooks?: Record<string, { command?: string }[]>;
|
|
374
581
|
};
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
|
|
582
|
+
const refs = ['SessionStart', 'SessionEnd'].every((ev) =>
|
|
583
|
+
(settings.hooks?.[ev] ?? []).some((h) => String(h?.command ?? '').includes('agentdb-writer.mjs')));
|
|
584
|
+
if (!refs) problems.push('hooks do not invoke the writer');
|
|
585
|
+
} catch {
|
|
586
|
+
problems.push('settings.json unreadable');
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
const mcp = JSON.parse(readFileSync(join(opts.projectRoot, '.mcp.json'), 'utf-8')) as {
|
|
590
|
+
mcpServers?: Record<string, { env?: Record<string, string> }>;
|
|
591
|
+
};
|
|
592
|
+
if (mcp.mcpServers?.['agentdb']?.env?.['AGENTDB_PATH'] !== agentdbStorePath(opts.projectRoot)) {
|
|
593
|
+
problems.push('.mcp.json agentdb missing or not pinned to .dz/agentdb.db');
|
|
594
|
+
}
|
|
595
|
+
} catch {
|
|
596
|
+
problems.push('.mcp.json unreadable');
|
|
380
597
|
}
|
|
598
|
+
steps.push(problems.length === 0
|
|
599
|
+
? { name: 'agentdb wiring', status: 'done', detail: 'hooks → writer → .dz/agentdb.db ← MCP (one shared store)' }
|
|
600
|
+
: { name: 'agentdb wiring', status: 'error', detail: `INCOMPLETE: ${problems.join('; ')}` });
|
|
381
601
|
}
|
|
382
602
|
|
|
383
|
-
// Step 6: Update .gitignore
|
|
603
|
+
// Step 6: Update .gitignore. Append only the ENTRIES that are actually missing — a single
|
|
604
|
+
// sentinel check (e.g. sessions.jsonl, present in both backends) would skip agentdb.db/-wal/-shm
|
|
605
|
+
// on the documented jsonl→agentdb `--force` switch, leaking the binary store into git.
|
|
384
606
|
const gitignorePath = join(opts.projectRoot, '.gitignore');
|
|
385
|
-
const
|
|
386
|
-
? '
|
|
387
|
-
: '
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
607
|
+
const dzIgnoreLines = backend === 'agentdb'
|
|
608
|
+
? ['.dz/agentdb.db', '.dz/agentdb.db-wal', '.dz/agentdb.db-shm', '.dz/sessions.jsonl']
|
|
609
|
+
: ['.dz/sessions.jsonl', '.dz/patterns.jsonl'];
|
|
610
|
+
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf-8') : '';
|
|
611
|
+
const missing = dzIgnoreLines.filter((line) => !existing.split(/\r?\n/).includes(line));
|
|
612
|
+
if (missing.length > 0) {
|
|
613
|
+
const prefix = existing === '' ? '' : (existing.endsWith('\n') ? '' : '\n');
|
|
614
|
+
const block = `${prefix}\n# DZ Harness learning data\n${missing.join('\n')}\n`;
|
|
615
|
+
writeFileSync(gitignorePath, existing + block);
|
|
616
|
+
steps.push({
|
|
617
|
+
name: existsSync(gitignorePath) && existing !== '' ? 'Update .gitignore' : 'Create .gitignore',
|
|
618
|
+
status: 'done',
|
|
619
|
+
detail: `added ${missing.join(', ')}`,
|
|
620
|
+
});
|
|
396
621
|
} else {
|
|
397
|
-
|
|
398
|
-
steps.push({ name: 'Create .gitignore', status: 'done', detail: 'with .dz entries' });
|
|
622
|
+
steps.push({ name: 'Update .gitignore', status: 'skipped', detail: 'already ignoring .dz data' });
|
|
399
623
|
}
|
|
400
624
|
|
|
401
625
|
// Step 7: Install the CLI-driver skill + agent docs (--install-driver)
|