@dzhechkov/harness-core 0.8.47 → 0.8.48
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/.dz-manifest.json +43 -23
- package/README.md +47 -1
- package/dist/setup-memory-deps.d.ts +14 -0
- package/dist/setup-memory-deps.d.ts.map +1 -0
- package/dist/setup-memory-deps.js +210 -0
- package/dist/setup-memory-deps.js.map +1 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +379 -378
- package/dist/setup.js.map +1 -1
- package/dist/stage-usage.d.ts +102 -2
- package/dist/stage-usage.d.ts.map +1 -1
- package/dist/stage-usage.js +252 -2
- package/dist/stage-usage.js.map +1 -1
- package/dist/workflow-run-dispatch.d.ts +15 -0
- package/dist/workflow-run-dispatch.d.ts.map +1 -1
- package/dist/workflow-run-dispatch.js +50 -8
- package/dist/workflow-run-dispatch.js.map +1 -1
- package/dist/workflow-run.d.ts +8 -1
- package/dist/workflow-run.d.ts.map +1 -1
- package/dist/workflow-run.js +64 -0
- package/dist/workflow-run.js.map +1 -1
- package/package.json +1 -1
- package/sbom.json +72 -22
- package/src/setup-memory-deps.ts +166 -0
- package/src/setup.ts +95 -98
- package/src/stage-usage.ts +196 -2
- package/src/workflow-run-dispatch.ts +50 -5
- package/src/workflow-run.ts +61 -1
package/dist/setup.js
CHANGED
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
*
|
|
16
16
|
* @packageDocumentation
|
|
17
17
|
*/
|
|
18
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
|
18
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync, renameSync } from 'node:fs';
|
|
19
19
|
import { basename, dirname, isAbsolute, join, relative } from 'node:path';
|
|
20
|
-
import {
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import { randomUUID } from 'node:crypto';
|
|
22
|
+
import { reconcileMemoryDependencies } from './setup-memory-deps.js';
|
|
21
23
|
import { mergeManagedHookEntries } from './managed-hooks.js';
|
|
22
24
|
import { writeUniqueStampedFile } from './stamped-path.js';
|
|
23
25
|
import { CLAUDE_DESTRUCTIVE_HOOK_COMMAND, CLAUDE_DESTRUCTIVE_HOOK_MATCHER, CLAUDE_DESTRUCTIVE_HOOK_RELPATH, generateClaudeDestructiveHook, isDzManagedHookBody, } from './claude-hooks-assets.js';
|
|
@@ -452,10 +454,6 @@ function generateDzConfig(target, preset, backend) {
|
|
|
452
454
|
},
|
|
453
455
|
}, null, 2);
|
|
454
456
|
}
|
|
455
|
-
/** True if `agentdb` resolves from the project's node_modules (the hook writer needs it there). */
|
|
456
|
-
function isAgentdbInstalledLocally(projectRoot) {
|
|
457
|
-
return existsSync(join(projectRoot, 'node_modules', 'agentdb', 'package.json'));
|
|
458
|
-
}
|
|
459
457
|
/**
|
|
460
458
|
* The exact agentdb version installed in the project, or `'latest'` as a fallback. Used to pin the
|
|
461
459
|
* MCP server spec (`agentdb@<version>`) so the long-running MCP server and the hook writer — which
|
|
@@ -477,39 +475,6 @@ function installedAgentdbSpec(projectRoot) {
|
|
|
477
475
|
* build tools — and gives true cross-process WAL concurrency so the hook and the MCP server share
|
|
478
476
|
* one live store). Best-effort: returns false (caller degrades to jsonl) if install fails.
|
|
479
477
|
*/
|
|
480
|
-
function installAgentdbLocally(projectRoot) {
|
|
481
|
-
if (isAgentdbInstalledLocally(projectRoot))
|
|
482
|
-
return true;
|
|
483
|
-
try {
|
|
484
|
-
// Anchor npm to THIS project: without a package.json here, npm's prefix walk-up would
|
|
485
|
-
// install into (and mutate the lockfile of) the nearest ANCESTOR project (audit code#2).
|
|
486
|
-
const pkgJsonPath = join(projectRoot, 'package.json');
|
|
487
|
-
if (!existsSync(pkgJsonPath)) {
|
|
488
|
-
writeFileSync(pkgJsonPath, JSON.stringify({ name: 'dz-harness-project', private: true, version: '0.0.0' }, null, 2) + '\n');
|
|
489
|
-
}
|
|
490
|
-
// NB: use the ESM-imported execSync — `require()` is undefined in this ESM module (the
|
|
491
|
-
// original agentdb hooks failed silently for exactly this reason). stdio:'ignore' (not
|
|
492
|
-
// 'pipe') avoids execSync's 1 MB maxBuffer aborting the child on npm's verbose output.
|
|
493
|
-
// --save-exact: agentdb is alpha; a semver range would let a later `npm update` drift the
|
|
494
|
-
// local copy away from the version the MCP registration pins (audit gap G7).
|
|
495
|
-
//
|
|
496
|
-
// better-sqlite3@^11 (AM-2, dz-harness-hub issue #10 defect 1, MEASURED Node 20.20.2 with no
|
|
497
|
-
// `make` on PATH): an unpinned `npm install better-sqlite3` resolved 12.11.1, which ships no
|
|
498
|
-
// prebuilt binary for Node 20's ABI 115 — the install fell through to a node-gyp source build
|
|
499
|
-
// and failed on a machine with no C toolchain. `agentdb` itself requests `^11.8.1`, which DOES
|
|
500
|
-
// publish an ABI-115 prebuild, so pinning the range here costs nothing agentdb wasn't already
|
|
501
|
-
// going to resolve to, and buys a working install on a bare Node 20/22 host.
|
|
502
|
-
execSync('npm install agentdb better-sqlite3@^11 --save-exact --no-audit --no-fund --loglevel=error', {
|
|
503
|
-
cwd: projectRoot,
|
|
504
|
-
stdio: 'ignore',
|
|
505
|
-
timeout: 300000,
|
|
506
|
-
});
|
|
507
|
-
return isAgentdbInstalledLocally(projectRoot);
|
|
508
|
-
}
|
|
509
|
-
catch {
|
|
510
|
-
return false;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
478
|
/** Run full environment setup. */
|
|
514
479
|
/** Marker that brackets the dz-harness section in a shared CLAUDE.md/AGENTS.md. */
|
|
515
480
|
const DRIVER_MARKER_START = '<!-- dz-harness-driver:start -->';
|
|
@@ -856,20 +821,45 @@ export function runSetup(opts) {
|
|
|
856
821
|
// the comparison is against the PRIOR config, never the one this same call is about to produce.
|
|
857
822
|
const resolvedMemory = resolveSetupMemoryBackend(opts.projectRoot, opts.memory, opts.noMemory === true);
|
|
858
823
|
const backend = resolvedMemory.backend;
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
steps.push({ name: 'Install agentdb + better-sqlite3', status: 'done', detail: 'local deps for real vector writes' });
|
|
824
|
+
const configPath = join(dzDir, 'config.json');
|
|
825
|
+
let config;
|
|
826
|
+
const observedBackend = () => {
|
|
827
|
+
try {
|
|
828
|
+
const value = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
829
|
+
return value?.memory?.backend === 'agentdb' || value?.memory?.backend === 'jsonl' ? value.memory.backend : 'unknown';
|
|
866
830
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
831
|
+
catch {
|
|
832
|
+
return 'unknown';
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
const finish = () => ({
|
|
836
|
+
steps, totalSteps: steps.length, completed: steps.filter(step => step.status === 'done').length,
|
|
837
|
+
skipped: steps.filter(step => step.status === 'skipped').length,
|
|
838
|
+
memoryBackend: observedBackend() === 'unknown' ? resolvedMemory.backend : observedBackend(),
|
|
839
|
+
memoryBackendObserved: observedBackend(), memoryBackendSource: resolvedMemory.source,
|
|
840
|
+
memoryBackendDowngraded: resolvedMemory.downgraded,
|
|
841
|
+
});
|
|
842
|
+
try {
|
|
843
|
+
if (existsSync(configPath)) {
|
|
844
|
+
const value = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
845
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
846
|
+
throw Error('config must be an object');
|
|
847
|
+
config = value;
|
|
848
|
+
const memory = config['memory'];
|
|
849
|
+
if (memory === null || typeof memory !== 'object' || Array.isArray(memory) || !['jsonl', 'agentdb'].includes(String(memory['backend'])))
|
|
850
|
+
throw Error('memory.backend must be jsonl or agentdb');
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
catch (error) {
|
|
854
|
+
steps.push({ name: 'Memory saved state', status: 'error', detail: `unknown INCOMPLETE: malformed .dz/config.json preserved; ${String(error)}` });
|
|
855
|
+
return finish();
|
|
856
|
+
}
|
|
857
|
+
if (backend === 'agentdb' && !opts.noMemory) {
|
|
858
|
+
const deps = reconcileMemoryDependencies(opts.projectRoot);
|
|
859
|
+
steps.push({ name: 'Memory dependencies', status: deps.ready ? (deps.changedManifest || deps.changedLock || deps.detail.startsWith('repaired') ? 'done' : 'skipped') : 'error', detail: deps.detail });
|
|
860
|
+
if (!deps.ready) {
|
|
861
|
+
steps.push({ name: 'Memory backend transition', status: 'error', detail: `saved backend ${observedBackend()} INCOMPLETE before config persistence; dependency/npm/native failure; later memory phases not run` });
|
|
862
|
+
return finish();
|
|
873
863
|
}
|
|
874
864
|
}
|
|
875
865
|
// Step 1: Create .dz directory
|
|
@@ -880,351 +870,364 @@ export function runSetup(opts) {
|
|
|
880
870
|
else {
|
|
881
871
|
steps.push({ name: 'Create .dz directory', status: 'skipped', detail: 'already exists' });
|
|
882
872
|
}
|
|
883
|
-
//
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const configPath = join(dzDir, 'config.json');
|
|
888
|
-
if (!existsSync(configPath) || opts.force) {
|
|
889
|
-
writeFileSync(configPath, generateDzConfig(opts.target, opts.preset, backend));
|
|
890
|
-
steps.push({ name: 'Write .dz/config.json', status: 'done', detail: `${backend} backend` });
|
|
891
|
-
}
|
|
892
|
-
else if (resolvedMemory.downgraded) {
|
|
893
|
-
// Lead edit after Codex review (finding 3): a downgrade changes ONLY memory.backend — every other
|
|
894
|
-
// field the owner keeps in .dz/config.json survives; an unparsable file falls back to regeneration.
|
|
895
|
-
let rewritten = false;
|
|
896
|
-
try {
|
|
897
|
-
const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
898
|
-
const memory = (cfg['memory'] !== null && typeof cfg['memory'] === 'object') ? cfg['memory'] : {};
|
|
899
|
-
cfg['memory'] = { ...memory, backend };
|
|
900
|
-
writeFileSync(configPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
901
|
-
rewritten = true;
|
|
902
|
-
}
|
|
903
|
-
catch { /* fall through to regeneration */ }
|
|
904
|
-
if (!rewritten)
|
|
905
|
-
writeFileSync(configPath, generateDzConfig(opts.target, opts.preset, backend));
|
|
906
|
-
steps.push({ name: 'Write .dz/config.json', status: 'done', detail: `memory.backend → ${backend} (other fields kept)` });
|
|
907
|
-
}
|
|
908
|
-
else {
|
|
909
|
-
steps.push({ name: 'Write .dz/config.json', status: 'skipped', detail: 'already exists (use --force)' });
|
|
910
|
-
}
|
|
911
|
-
if (resolvedMemory.downgraded) {
|
|
912
|
-
steps.push({
|
|
913
|
-
name: 'Memory backend downgrade',
|
|
914
|
-
status: 'done',
|
|
915
|
-
detail: '⚠ memory backend downgraded agentdb → jsonl by --memory jsonl',
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
// Step 3: Initialize session log
|
|
919
|
-
const sessionsPath = join(dzDir, 'sessions.jsonl');
|
|
920
|
-
if (!existsSync(sessionsPath)) {
|
|
921
|
-
writeFileSync(sessionsPath, '');
|
|
922
|
-
steps.push({ name: 'Initialize sessions.jsonl', status: 'done', detail: 'session tracking ready' });
|
|
873
|
+
// Persist only the requested backend field of readable existing config, atomically.
|
|
874
|
+
const priorBackend = observedBackend();
|
|
875
|
+
if (opts.noMemory) {
|
|
876
|
+
steps.push({ name: 'Write .dz/config.json', status: 'skipped', detail: '--no-memory: existing memory configuration preserved' });
|
|
923
877
|
}
|
|
924
878
|
else {
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
// orphan placeholder file — the writer targets the real, shared native store.
|
|
932
|
-
const writerPath = join(dzDir, 'agentdb-writer.mjs');
|
|
933
|
-
// Regenerate when missing, forced, OR the deployed stamp is older than the current
|
|
934
|
-
// generator — deployed writers must not fossilize outside the package lifecycle (gap G4).
|
|
935
|
-
const deployedVersion = existsSync(writerPath) ? writerVersionOf(readFileSync(writerPath, 'utf-8')) : -1;
|
|
936
|
-
if (deployedVersion === -1 || opts.force || deployedVersion < AGENTDB_WRITER_VERSION) {
|
|
937
|
-
writeFileSync(writerPath, generateAgentdbWriter(opts.projectRoot));
|
|
938
|
-
steps.push({
|
|
939
|
-
name: 'Write agentdb-writer.mjs',
|
|
940
|
-
status: 'done',
|
|
941
|
-
detail: deployedVersion > -1 && deployedVersion < AGENTDB_WRITER_VERSION
|
|
942
|
-
? `upgraded v${deployedVersion} → v${AGENTDB_WRITER_VERSION}`
|
|
943
|
-
: `session telemetry writer v${AGENTDB_WRITER_VERSION}`,
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
else {
|
|
947
|
-
steps.push({ name: 'Write agentdb-writer.mjs', status: 'skipped', detail: `current (v${deployedVersion})` });
|
|
948
|
-
}
|
|
949
|
-
// Keep the jsonl fallback log available for the writer's degraded path.
|
|
950
|
-
const sessionsPath = join(dzDir, 'sessions.jsonl');
|
|
951
|
-
if (!existsSync(sessionsPath))
|
|
952
|
-
writeFileSync(sessionsPath, '');
|
|
953
|
-
}
|
|
954
|
-
else {
|
|
955
|
-
// JSONL backend
|
|
956
|
-
const sessionsPath = join(dzDir, 'sessions.jsonl');
|
|
957
|
-
if (!existsSync(sessionsPath)) {
|
|
958
|
-
writeFileSync(sessionsPath, '');
|
|
959
|
-
steps.push({ name: 'Initialize sessions.jsonl', status: 'done', detail: 'session tracking ready' });
|
|
960
|
-
}
|
|
961
|
-
else {
|
|
962
|
-
steps.push({ name: 'Initialize sessions.jsonl', status: 'skipped', detail: 'already exists' });
|
|
963
|
-
}
|
|
964
|
-
const patternsPath = join(dzDir, 'patterns.jsonl');
|
|
965
|
-
if (!existsSync(patternsPath)) {
|
|
966
|
-
writeFileSync(patternsPath, '');
|
|
967
|
-
steps.push({ name: 'Initialize patterns.jsonl', status: 'done', detail: 'pattern learning ready' });
|
|
968
|
-
}
|
|
969
|
-
else {
|
|
970
|
-
steps.push({ name: 'Initialize patterns.jsonl', status: 'skipped', detail: 'already exists' });
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
// Step 4.6: Install apply-leg — the WORK happens here (before "Configure hooks" writes
|
|
974
|
-
// SessionStart), so a foreign SessionStart entry is already in place before that step's own
|
|
975
|
-
// merge ever sees it; see `applyLegStepResult`'s doc for why order matters. The STEP is reported
|
|
976
|
-
// further down, after "Configure hooks" pushes its own, so the printed order still reads as
|
|
977
|
-
// "collect → rank → apply".
|
|
978
|
-
const applyLegStep = applyLegStepResult(opts, backend);
|
|
979
|
-
// Step 5: Configure hooks (write to .claude/settings.json) — EVENT-LEVEL merge (gap G2):
|
|
980
|
-
// dz-generated entries (recognized by signature, incl. the broken legacy `agentdb add` hooks
|
|
981
|
-
// this feature fixes) are replaced in place WITHOUT --force; the user's own hooks and every
|
|
982
|
-
// other settings key are preserved. Full-file overwrite happens only when the file is absent.
|
|
983
|
-
if (!opts.noHooks) {
|
|
984
|
-
const settingsDir = join(opts.projectRoot, '.claude');
|
|
985
|
-
const settingsPath = join(settingsDir, 'settings.json');
|
|
986
|
-
// The BODY goes in first, and the ENTRY goes in only after a LIVE receipt that the body runs
|
|
987
|
-
// and refuses. Written from the INSTALLED package, never copied out of our repository — a
|
|
988
|
-
// consumer has no `packages/@dzhechkov/...` above their project.
|
|
989
|
-
//
|
|
990
|
-
// Round 4, P2: these two used to be independent. A failed write was recorded as an error and
|
|
991
|
-
// the entry was merged anyway, so a consumer whose install failed got a `PreToolUse` entry
|
|
992
|
-
// pointing at something that is not a runnable hook — and that breaks EVERY Bash call, not one.
|
|
993
|
-
//
|
|
994
|
-
// Round 5, P1: the write was also UNCONDITIONAL. Setup is additive everywhere else — the
|
|
995
|
-
// settings merge keeps the user's own hooks, `.gitignore` is appended to, an existing skill is
|
|
996
|
-
// skipped — and this path overwrote a well-known filename with no ownership check, so a
|
|
997
|
-
// consumer's hand-authored `.claude/hooks/destructive-guard.cjs` was destroyed by a routine
|
|
998
|
-
// run. A body we wrote carries a MARKER; a file without it is the consumer's, and only an
|
|
999
|
-
// explicit `--force` may replace it, after a timestamped backup.
|
|
1000
|
-
const hookPath = join(opts.projectRoot, ...CLAUDE_DESTRUCTIVE_HOOK_RELPATH.split('/'));
|
|
1001
|
-
let installError = null;
|
|
1002
|
-
let preserved = null;
|
|
1003
|
-
let backupPath = null;
|
|
1004
|
-
// Unreadable (absent, or something that is not a file at all) is NOT a claim of ownership: it
|
|
1005
|
-
// falls through to the write, whose failure the round-4 receipt below already reports.
|
|
1006
|
-
let current = null;
|
|
1007
|
-
try {
|
|
1008
|
-
current = readFileSync(hookPath, 'utf-8');
|
|
1009
|
-
}
|
|
1010
|
-
catch {
|
|
1011
|
-
current = null;
|
|
1012
|
-
}
|
|
1013
|
-
const foreign = current !== null && !isDzManagedHookBody(current);
|
|
1014
|
-
if (foreign && opts.force !== true) {
|
|
1015
|
-
preserved =
|
|
1016
|
-
'файл на этом пути не наш (нет маркера dz) — ОСТАВЛЕН нетронутым и НЕ ЗАПУСКАЛСЯ; запись в settings.json на этот путь тоже не трогаем (ни своей не добавляем, ни вашу не снимаем); заменить: dz setup --force';
|
|
1017
|
-
}
|
|
1018
|
-
else {
|
|
879
|
+
const transition = config !== undefined && opts.memory !== undefined && priorBackend !== backend;
|
|
880
|
+
if (config === undefined || opts.force || transition) {
|
|
881
|
+
const next = config !== undefined
|
|
882
|
+
? { ...config, memory: { ...config['memory'], backend } }
|
|
883
|
+
: JSON.parse(generateDzConfig(opts.target, opts.preset, backend));
|
|
884
|
+
const temporary = `${configPath}.${randomUUID()}.tmp`;
|
|
1019
885
|
try {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1024
|
-
backupPath = writeUniqueStampedFile(`${hookPath}.bak-`, stamp, current, writeFileSync);
|
|
1025
|
-
}
|
|
1026
|
-
mkdirSync(dirname(hookPath), { recursive: true });
|
|
1027
|
-
writeFileSync(hookPath, generateClaudeDestructiveHook(), { mode: 0o755 });
|
|
886
|
+
writeFileSync(temporary, JSON.stringify(next, null, 2) + '\n', { flag: 'wx' });
|
|
887
|
+
renameSync(temporary, configPath);
|
|
888
|
+
steps.push({ name: 'Write .dz/config.json', status: 'done', detail: transition ? `memory.backend ${priorBackend} → ${backend}; all other config fields kept` : `${backend} backend` });
|
|
1028
889
|
}
|
|
1029
|
-
catch (
|
|
1030
|
-
|
|
890
|
+
catch (error) {
|
|
891
|
+
if (existsSync(temporary))
|
|
892
|
+
rmSync(temporary);
|
|
893
|
+
steps.push({ name: 'Memory backend transition', status: 'error', detail: `saved backend ${observedBackend()} INCOMPLETE: config persistence failed; later memory wiring not run; ${String(error)}` });
|
|
894
|
+
return finish();
|
|
1031
895
|
}
|
|
1032
896
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
:
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
// does not refuse gets no entry, exactly like a failed install (round 4).
|
|
1068
|
-
const notes = [
|
|
1069
|
-
preserved === null ? '' : `${preserved}; `,
|
|
1070
|
-
backupPath === null ? '' : `прежний файл сохранён: ${basename(backupPath)}; `,
|
|
1071
|
-
installError === null ? '' : `${installError}; `,
|
|
1072
|
-
].join('');
|
|
1073
|
-
steps.push(guardArmed
|
|
1074
|
-
? { name: 'Install destructive guard', status: 'done', detail: `${notes}${CLAUDE_DESTRUCTIVE_HOOK_RELPATH} — ${receipt.detail}` }
|
|
1075
|
-
: {
|
|
1076
|
-
name: 'Install destructive guard',
|
|
1077
|
-
status: preserved === null ? 'error' : 'skipped',
|
|
1078
|
-
detail: `${notes}${receipt.detail} — запись в settings.json НЕ добавлена`,
|
|
1079
|
-
});
|
|
1080
|
-
const generated = JSON.parse(generateHooksConfig(opts.projectRoot, backend));
|
|
1081
|
-
// No working body ⇒ no entry, and the EVENT KEY STAYS — as an empty managed list when nothing
|
|
1082
|
-
// else of ours belongs there.
|
|
1083
|
-
//
|
|
1084
|
-
// CORRECTION OF RECORD (round 7, P1). The round-5 version DELETED the key and this comment
|
|
1085
|
-
// claimed the merge would then also drop a guard entry left by an earlier setup. That was
|
|
1086
|
-
// asserted without measuring and it is false: `mergeManagedHookEntries` iterates
|
|
1087
|
-
// `Object.keys(managed)`, so an event absent from the managed input is copied through
|
|
1088
|
-
// UNTOUCHED — a project whose guard used to be armed kept invoking it on every Bash call while
|
|
1089
|
-
// the report said the entry was not added. Handing the event an EMPTY list is what makes the
|
|
1090
|
-
// merge EXAMINE it: our entries are dropped by `isManaged`, the user's are preserved in order,
|
|
1091
|
-
// and nothing is appended. The round-5 test passed for the wrong reason — its project had no
|
|
1092
|
-
// pre-existing settings.json, so there was no stale entry for the claim to be wrong about.
|
|
1093
|
-
if (!guardArmed) {
|
|
1094
|
-
generated.hooks['PreToolUse'] = (generated.hooks['PreToolUse'] ?? []).filter((entry) => !entry.hooks.some((h) => isManagedClaudeDestructiveHookCommand(h.command)));
|
|
1095
|
-
}
|
|
1096
|
-
if (!existsSync(settingsPath)) {
|
|
1097
|
-
mkdirSync(settingsDir, { recursive: true });
|
|
1098
|
-
writeFileSync(settingsPath, JSON.stringify({ hooks: generated.hooks }, null, 2));
|
|
1099
|
-
steps.push({ name: 'Configure hooks', status: 'done', detail: `${backend} session hooks` });
|
|
1100
|
-
}
|
|
1101
|
-
else {
|
|
1102
|
-
try {
|
|
1103
|
-
const existing = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
1104
|
-
// ONE merge implementation, shared with the Codex target (AM-3 / G-E). Claude's exact
|
|
1105
|
-
// command attribution is passed in rather than reimplemented, so emitted bytes, report
|
|
1106
|
-
// tail text, and the no-write path stay on the shared merge contract (AM-37).
|
|
1107
|
-
const isManagedCommand = (cmd) => cmd.includes('agentdb add') ||
|
|
1108
|
-
cmd.includes('agentdb-writer.mjs') ||
|
|
1109
|
-
cmd.includes('sessions.jsonl') ||
|
|
1110
|
-
// Ours ONLY while the body at that path is ours (round 8, P2). Without the
|
|
1111
|
-
// path clause a second `dz setup` would append a duplicate guard entry instead of
|
|
1112
|
-
// replacing the first; without the ownership clause it would delete the entry a
|
|
1113
|
-
// consumer wrote for their own preserved hook.
|
|
1114
|
-
(!foreignBodyKept && isManagedClaudeDestructiveHookCommand(cmd));
|
|
1115
|
-
const plan = mergeManagedHookEntries((existing['hooks'] ?? {}), generated.hooks, {
|
|
1116
|
-
// Drop dz-generated entries (any vintage, either shape) — keep the user's own hooks
|
|
1117
|
-
// untouched. Flat dz entries (≤0.3.43) are dropped too, migrating them to the valid
|
|
1118
|
-
// matcher-group shape appended below.
|
|
1119
|
-
isManaged: (entry) => commandsOf(entry).some(isManagedCommand),
|
|
1120
|
-
isLegacy: (entry) => !Array.isArray(entry?.hooks) ||
|
|
1121
|
-
commandsOf(entry).some((cmd) => cmd.includes('agentdb add')),
|
|
1122
|
-
// Ownership is per HANDLER, not per matcher group. A user's handler may deliberately
|
|
1123
|
-
// share the Bash group with dz's guard; replacing ours must retain their handler object
|
|
1124
|
-
// and every surrounding group field byte-for-byte through JSON serialization.
|
|
1125
|
-
retainForeign: (entry) => {
|
|
1126
|
-
const grouped = entry;
|
|
1127
|
-
if (!Array.isArray(grouped?.hooks))
|
|
1128
|
-
return null;
|
|
1129
|
-
const kept = grouped.hooks.filter((hook) => !isManagedCommand(String(hook?.command ?? '')));
|
|
1130
|
-
return kept.length === 0
|
|
1131
|
-
? null
|
|
1132
|
-
: { ...entry, hooks: kept };
|
|
1133
|
-
},
|
|
1134
|
-
reportLabel: backend,
|
|
1135
|
-
});
|
|
1136
|
-
if (plan.changed) {
|
|
1137
|
-
existing['hooks'] = plan.hooks;
|
|
1138
|
-
writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
|
|
1139
|
-
steps.push({ name: 'Configure hooks', status: 'done', detail: plan.report });
|
|
897
|
+
else
|
|
898
|
+
steps.push({ name: 'Write .dz/config.json', status: 'skipped', detail: 'already current; existing config preserved' });
|
|
899
|
+
if (resolvedMemory.downgraded)
|
|
900
|
+
steps.push({ name: 'Memory backend downgrade', status: 'done', detail: '⚠ memory backend downgraded agentdb → jsonl by --memory jsonl; other config fields kept' });
|
|
901
|
+
}
|
|
902
|
+
try {
|
|
903
|
+
if (!opts.noMemory) {
|
|
904
|
+
// Step 3: Initialize session log
|
|
905
|
+
const sessionsPath = join(dzDir, 'sessions.jsonl');
|
|
906
|
+
if (!existsSync(sessionsPath)) {
|
|
907
|
+
writeFileSync(sessionsPath, '');
|
|
908
|
+
steps.push({ name: 'Initialize sessions.jsonl', status: 'done', detail: 'session tracking ready' });
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
steps.push({ name: 'Initialize sessions.jsonl', status: 'skipped', detail: 'already exists' });
|
|
912
|
+
}
|
|
913
|
+
// Step 4: Initialize memory store
|
|
914
|
+
if (backend === 'agentdb') {
|
|
915
|
+
// Write the session-hook writer. The agentdb.db store itself is auto-created on first write
|
|
916
|
+
// by createDatabase() (both the writer and the MCP server init the schema), so there is no
|
|
917
|
+
// orphan placeholder file — the writer targets the real, shared native store.
|
|
918
|
+
const writerPath = join(dzDir, 'agentdb-writer.mjs');
|
|
919
|
+
// Regenerate when missing, forced, OR the deployed stamp is older than the current
|
|
920
|
+
// generator — deployed writers must not fossilize outside the package lifecycle (gap G4).
|
|
921
|
+
const deployedVersion = existsSync(writerPath) ? writerVersionOf(readFileSync(writerPath, 'utf-8')) : -1;
|
|
922
|
+
if (deployedVersion === -1 || opts.force || deployedVersion < AGENTDB_WRITER_VERSION) {
|
|
923
|
+
writeFileSync(writerPath, generateAgentdbWriter(opts.projectRoot));
|
|
924
|
+
steps.push({
|
|
925
|
+
name: 'Write agentdb-writer.mjs',
|
|
926
|
+
status: 'done',
|
|
927
|
+
detail: deployedVersion > -1 && deployedVersion < AGENTDB_WRITER_VERSION
|
|
928
|
+
? `upgraded v${deployedVersion} → v${AGENTDB_WRITER_VERSION}`
|
|
929
|
+
: `session telemetry writer v${AGENTDB_WRITER_VERSION}`,
|
|
930
|
+
});
|
|
1140
931
|
}
|
|
1141
932
|
else {
|
|
1142
|
-
steps.push({ name: '
|
|
933
|
+
steps.push({ name: 'Write agentdb-writer.mjs', status: 'skipped', detail: `current (v${deployedVersion})` });
|
|
1143
934
|
}
|
|
935
|
+
// Keep the jsonl fallback log available for the writer's degraded path.
|
|
936
|
+
const sessionsPath = join(dzDir, 'sessions.jsonl');
|
|
937
|
+
if (!existsSync(sessionsPath))
|
|
938
|
+
writeFileSync(sessionsPath, '');
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
// JSONL backend
|
|
942
|
+
const sessionsPath = join(dzDir, 'sessions.jsonl');
|
|
943
|
+
if (!existsSync(sessionsPath)) {
|
|
944
|
+
writeFileSync(sessionsPath, '');
|
|
945
|
+
steps.push({ name: 'Initialize sessions.jsonl', status: 'done', detail: 'session tracking ready' });
|
|
946
|
+
}
|
|
947
|
+
else {
|
|
948
|
+
steps.push({ name: 'Initialize sessions.jsonl', status: 'skipped', detail: 'already exists' });
|
|
949
|
+
}
|
|
950
|
+
const patternsPath = join(dzDir, 'patterns.jsonl');
|
|
951
|
+
if (!existsSync(patternsPath)) {
|
|
952
|
+
writeFileSync(patternsPath, '');
|
|
953
|
+
steps.push({ name: 'Initialize patterns.jsonl', status: 'done', detail: 'pattern learning ready' });
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
steps.push({ name: 'Initialize patterns.jsonl', status: 'skipped', detail: 'already exists' });
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
} // --no-memory performs no memory store initialization.
|
|
960
|
+
// Step 4.6: Install apply-leg — the WORK happens here (before "Configure hooks" writes
|
|
961
|
+
// SessionStart), so a foreign SessionStart entry is already in place before that step's own
|
|
962
|
+
// merge ever sees it; see `applyLegStepResult`'s doc for why order matters. The STEP is reported
|
|
963
|
+
// further down, after "Configure hooks" pushes its own, so the printed order still reads as
|
|
964
|
+
// "collect → rank → apply".
|
|
965
|
+
const applyLegStep = opts.noMemory ? { name: 'Install apply-leg', status: 'skipped', detail: '--no-memory' } : applyLegStepResult(opts, backend);
|
|
966
|
+
// Step 5: Configure hooks (write to .claude/settings.json) — EVENT-LEVEL merge (gap G2):
|
|
967
|
+
// dz-generated entries (recognized by signature, incl. the broken legacy `agentdb add` hooks
|
|
968
|
+
// this feature fixes) are replaced in place WITHOUT --force; the user's own hooks and every
|
|
969
|
+
// other settings key are preserved. Full-file overwrite happens only when the file is absent.
|
|
970
|
+
if (!opts.noHooks && !opts.noMemory) {
|
|
971
|
+
const settingsDir = join(opts.projectRoot, '.claude');
|
|
972
|
+
const settingsPath = join(settingsDir, 'settings.json');
|
|
973
|
+
// The BODY goes in first, and the ENTRY goes in only after a LIVE receipt that the body runs
|
|
974
|
+
// and refuses. Written from the INSTALLED package, never copied out of our repository — a
|
|
975
|
+
// consumer has no `packages/@dzhechkov/...` above their project.
|
|
976
|
+
//
|
|
977
|
+
// Round 4, P2: these two used to be independent. A failed write was recorded as an error and
|
|
978
|
+
// the entry was merged anyway, so a consumer whose install failed got a `PreToolUse` entry
|
|
979
|
+
// pointing at something that is not a runnable hook — and that breaks EVERY Bash call, not one.
|
|
980
|
+
//
|
|
981
|
+
// Round 5, P1: the write was also UNCONDITIONAL. Setup is additive everywhere else — the
|
|
982
|
+
// settings merge keeps the user's own hooks, `.gitignore` is appended to, an existing skill is
|
|
983
|
+
// skipped — and this path overwrote a well-known filename with no ownership check, so a
|
|
984
|
+
// consumer's hand-authored `.claude/hooks/destructive-guard.cjs` was destroyed by a routine
|
|
985
|
+
// run. A body we wrote carries a MARKER; a file without it is the consumer's, and only an
|
|
986
|
+
// explicit `--force` may replace it, after a timestamped backup.
|
|
987
|
+
const hookPath = join(opts.projectRoot, ...CLAUDE_DESTRUCTIVE_HOOK_RELPATH.split('/'));
|
|
988
|
+
let installError = null;
|
|
989
|
+
let preserved = null;
|
|
990
|
+
let backupPath = null;
|
|
991
|
+
// Unreadable (absent, or something that is not a file at all) is NOT a claim of ownership: it
|
|
992
|
+
// falls through to the write, whose failure the round-4 receipt below already reports.
|
|
993
|
+
let current = null;
|
|
994
|
+
try {
|
|
995
|
+
current = readFileSync(hookPath, 'utf-8');
|
|
1144
996
|
}
|
|
1145
997
|
catch {
|
|
1146
|
-
|
|
998
|
+
current = null;
|
|
1147
999
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1000
|
+
const foreign = current !== null && !isDzManagedHookBody(current);
|
|
1001
|
+
if (foreign && opts.force !== true) {
|
|
1002
|
+
preserved =
|
|
1003
|
+
'файл на этом пути не наш (нет маркера dz) — ОСТАВЛЕН нетронутым и НЕ ЗАПУСКАЛСЯ; запись в settings.json на этот путь тоже не трогаем (ни своей не добавляем, ни вашу не снимаем); заменить: dz setup --force';
|
|
1004
|
+
}
|
|
1005
|
+
else {
|
|
1006
|
+
try {
|
|
1007
|
+
if (foreign && current !== null) {
|
|
1008
|
+
// Same shape as the codex `hooks.json` backup: the original beside the original, stamped,
|
|
1009
|
+
// so `--force` is recoverable rather than merely loud.
|
|
1010
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1011
|
+
backupPath = writeUniqueStampedFile(`${hookPath}.bak-`, stamp, current, writeFileSync);
|
|
1012
|
+
}
|
|
1013
|
+
mkdirSync(dirname(hookPath), { recursive: true });
|
|
1014
|
+
writeFileSync(hookPath, generateClaudeDestructiveHook(), { mode: 0o755 });
|
|
1015
|
+
}
|
|
1016
|
+
catch (err) {
|
|
1017
|
+
installError = String(err.message);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
const foreignBodyKept = preserved !== null;
|
|
1021
|
+
// The receipt SPAWNS the file, so it may only ever be taken on a body dz owns.
|
|
1022
|
+
//
|
|
1023
|
+
// Round 11, P1 SECURITY — correcting my own round-5 sentence, "the receipt is taken from the
|
|
1024
|
+
// file that IS there". Combined with round 6, which preserves a body dz does not own, that made
|
|
1025
|
+
// `dz setup` EXECUTE whatever a repository had committed at this path: clone a hostile repo,
|
|
1026
|
+
// run the documented setup command, and its `.claude/hooks/destructive-guard.cjs` ran — with
|
|
1027
|
+
// none of the host's hook-trust prompting in between. MEASURED: a foreign body writing a marker
|
|
1028
|
+
// file had written it by the time setup returned.
|
|
1029
|
+
//
|
|
1030
|
+
// So a preserved foreign body is NOT probed, NOT registered, and NOT run. `--force` is consent
|
|
1031
|
+
// to REPLACE it (our body is written above, before this line) — never consent to execute it.
|
|
1032
|
+
// The receipt is still taken whether or not the write threw, because a failed write over an
|
|
1033
|
+
// OLDER BODY OF OURS leaves something we may legitimately run.
|
|
1034
|
+
const receipt = foreignBodyKept
|
|
1035
|
+
? { ok: false, detail: 'проба не проводилась — запускать чужой файл не наше право' }
|
|
1036
|
+
: probeInstalledGuard(hookPath);
|
|
1037
|
+
// Round 8, P2: ownership of the ENTRY follows ownership of the BODY, never the filename.
|
|
1038
|
+
//
|
|
1039
|
+
// Round 6 preserved a consumer's hook file; attribution of its registry entry stayed path-only,
|
|
1040
|
+
// so a routine run deleted the registration of the very file it had just decided not to touch —
|
|
1041
|
+
// their hook left on disk and switched off (MEASURED: their `PreToolUse` entry came back `[]`).
|
|
1042
|
+
// The reverse was just as wrong: a foreign body that happened to refuse made dz ADD an entry
|
|
1043
|
+
// for somebody else's file (MEASURED), taking responsibility for code it may neither read as
|
|
1044
|
+
// its own nor replace.
|
|
1045
|
+
//
|
|
1046
|
+
// So when a foreign body is kept, dz stands down from the whole event: it adds nothing, and
|
|
1047
|
+
// `isManaged` below stops claiming an entry that points at that path. Whether the foreign hook
|
|
1048
|
+
// refuses is not merely the consumer's business — it is a question dz no longer ASKS, because
|
|
1049
|
+
// asking meant running their file (round 11). All of it is said in one line rather than left
|
|
1050
|
+
// for them to find by diffing settings.json.
|
|
1051
|
+
const guardArmed = receipt.ok && !foreignBodyKept;
|
|
1052
|
+
// The receipt is taken from the file that IS at the path — ours, or the one we preserved. A
|
|
1053
|
+
// foreign hook that demonstrably refuses is registered on its own merits; a foreign hook that
|
|
1054
|
+
// does not refuse gets no entry, exactly like a failed install (round 4).
|
|
1055
|
+
const notes = [
|
|
1056
|
+
preserved === null ? '' : `${preserved}; `,
|
|
1057
|
+
backupPath === null ? '' : `прежний файл сохранён: ${basename(backupPath)}; `,
|
|
1058
|
+
installError === null ? '' : `${installError}; `,
|
|
1059
|
+
].join('');
|
|
1060
|
+
steps.push(guardArmed
|
|
1061
|
+
? { name: 'Install destructive guard', status: 'done', detail: `${notes}${CLAUDE_DESTRUCTIVE_HOOK_RELPATH} — ${receipt.detail}` }
|
|
1062
|
+
: {
|
|
1063
|
+
name: 'Install destructive guard',
|
|
1064
|
+
status: preserved === null ? 'error' : 'skipped',
|
|
1065
|
+
detail: `${notes}${receipt.detail} — запись в settings.json НЕ добавлена`,
|
|
1190
1066
|
});
|
|
1067
|
+
const generated = JSON.parse(generateHooksConfig(opts.projectRoot, backend));
|
|
1068
|
+
// No working body ⇒ no entry, and the EVENT KEY STAYS — as an empty managed list when nothing
|
|
1069
|
+
// else of ours belongs there.
|
|
1070
|
+
//
|
|
1071
|
+
// CORRECTION OF RECORD (round 7, P1). The round-5 version DELETED the key and this comment
|
|
1072
|
+
// claimed the merge would then also drop a guard entry left by an earlier setup. That was
|
|
1073
|
+
// asserted without measuring and it is false: `mergeManagedHookEntries` iterates
|
|
1074
|
+
// `Object.keys(managed)`, so an event absent from the managed input is copied through
|
|
1075
|
+
// UNTOUCHED — a project whose guard used to be armed kept invoking it on every Bash call while
|
|
1076
|
+
// the report said the entry was not added. Handing the event an EMPTY list is what makes the
|
|
1077
|
+
// merge EXAMINE it: our entries are dropped by `isManaged`, the user's are preserved in order,
|
|
1078
|
+
// and nothing is appended. The round-5 test passed for the wrong reason — its project had no
|
|
1079
|
+
// pre-existing settings.json, so there was no stale entry for the claim to be wrong about.
|
|
1080
|
+
if (!guardArmed) {
|
|
1081
|
+
generated.hooks['PreToolUse'] = (generated.hooks['PreToolUse'] ?? []).filter((entry) => !entry.hooks.some((h) => isManagedClaudeDestructiveHookCommand(h.command)));
|
|
1082
|
+
}
|
|
1083
|
+
if (!existsSync(settingsPath)) {
|
|
1084
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
1085
|
+
writeFileSync(settingsPath, JSON.stringify({ hooks: generated.hooks }, null, 2));
|
|
1086
|
+
steps.push({ name: 'Configure hooks', status: 'done', detail: `${backend} session hooks` });
|
|
1191
1087
|
}
|
|
1192
1088
|
else {
|
|
1193
|
-
|
|
1089
|
+
try {
|
|
1090
|
+
const existing = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
1091
|
+
// ONE merge implementation, shared with the Codex target (AM-3 / G-E). Claude's exact
|
|
1092
|
+
// command attribution is passed in rather than reimplemented, so emitted bytes, report
|
|
1093
|
+
// tail text, and the no-write path stay on the shared merge contract (AM-37).
|
|
1094
|
+
const isManagedCommand = (cmd) => cmd.includes('agentdb add') ||
|
|
1095
|
+
cmd.includes('agentdb-writer.mjs') ||
|
|
1096
|
+
cmd.includes('sessions.jsonl') ||
|
|
1097
|
+
// Ours ONLY while the body at that path is ours (round 8, P2). Without the
|
|
1098
|
+
// path clause a second `dz setup` would append a duplicate guard entry instead of
|
|
1099
|
+
// replacing the first; without the ownership clause it would delete the entry a
|
|
1100
|
+
// consumer wrote for their own preserved hook.
|
|
1101
|
+
(!foreignBodyKept && isManagedClaudeDestructiveHookCommand(cmd));
|
|
1102
|
+
const plan = mergeManagedHookEntries((existing['hooks'] ?? {}), generated.hooks, {
|
|
1103
|
+
// Drop dz-generated entries (any vintage, either shape) — keep the user's own hooks
|
|
1104
|
+
// untouched. Flat dz entries (≤0.3.43) are dropped too, migrating them to the valid
|
|
1105
|
+
// matcher-group shape appended below.
|
|
1106
|
+
isManaged: (entry) => commandsOf(entry).some(isManagedCommand),
|
|
1107
|
+
isLegacy: (entry) => !Array.isArray(entry?.hooks) ||
|
|
1108
|
+
commandsOf(entry).some((cmd) => cmd.includes('agentdb add')),
|
|
1109
|
+
// Ownership is per HANDLER, not per matcher group. A user's handler may deliberately
|
|
1110
|
+
// share the Bash group with dz's guard; replacing ours must retain their handler object
|
|
1111
|
+
// and every surrounding group field byte-for-byte through JSON serialization.
|
|
1112
|
+
retainForeign: (entry) => {
|
|
1113
|
+
const grouped = entry;
|
|
1114
|
+
if (!Array.isArray(grouped?.hooks))
|
|
1115
|
+
return null;
|
|
1116
|
+
const kept = grouped.hooks.filter((hook) => !isManagedCommand(String(hook?.command ?? '')));
|
|
1117
|
+
return kept.length === 0
|
|
1118
|
+
? null
|
|
1119
|
+
: { ...entry, hooks: kept };
|
|
1120
|
+
},
|
|
1121
|
+
reportLabel: backend,
|
|
1122
|
+
});
|
|
1123
|
+
if (plan.changed) {
|
|
1124
|
+
existing['hooks'] = plan.hooks;
|
|
1125
|
+
writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
|
|
1126
|
+
steps.push({ name: 'Configure hooks', status: 'done', detail: plan.report });
|
|
1127
|
+
}
|
|
1128
|
+
else {
|
|
1129
|
+
steps.push({ name: 'Configure hooks', status: 'skipped', detail: plan.report });
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
catch {
|
|
1133
|
+
steps.push({ name: 'Configure hooks', status: 'error', detail: 'could not parse existing settings.json — fix it and re-run' });
|
|
1134
|
+
}
|
|
1194
1135
|
}
|
|
1195
1136
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
1199
|
-
steps.push({ name: 'Register agentdb MCP', status: 'error', detail: `${reason}: ${detail}` });
|
|
1137
|
+
else {
|
|
1138
|
+
steps.push({ name: 'Configure hooks', status: 'skipped', detail: opts.noMemory ? '--no-memory: existing hooks and guards preserved; no delivery' : '--no-hooks' });
|
|
1200
1139
|
}
|
|
1201
|
-
//
|
|
1202
|
-
//
|
|
1203
|
-
|
|
1204
|
-
|
|
1140
|
+
// Step 5.6: Install apply-leg — report pushed AFTER "Configure hooks" below (for a report order
|
|
1141
|
+
// that reads naturally), but see `applyLegStepResult()` above `runSetup` for why the WRITE itself
|
|
1142
|
+
// happens BEFORE it.
|
|
1143
|
+
steps.push(applyLegStep);
|
|
1144
|
+
// Step 5.5: Register agentdb MCP through the SAME ownership-aware transaction used by `dz init`.
|
|
1145
|
+
// `.mcp.json` is the project-scope carrier Claude Code actually loads. A known historical dz
|
|
1146
|
+
// agentdb shape is adopted; an ambiguous hand-authored entry is preserved and named as an error.
|
|
1147
|
+
if (backend === 'agentdb' && !opts.noHooks && !opts.noMemory) {
|
|
1148
|
+
const agentdbEntry = {
|
|
1149
|
+
command: 'npx',
|
|
1150
|
+
// Pin to the INSTALLED agentdb version (not @latest) so the MCP server and the hook
|
|
1151
|
+
// writer run the same alpha schema against one DB.
|
|
1152
|
+
args: [installedAgentdbSpec(opts.projectRoot), 'mcp', 'start'],
|
|
1153
|
+
// Pin the server to its OWN store — NEVER the writer's .dz/agentdb.db. Two engines on one
|
|
1154
|
+
// SQLite file (native better-sqlite3 + a silent sql.js fallback) whole-file-rewrite each
|
|
1155
|
+
// other: measured 2026-07-09, 5 of 20 samples zero bytes and 4 torn (ADR-001, 2026-08-26).
|
|
1156
|
+
env: { AGENTDB_PATH: agentdbMcpStorePath(opts.projectRoot) },
|
|
1157
|
+
};
|
|
1205
1158
|
try {
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1159
|
+
const applied = applyIntegrationFragments({
|
|
1160
|
+
projectRoot: opts.projectRoot,
|
|
1161
|
+
fragments: [{
|
|
1162
|
+
component: 'mcp',
|
|
1163
|
+
carrierPath: '.mcp.json',
|
|
1164
|
+
scope: 'project',
|
|
1165
|
+
format: 'json',
|
|
1166
|
+
rootKey: 'mcpServers',
|
|
1167
|
+
entries: { agentdb: agentdbEntry },
|
|
1168
|
+
}],
|
|
1169
|
+
});
|
|
1170
|
+
if (applied.written.includes('.mcp.json')) {
|
|
1171
|
+
steps.push({
|
|
1172
|
+
name: 'Register agentdb MCP',
|
|
1173
|
+
status: 'done',
|
|
1174
|
+
// No tool count: a hardcoded number is a lie waiting to age (the live server answered
|
|
1175
|
+
// 35 while its own banner said 32 and the README said 41 — measured 2026-08-26).
|
|
1176
|
+
detail: `.mcp.json: ${installedAgentdbSpec(opts.projectRoot)} → .dz/agentdb-mcp.db (own store; hooks keep .dz/agentdb.db)`,
|
|
1177
|
+
});
|
|
1211
1178
|
}
|
|
1212
1179
|
else {
|
|
1213
|
-
steps.push({ name: '
|
|
1180
|
+
steps.push({ name: 'Register agentdb MCP', status: 'skipped', detail: 'already registered and current' });
|
|
1214
1181
|
}
|
|
1215
1182
|
}
|
|
1216
|
-
catch {
|
|
1217
|
-
|
|
1183
|
+
catch (error) {
|
|
1184
|
+
const reason = error instanceof IntegrationApplyError ? error.reasonCode : 'APPLY_FAILED';
|
|
1185
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1186
|
+
steps.push({ name: 'Register agentdb MCP', status: 'error', detail: `${reason}: ${detail}` });
|
|
1187
|
+
}
|
|
1188
|
+
// Migrate off the legacy location: `.claude/mcp.json` is not loaded by Claude Code. If it
|
|
1189
|
+
// holds ONLY our old agentdb registration, remove the file; otherwise leave it and warn.
|
|
1190
|
+
const legacyPath = join(opts.projectRoot, '.claude', 'mcp.json');
|
|
1191
|
+
if (existsSync(legacyPath)) {
|
|
1192
|
+
try {
|
|
1193
|
+
const legacy = JSON.parse(readFileSync(legacyPath, 'utf-8'));
|
|
1194
|
+
const keys = Object.keys(legacy.mcpServers ?? {});
|
|
1195
|
+
if (keys.length === 1 && keys[0] === 'agentdb') {
|
|
1196
|
+
rmSync(legacyPath);
|
|
1197
|
+
steps.push({ name: 'Migrate legacy .claude/mcp.json', status: 'done', detail: 'removed (not loaded by Claude Code); registration now in .mcp.json' });
|
|
1198
|
+
}
|
|
1199
|
+
else {
|
|
1200
|
+
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' });
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
catch {
|
|
1204
|
+
steps.push({ name: 'Migrate legacy .claude/mcp.json', status: 'error', detail: 'unparseable legacy file — Claude Code does not load it; review manually' });
|
|
1205
|
+
}
|
|
1218
1206
|
}
|
|
1219
1207
|
}
|
|
1220
1208
|
}
|
|
1209
|
+
catch (error) {
|
|
1210
|
+
steps.push({ name: 'Memory backend transition', status: 'error', detail: `saved backend ${observedBackend()} INCOMPLETE: managed wiring failed after persistence; ${String(error)}` });
|
|
1211
|
+
return finish();
|
|
1212
|
+
}
|
|
1213
|
+
// Saved/local/native agreement is independent of hook installation.
|
|
1214
|
+
if (!opts.noMemory) {
|
|
1215
|
+
const actual = observedBackend();
|
|
1216
|
+
const problems = actual === backend ? [] : [`saved backend ${actual} differs from requested ${backend}`];
|
|
1217
|
+
if (backend === 'agentdb') {
|
|
1218
|
+
const deps = reconcileMemoryDependencies(opts.projectRoot, false);
|
|
1219
|
+
if (!deps.ready)
|
|
1220
|
+
problems.push(deps.detail);
|
|
1221
|
+
}
|
|
1222
|
+
steps.push({ name: 'Memory saved state', status: problems.length ? 'error' : 'done', detail: problems.length ? `saved backend ${actual} INCOMPLETE: ${problems.join('; ')}` : `saved backend ${actual}; dependencies/native state consistent` });
|
|
1223
|
+
}
|
|
1221
1224
|
// Step 5.9: agentdb wiring invariant check (audit code#3). Skip-branches across repeated runs
|
|
1222
1225
|
// can leave inconsistent combinations (e.g. writer+MCP present but hooks still jsonl). Verify
|
|
1223
1226
|
// the three-way invariant explicitly and surface a loud error step instead of silent "skipped"s.
|
|
1224
|
-
if (backend === 'agentdb' && !opts.noHooks) {
|
|
1227
|
+
if (backend === 'agentdb' && !opts.noHooks && !opts.noMemory) {
|
|
1225
1228
|
const problems = [];
|
|
1226
|
-
if (
|
|
1227
|
-
problems.push(
|
|
1229
|
+
if (observedBackend() !== backend)
|
|
1230
|
+
problems.push(`saved backend ${observedBackend()} differs from ${backend}`);
|
|
1228
1231
|
try {
|
|
1229
1232
|
const settings = JSON.parse(readFileSync(join(opts.projectRoot, '.claude', 'settings.json'), 'utf-8'));
|
|
1230
1233
|
const refs = ['SessionStart', 'SessionEnd', 'PreCompact'].every((ev) => (settings.hooks?.[ev] ?? []).some((h) => commandsOf(h).some((cmd) => cmd.includes('agentdb-writer.mjs'))));
|
|
@@ -1252,7 +1255,7 @@ export function runSetup(opts) {
|
|
|
1252
1255
|
// sentinel check (e.g. sessions.jsonl, present in both backends) would skip agentdb.db/-wal/-shm
|
|
1253
1256
|
// on the documented jsonl→agentdb `--force` switch, leaking the binary store into git.
|
|
1254
1257
|
const gitignorePath = join(opts.projectRoot, '.gitignore');
|
|
1255
|
-
const dzIgnoreLines = backend === 'agentdb'
|
|
1258
|
+
const dzIgnoreLines = opts.noMemory ? [] : backend === 'agentdb'
|
|
1256
1259
|
? ['.dz/agentdb.db', '.dz/agentdb.db-wal', '.dz/agentdb.db-shm',
|
|
1257
1260
|
'.dz/agentdb-mcp.db', '.dz/agentdb-mcp.db-wal', '.dz/agentdb-mcp.db-shm',
|
|
1258
1261
|
'.dz/sessions.jsonl']
|
|
@@ -1270,21 +1273,19 @@ export function runSetup(opts) {
|
|
|
1270
1273
|
});
|
|
1271
1274
|
}
|
|
1272
1275
|
else {
|
|
1273
|
-
steps.push({ name: 'Update .gitignore', status: 'skipped', detail: 'already ignoring .dz data' });
|
|
1276
|
+
steps.push({ name: 'Update .gitignore', status: 'skipped', detail: opts.noMemory ? '--no-memory: existing ignore rules preserved' : 'already ignoring .dz data' });
|
|
1274
1277
|
}
|
|
1275
1278
|
// Step 7: Install the CLI-driver skill + agent docs (--install-driver)
|
|
1276
1279
|
if (opts.installDriver) {
|
|
1277
1280
|
const detail = installDriverDocs(opts.projectRoot, opts.force ?? false);
|
|
1278
1281
|
steps.push({ name: 'Install driver skill', status: 'done', detail });
|
|
1279
1282
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
memoryBackendDowngraded: resolvedMemory.downgraded,
|
|
1288
|
-
};
|
|
1283
|
+
const failed = steps.filter(step => step.status === 'error');
|
|
1284
|
+
if (!opts.noMemory && backend === 'agentdb')
|
|
1285
|
+
steps.push({
|
|
1286
|
+
name: 'Memory backend transition', status: failed.length ? 'error' : 'done',
|
|
1287
|
+
detail: failed.length ? `saved backend ${observedBackend()} INCOMPLETE; failed phase: ${failed.map(step => step.name).join(', ')}` : `saved backend ${observedBackend()} ready; ${priorBackend === backend ? 'already current' : `${priorBackend} → ${backend} transition complete`}`,
|
|
1288
|
+
});
|
|
1289
|
+
return finish();
|
|
1289
1290
|
}
|
|
1290
1291
|
//# sourceMappingURL=setup.js.map
|