@dzhechkov/harness-cli 0.3.138 → 0.3.142

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/README.md CHANGED
@@ -325,7 +325,7 @@ Get the whole set with `dz init --target claude-code --preset meta`, or pick one
325
325
 
326
326
  > **A skill and its npx toolkit are not duplicates — they're a graduation.** Several skills (e.g. `feature-adr`, `design-thinking`) exist BOTH as a skill inside a `dz` preset AND as a standalone `npx` package. The preset's SKILL.md is **fully functional on its own** (the whole methodology — modules + references — travels with it, and it auto-activates by description), and it's the only way to compile that capability to the **non-Claude platforms** (Codex/OpenCode/Hermes/OpenClaude) via `dz`. The npx package adds **project-level runtime governance** around the same skill: a slash command, governance rules, a context shard, and (for feature-adr) reward-learning + `/harvest`. So: pick the **skill/preset** for a working capability across platforms; pick the **npx toolkit** when you want it as a governed, command-driven fixture of one project.
327
327
 
328
- ## All Commands (35)
328
+ ## All Commands (36)
329
329
 
330
330
  ```
331
331
  dz setup --target <name> [--preset <name>] [--select id,id,...] [--skills-dir <dir>] [--memory agentdb] [--no-memory] [--no-hooks] [--install-driver] [--force]
@@ -336,6 +336,11 @@ dz teach "<pattern>" [--reward <0-1>] [--domain <name>] [--type rule
336
336
  dz consolidate [--sessions-dir <dir>] [--project <dir>] [--no-mirror]
337
337
  dz recall "<query>" [--limit <N>] [--project <dir>]
338
338
  dz recall --books "<query>" [--book <slug>] # digitized-book KUs; --book narrows to one book
339
+ dz brain list [--json] # the durable cross-project knowledge brain
340
+ dz brain query "<q>" [--source <slug>] [--limit <N>] [--json] # cross-source lexical recall over the brain
341
+ dz brain add [--source <slug>] [--project <dir>] [--json] # promote this project's digitized-book KUs into the brain
342
+ dz brain ground "<prompt>" [--k <N>] [--source <slug>] [--text] # retrieve + emit grounding citations (hook entrypoint; silent if irrelevant)
343
+ dz brain init [--project <dir>] [--k <N>] # opt-in: wire the grounding UserPromptSubmit hook into settings.json
339
344
  dz pretrain [--project <dir>]
340
345
  dz recommend "<task description>"
341
346
  dz compose <preset1+preset2+...> [--target <name>]
package/dist/cli.d.ts CHANGED
@@ -7,6 +7,12 @@
7
7
  export interface CliIo {
8
8
  readonly cwd?: string;
9
9
  readonly write?: (line: string) => void;
10
+ /**
11
+ * Pre-read STDIN content (injectable so `dz brain ground`'s hook path is testable without
12
+ * an actual pipe). When omitted, the CLI reads fd 0 synchronously — but only for the one
13
+ * command that needs it (`brain ground`), and never when stdin is a TTY (nothing piped).
14
+ */
15
+ readonly stdin?: string;
10
16
  }
11
17
  export declare function runCli(argv: string[], io?: CliIo): Promise<number>;
12
18
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA6GH,2EAA2E;AAC3E,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAw6DD,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,KAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA0F5E"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAsHH,2EAA2E;AAC3E,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AA6xED,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE,KAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAsG5E"}
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { execSync } from 'node:child_process';
9
9
  import { homedir } from 'node:os';
10
10
  import { createRequire } from 'node:module';
11
11
  import { fileURLToPath } from 'node:url';
12
- import { createSkill, getSkillInfo, getWorkflow, isTargetName, listSkills, runDoctor, runInit, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, checkUpstream, compareSkills, checkAllUpstream, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, TARGET_NAMES, WORKFLOW_NAMES, importEcc, recordPattern, consolidateSessions, pruneNoisePatterns, recallPatterns, queryBookKnowledge, loadStorePatternsSync, bundleSkills, brainHome, listBrain, promoteProjectToBrain, queryBrain, } from '@dzhechkov/harness-core';
12
+ import { createSkill, getSkillInfo, getWorkflow, isTargetName, listSkills, runDoctor, runInit, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, checkUpstream, compareSkills, checkAllUpstream, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, TARGET_NAMES, WORKFLOW_NAMES, importEcc, recordPattern, consolidateSessions, pruneNoisePatterns, recallPatterns, queryBookKnowledge, loadStorePatternsSync, bundleSkills, brainHome, listBrain, promoteProjectToBrain, queryBrain, groundPrompt, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, } from '@dzhechkov/harness-core';
13
13
  import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
14
14
  import { scanGitHub, analyzeRepo, generateReport, deepAnalyze, scanAllSources, ScoutMemory } from '@dzhechkov/scout';
15
15
  const USAGE = `dz - DZ cross-platform harness CLI
@@ -35,7 +35,11 @@ Usage:
35
35
  dz recall "<query>" [--limit <N>] [--books [--book <slug>]] [--project <dir>] | dz recall --all [--json]
36
36
  dz brain list [--json] (the durable cross-project knowledge brain)
37
37
  dz brain query "<q>" [--source <slug>] [--limit <N>] [--json] (cross-source lexical recall over the brain)
38
- dz brain add [--source <slug>] [--project <dir>] [--json] (promote this project's digitized-book KUs into the brain)
38
+ dz brain add [--source <slug>] [--project <dir>] [--from-slice <f>|--from-pack <p>|--from-kus <f> --slug <s>] [--kind <k>] [--license <spdx>] [--json] (grow the brain: promote this project, or import a slice/pack/KU-array)
39
+ dz brain primer <slug> [--json] (print a source's capability card — KU-type histogram + top decision moments)
40
+ dz brain export --source <slug> --out <file> (export ONE source as a portable, lexical-only books.sqlite slice)
41
+ dz brain ground [<prompt>] [--k <N>] [--source <slug>] [--text] (UserPromptSubmit hook: inject brain citations for a prompt; reads STDIN if no prompt)
42
+ dz brain init [--project <dir>] [--k <N>] (wire the grounding hook into .claude/settings.json — opt-in)
39
43
  dz pretrain [--project <dir>]
40
44
  dz recommend "<task description>"
41
45
  dz compose <preset1+preset2+...> [--target <name>]
@@ -906,9 +910,60 @@ async function cmdRecall(options, flags, cwd, write) {
906
910
  const BRAIN_USAGE = `dz brain — the durable, cross-project knowledge brain
907
911
 
908
912
  Usage:
909
- dz brain list [--json]
910
- dz brain query "<q>" [--source <slug>] [--limit <N>] [--json]
911
- dz brain add [--source <slug>] [--project <dir>] [--json]`;
913
+ dz brain list [--json]
914
+ dz brain query "<q>" [--source <slug>] [--limit <N>] [--json]
915
+ dz brain add [--source <slug>] [--project <dir>] [--json]
916
+ dz brain add --from-slice <file.sqlite> [--json]
917
+ dz brain add --from-pack <pkg-or-dir> [--json]
918
+ dz brain add --from-kus <file.json> --slug <s> [--kind repo|book|paper] [--license <spdx>] [--override] [--json]
919
+ dz brain primer <slug> [--json]
920
+ dz brain export --source <slug> --out <file>
921
+ dz brain ground [<prompt>] [--k <N>] [--source <slug>] [--text]
922
+ dz brain init [--project <dir>] [--k <N>]
923
+
924
+ add: default promotes THIS project's digitized-book KUs into the brain. The three --from-* modes
925
+ are mutually exclusive alternate inputs:
926
+ --from-slice imports a standalone per-book slice (a \`dz brain export\` output).
927
+ --from-pack resolves a pack (node_modules pkg or local dir) and imports every
928
+ \`brain/<slug>.sqlite\` slice it ships.
929
+ --from-kus registers a JSON array of already-shaped KUs under --slug (default --kind repo).
930
+ primer: prints a source's capability card (the deterministic KU-type histogram + top decision moments).
931
+ export: writes ONE source's KUs as a portable, lexical-only \`books.sqlite\` slice (vectors re-embed on import).
932
+ ground: the UserPromptSubmit hook entrypoint. Grounds a prompt (positional or STDIN) against
933
+ the brain and, when relevant KUs are found, prints Claude-Code-injectable JSON on
934
+ stdout ({"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":…}}).
935
+ Always exits 0 — grounding is advisory and must never fail a prompt. --text prints the
936
+ raw citation block instead of the JSON wrapper (for manual inspection).
937
+ init: wires \`brain ground\` into .claude/settings.json as an opt-in UserPromptSubmit hook.`;
938
+ /**
939
+ * Extract the user prompt from a Claude Code `UserPromptSubmit` hook STDIN payload. Tries the
940
+ * common JSON shapes (\`.prompt\`, \`.user_prompt\`, \`.userPrompt\`); if the payload is plain
941
+ * non-JSON text, the whole (trimmed) text is the prompt. Empty / unrecognized → \`''\` (the caller
942
+ * then emits nothing and exits 0 — grounding never blocks).
943
+ */
944
+ function extractPromptFromStdin(raw) {
945
+ const text = raw.trim();
946
+ if (text === '')
947
+ return '';
948
+ let parsed;
949
+ try {
950
+ parsed = JSON.parse(text);
951
+ }
952
+ catch {
953
+ // Plain non-JSON text → treat the whole thing as the prompt.
954
+ return text;
955
+ }
956
+ if (parsed !== null && typeof parsed === 'object') {
957
+ const obj = parsed;
958
+ for (const key of ['prompt', 'user_prompt', 'userPrompt']) {
959
+ const v = obj[key];
960
+ if (typeof v === 'string' && v.trim() !== '')
961
+ return v;
962
+ }
963
+ }
964
+ // Valid JSON but no recognizable prompt field (or a bare scalar) → nothing to ground.
965
+ return '';
966
+ }
912
967
  /**
913
968
  * Resolve the deps root — the directory whose `package.json` can resolve `better-sqlite3` **and**
914
969
  * `agentdb`, the native driver `book-kb` + `agentdb-index` load through. Try `cwd` first (a project
@@ -933,7 +988,71 @@ function resolveDepsRoot(cwd) {
933
988
  const cliDir = dirname(fileURLToPath(import.meta.url)); // .../@dzhechkov/harness-cli/dist
934
989
  return canResolve(cliDir) ? cliDir : cwd;
935
990
  }
936
- async function cmdBrain(options, flags, cwd, write) {
991
+ /**
992
+ * Resolve a `--from-pack` spec to a pack directory: a local directory (absolute or cwd-relative)
993
+ * wins; otherwise resolve it as an installed node package (its `package.json` from `cwd` then the
994
+ * CLI's deps root). Returns the pack's root dir, or `undefined` if it is neither.
995
+ */
996
+ function resolvePackDir(cwd, depsRoot, spec) {
997
+ const asDir = resolve(cwd, spec);
998
+ if (existsSync(asDir) && lstatSync(asDir).isDirectory())
999
+ return asDir;
1000
+ for (const root of [cwd, depsRoot]) {
1001
+ try {
1002
+ const req = createRequire(join(root, 'package.json'));
1003
+ return dirname(req.resolve(`${spec}/package.json`));
1004
+ }
1005
+ catch {
1006
+ /* try the next resolution base */
1007
+ }
1008
+ }
1009
+ return undefined;
1010
+ }
1011
+ /**
1012
+ * Validate that a parsed JSON value is an array of KU-shaped objects (the `--from-kus` backend).
1013
+ * Every element must carry the required BookKU string fields; `book` is optional (the caller's
1014
+ * `--slug` is authoritative). On the first bad element returns `{ error }` naming exactly what is
1015
+ * wrong; otherwise returns clean `BookKU[]` (extra fields dropped).
1016
+ */
1017
+ function validateKuArray(parsed) {
1018
+ if (!Array.isArray(parsed))
1019
+ return { error: 'expected a JSON array of KU objects at the top level' };
1020
+ if (parsed.length === 0)
1021
+ return { error: 'the KU array is empty' };
1022
+ const required = ['kuId', 'corpusVersion', 'type', 'name', 'problem', 'content'];
1023
+ const kus = [];
1024
+ for (let i = 0; i < parsed.length; i += 1) {
1025
+ const v = parsed[i];
1026
+ if (v === null || typeof v !== 'object' || Array.isArray(v)) {
1027
+ return { error: `KU #${i} is not an object` };
1028
+ }
1029
+ const o = v;
1030
+ const missing = required.filter((k) => typeof o[k] !== 'string');
1031
+ if (missing.length > 0) {
1032
+ return { error: `KU #${i} missing/invalid string field(s): ${missing.join(', ')}` };
1033
+ }
1034
+ const pages = Array.isArray(o['pages'])
1035
+ ? o['pages'].filter((n) => typeof n === 'number')
1036
+ : undefined;
1037
+ const meta = o['metadata'] !== null && typeof o['metadata'] === 'object' && !Array.isArray(o['metadata'])
1038
+ ? o['metadata']
1039
+ : undefined;
1040
+ kus.push({
1041
+ book: typeof o['book'] === 'string' ? o['book'] : '',
1042
+ kuId: o['kuId'],
1043
+ corpusVersion: o['corpusVersion'],
1044
+ type: o['type'],
1045
+ name: o['name'],
1046
+ problem: o['problem'],
1047
+ content: o['content'],
1048
+ ...(typeof o['chapter'] === 'string' ? { chapter: o['chapter'] } : {}),
1049
+ ...(pages !== undefined ? { pages } : {}),
1050
+ ...(meta !== undefined ? { metadata: meta } : {}),
1051
+ });
1052
+ }
1053
+ return { kus };
1054
+ }
1055
+ async function cmdBrain(options, flags, cwd, write, readStdin) {
937
1056
  const sub = options.get('_positional_0');
938
1057
  const asJson = flags.has('json');
939
1058
  // ── dz brain list ──────────────────────────────────────────────────────────────────────────
@@ -989,13 +1108,189 @@ async function cmdBrain(options, flags, cwd, write) {
989
1108
  }
990
1109
  return 0;
991
1110
  }
1111
+ // ── dz brain primer <slug> ───────────────────────────────────────────────────────────────────
1112
+ // Print a source's capability card (KU-type histogram + top decision moments) from the brain.
1113
+ if (sub === 'primer') {
1114
+ const slug = options.get('_positional_1');
1115
+ if (!slug) {
1116
+ write('dz brain primer: a source slug is required');
1117
+ write(' Example: dz brain primer ddia');
1118
+ return 1;
1119
+ }
1120
+ const { markdown, error } = await buildPrimer({ slug, depsRoot: resolveDepsRoot(cwd) });
1121
+ if (error !== undefined) {
1122
+ write(`dz brain primer: ${error}`);
1123
+ return 1;
1124
+ }
1125
+ if (asJson) {
1126
+ write(JSON.stringify({ slug, markdown }));
1127
+ return 0;
1128
+ }
1129
+ write(markdown);
1130
+ return 0;
1131
+ }
1132
+ // ── dz brain export --source <slug> --out <file> ─────────────────────────────────────────────
1133
+ // Export ONE source's KUs as a portable, lexical-only books.sqlite slice.
1134
+ if (sub === 'export') {
1135
+ const source = options.get('source');
1136
+ const out = options.get('out') ?? (flags.has('out') ? '' : undefined);
1137
+ if (source === undefined || source === '' || out === undefined || out === '') {
1138
+ write('dz brain export: --source <slug> and --out <file> are both required');
1139
+ write(' Example: dz brain export --source ddia --out ./ddia.sqlite');
1140
+ return 1;
1141
+ }
1142
+ const outPath = resolve(cwd, out);
1143
+ const { kuCount, error } = await exportBrainSlice({ slug: source, outPath, depsRoot: resolveDepsRoot(cwd) });
1144
+ if (error !== undefined) {
1145
+ write(`dz brain export: ${error}`);
1146
+ return 1;
1147
+ }
1148
+ write(`dz brain: exported ${kuCount} KU slice → ${outPath}`);
1149
+ return 0;
1150
+ }
992
1151
  // ── dz brain add ─────────────────────────────────────────────────────────────────────────────
1152
+ // Default promotes THIS project's book KB. Three mutually-exclusive --from-* modes ingest a
1153
+ // slice, a pack's shipped slices, or a raw KU array instead.
993
1154
  if (sub === 'add') {
1155
+ const depsRoot = resolveDepsRoot(cwd);
1156
+ const addedTs = new Date().toISOString();
1157
+ // A `--from-* ` given without a value parses as a FLAG (missing value); treat that as '' so the
1158
+ // mode still triggers its "requires a value" guard rather than silently falling through.
1159
+ const fromSlice = options.get('from-slice') ?? (flags.has('from-slice') ? '' : undefined);
1160
+ const fromPack = options.get('from-pack') ?? (flags.has('from-pack') ? '' : undefined);
1161
+ const fromKus = options.get('from-kus') ?? (flags.has('from-kus') ? '' : undefined);
1162
+ const modes = [fromSlice, fromPack, fromKus].filter((m) => m !== undefined).length;
1163
+ if (modes > 1) {
1164
+ write('dz brain add: --from-slice, --from-pack, and --from-kus are mutually exclusive');
1165
+ return 1;
1166
+ }
1167
+ // ── mode: --from-slice <file> → import a standalone per-book slice ──────────────────────────
1168
+ if (fromSlice !== undefined) {
1169
+ if (fromSlice === '') {
1170
+ write('dz brain add: --from-slice requires a slice file path');
1171
+ return 1;
1172
+ }
1173
+ const slicePath = resolve(cwd, fromSlice);
1174
+ if (!existsSync(slicePath)) {
1175
+ write(`dz brain add: no slice at ${slicePath}`);
1176
+ return 1;
1177
+ }
1178
+ const result = await importBrainSlice({ slicePath, depsRoot, addedTs });
1179
+ if (asJson) {
1180
+ write(JSON.stringify(result));
1181
+ return result.sources.length === 0 ? 1 : 0;
1182
+ }
1183
+ if (result.sources.length === 0) {
1184
+ write(`dz brain add: ${result.error ?? 'nothing to import from the slice'}`);
1185
+ return 1;
1186
+ }
1187
+ write(`dz brain: imported ${result.kus} KU from slice → ${result.sources.length} source(s) into ${brainHome()}: ${result.sources.join(', ')}`);
1188
+ if (result.error !== undefined)
1189
+ write(` (partial: ${result.error})`);
1190
+ return 0;
1191
+ }
1192
+ // ── mode: --from-pack <pkg-or-dir> → import every brain/<slug>.sqlite the pack ships ────────
1193
+ if (fromPack !== undefined) {
1194
+ if (fromPack === '') {
1195
+ write('dz brain add: --from-pack requires a package name or directory');
1196
+ return 1;
1197
+ }
1198
+ const packDir = resolvePackDir(cwd, depsRoot, fromPack);
1199
+ if (packDir === undefined) {
1200
+ write(`dz brain add: could not resolve pack '${fromPack}' (not a local directory, not an installed package)`);
1201
+ return 1;
1202
+ }
1203
+ const packBrainDir = join(packDir, 'brain');
1204
+ const slices = existsSync(packBrainDir)
1205
+ ? readdirSync(packBrainDir).filter((f) => f.endsWith('.sqlite')).sort()
1206
+ : [];
1207
+ if (slices.length === 0) {
1208
+ write(`dz brain add: no brain/<slug>.sqlite slice found in pack '${fromPack}' (${packDir})`);
1209
+ return 1;
1210
+ }
1211
+ const imported = [];
1212
+ for (const f of slices) {
1213
+ imported.push(await importBrainSlice({ slicePath: join(packBrainDir, f), depsRoot, addedTs }));
1214
+ }
1215
+ const allSources = imported.flatMap((r) => r.sources);
1216
+ const totalKus = imported.reduce((s, r) => s + r.kus, 0);
1217
+ const firstError = imported.find((r) => r.error !== undefined)?.error;
1218
+ if (asJson) {
1219
+ write(JSON.stringify({ sources: allSources, kus: totalKus, ...(firstError !== undefined ? { error: firstError } : {}) }));
1220
+ return allSources.length === 0 ? 1 : 0;
1221
+ }
1222
+ if (allSources.length === 0) {
1223
+ write(`dz brain add: ${firstError ?? 'nothing imported'} from pack '${fromPack}'`);
1224
+ return 1;
1225
+ }
1226
+ write(`dz brain: imported ${totalKus} KU from ${slices.length} slice(s) in pack '${fromPack}' → ${allSources.length} source(s) into ${brainHome()}: ${allSources.join(', ')}`);
1227
+ if (firstError !== undefined)
1228
+ write(` (partial: ${firstError})`);
1229
+ return 0;
1230
+ }
1231
+ // ── mode: --from-kus <file.json> --slug <s> → register a raw KU array ───────────────────────
1232
+ if (fromKus !== undefined) {
1233
+ if (fromKus === '') {
1234
+ write('dz brain add: --from-kus requires a JSON file path');
1235
+ return 1;
1236
+ }
1237
+ const slug = options.get('slug');
1238
+ if (slug === undefined || slug === '') {
1239
+ write('dz brain add --from-kus: --slug <s> is required (every KU is registered under it)');
1240
+ return 1;
1241
+ }
1242
+ const kusPath = resolve(cwd, fromKus);
1243
+ if (!existsSync(kusPath)) {
1244
+ write(`dz brain add: no KU file at ${kusPath}`);
1245
+ return 1;
1246
+ }
1247
+ let parsed;
1248
+ try {
1249
+ parsed = JSON.parse(readFileSync(kusPath, 'utf8'));
1250
+ }
1251
+ catch (err) {
1252
+ write(`dz brain add --from-kus: invalid JSON in ${kusPath}: ${err instanceof Error ? err.message : String(err)}`);
1253
+ return 1;
1254
+ }
1255
+ const valid = validateKuArray(parsed);
1256
+ if ('error' in valid) {
1257
+ write(`dz brain add --from-kus: bad KU shape — ${valid.error}`);
1258
+ return 1;
1259
+ }
1260
+ const kind = options.get('kind') ?? 'repo';
1261
+ if (kind !== 'repo' && kind !== 'book' && kind !== 'paper') {
1262
+ write(`dz brain add --from-kus: --kind must be one of repo|book|paper (got '${kind}')`);
1263
+ return 1;
1264
+ }
1265
+ const license = options.get('license');
1266
+ const result = await registerKusToBrain({
1267
+ kus: valid.kus,
1268
+ slug,
1269
+ kind,
1270
+ depsRoot,
1271
+ addedTs,
1272
+ ...(license !== undefined ? { license } : {}),
1273
+ ...(flags.has('override') ? { override: true } : {}),
1274
+ });
1275
+ if (asJson) {
1276
+ write(JSON.stringify({ slug, kind, ...result }));
1277
+ return result.kus === 0 ? 1 : 0;
1278
+ }
1279
+ if (result.kus === 0) {
1280
+ write(`dz brain add --from-kus: ${result.error ?? 'nothing registered'}`);
1281
+ return 1;
1282
+ }
1283
+ write(`dz brain: registered ${result.kus} KU under '${slug}' (kind ${kind}) into ${brainHome()}`);
1284
+ if (result.error !== undefined)
1285
+ write(` (partial: ${result.error})`);
1286
+ return 0;
1287
+ }
1288
+ // ── default: promote THIS project's digitized book KB ──────────────────────────────────────
994
1289
  const source = options.get('source');
995
1290
  const result = await promoteProjectToBrain({
996
1291
  projectRoot: resolve(cwd, options.get('project') ?? '.'),
997
- depsRoot: resolveDepsRoot(cwd),
998
- addedTs: new Date().toISOString(),
1292
+ depsRoot,
1293
+ addedTs,
999
1294
  ...(source !== undefined ? { source } : {}),
1000
1295
  });
1001
1296
  if (asJson) {
@@ -1014,6 +1309,101 @@ async function cmdBrain(options, flags, cwd, write) {
1014
1309
  write(` (partial: ${result.error})`);
1015
1310
  return 0;
1016
1311
  }
1312
+ // ── dz brain ground [<prompt>] ───────────────────────────────────────────────────────────────
1313
+ // The UserPromptSubmit hook entrypoint. ALWAYS exits 0 — grounding is advisory and must never
1314
+ // fail a prompt. Emits nothing (silent) unless the brain has relevant citations for the prompt.
1315
+ if (sub === 'ground') {
1316
+ const positional = options.get('_positional_1');
1317
+ // Positional prompt wins; else read the hook payload from STDIN. Empty stdin → emit nothing.
1318
+ const prompt = positional !== undefined && positional !== ''
1319
+ ? positional
1320
+ : extractPromptFromStdin(readStdin());
1321
+ if (prompt.trim() === '')
1322
+ return 0;
1323
+ const source = options.get('source');
1324
+ const kRaw = options.get('k');
1325
+ const k = kRaw !== undefined ? Math.max(1, parseInt(kRaw, 10) || 5) : undefined;
1326
+ const gopts = {
1327
+ prompt,
1328
+ depsRoot: resolveDepsRoot(cwd),
1329
+ };
1330
+ if (k !== undefined)
1331
+ gopts.k = k;
1332
+ if (source !== undefined)
1333
+ gopts.source = source;
1334
+ const res = await groundPrompt(gopts);
1335
+ if (!res.emitted)
1336
+ return 0; // no relevant citations → inject nothing
1337
+ // --text → raw block for manual inspection; default → Claude-Code-injectable additionalContext.
1338
+ if (flags.has('text')) {
1339
+ write(res.block);
1340
+ return 0;
1341
+ }
1342
+ write(JSON.stringify({
1343
+ hookSpecificOutput: {
1344
+ hookEventName: 'UserPromptSubmit',
1345
+ additionalContext: res.block,
1346
+ },
1347
+ }));
1348
+ return 0;
1349
+ }
1350
+ // ── dz brain init ────────────────────────────────────────────────────────────────────────────
1351
+ // Opt-in: wire `dz brain ground` into .claude/settings.json as a UserPromptSubmit hook.
1352
+ // Idempotent read-merge-write — preserve every existing hook/key (e.g. the agentic-qe route hook).
1353
+ if (sub === 'init') {
1354
+ const projectRoot = resolve(cwd, options.get('project') ?? '.');
1355
+ const kRaw = options.get('k');
1356
+ const k = kRaw !== undefined ? Math.max(1, parseInt(kRaw, 10) || 5) : 5;
1357
+ // Invoke the SAME bin this CLI runs from: dist/bin.js sits next to dist/cli.js.
1358
+ const dzBin = join(dirname(fileURLToPath(import.meta.url)), 'bin.js');
1359
+ const groundCmd = `node ${JSON.stringify(dzBin)} brain ground --k ${k}`;
1360
+ const settingsPath = join(projectRoot, '.claude', 'settings.json');
1361
+ let settings = {};
1362
+ if (existsSync(settingsPath)) {
1363
+ try {
1364
+ const parsed = JSON.parse(readFileSync(settingsPath, 'utf8'));
1365
+ if (parsed !== null && typeof parsed === 'object')
1366
+ settings = parsed;
1367
+ }
1368
+ catch {
1369
+ // Corrupt/unreadable settings → start fresh rather than crash (grounding is opt-in glue).
1370
+ settings = {};
1371
+ }
1372
+ }
1373
+ const hooks = (settings['hooks'] !== null && typeof settings['hooks'] === 'object'
1374
+ ? settings['hooks']
1375
+ : {});
1376
+ const ups = Array.isArray(hooks['UserPromptSubmit']) ? [...hooks['UserPromptSubmit']] : [];
1377
+ // The grounding hook, in Claude Code's matcher-group shape (UserPromptSubmit takes no matcher).
1378
+ const entry = { hooks: [{ type: 'command', command: groundCmd, timeout: 5000 }] };
1379
+ // Identify OUR entry by a `brain ground` command; replace in place (idempotent), else append.
1380
+ const isGroundEntry = (e) => {
1381
+ const ex = e;
1382
+ return Array.isArray(ex?.hooks)
1383
+ && ex.hooks.some((h) => typeof h?.command === 'string' && h.command.includes('brain ground'));
1384
+ };
1385
+ const idx = ups.findIndex(isGroundEntry);
1386
+ const replaced = idx >= 0;
1387
+ if (replaced)
1388
+ ups[idx] = entry;
1389
+ else
1390
+ ups.push(entry);
1391
+ hooks['UserPromptSubmit'] = ups;
1392
+ settings['hooks'] = hooks;
1393
+ mkdirSync(dirname(settingsPath), { recursive: true });
1394
+ writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
1395
+ write(`dz brain init: ${replaced ? 're-wired' : 'wired'} the grounding hook into ${settingsPath}`);
1396
+ write(` UserPromptSubmit → ${groundCmd}`);
1397
+ write(` Grounding is now ON for this project (k=${k}). Every prompt is screened against your brain;`);
1398
+ write(` when ≥2 content terms co-occur in a stored KU, the retrieved citations are injected as`);
1399
+ write(` additionalContext. The command exits 0 on any failure (empty brain, error, empty stdin),`);
1400
+ write(` so a grounding miss can never block or fail a prompt (continueOnError-equivalent).`);
1401
+ write(` Honesty: the hook only MECHANICALLY injects the retrieved citations — actually answering`);
1402
+ write(` FROM them (not from model memory/drift) is agent discipline (§7.2), not something the hook`);
1403
+ write(` can enforce.`);
1404
+ write(` To turn it OFF: remove the "brain ground" entry under hooks.UserPromptSubmit in ${settingsPath}.`);
1405
+ return 0;
1406
+ }
1017
1407
  // ── unknown / absent subcommand ──────────────────────────────────────────────────────────────
1018
1408
  write(BRAIN_USAGE);
1019
1409
  return sub === undefined ? 0 : 1;
@@ -1869,6 +2259,21 @@ async function cmdImportEcc(options, flags, cwd, write) {
1869
2259
  export async function runCli(argv, io = {}) {
1870
2260
  const cwd = io.cwd ?? process.cwd();
1871
2261
  const write = io.write ?? ((line) => { console.log(line); });
2262
+ // Lazy STDIN reader — only `dz brain ground` reads it, and only when no positional prompt is
2263
+ // given. Never blocks: injected `io.stdin` wins; else read fd 0 synchronously, but bail to '' on
2264
+ // a TTY (nothing piped) or any read error. Grounding must never hang waiting on an empty pipe.
2265
+ const readStdin = () => {
2266
+ if (io.stdin !== undefined)
2267
+ return io.stdin;
2268
+ try {
2269
+ if (process.stdin.isTTY)
2270
+ return '';
2271
+ return readFileSync(0, 'utf8');
2272
+ }
2273
+ catch {
2274
+ return '';
2275
+ }
2276
+ };
1872
2277
  const { command, options, flags } = parseArgs(argv);
1873
2278
  if (command === '' || command === 'help' || flags.has('help')) {
1874
2279
  write(USAGE);
@@ -1908,7 +2313,7 @@ export async function runCli(argv, io = {}) {
1908
2313
  case 'recall':
1909
2314
  return await cmdRecall(options, flags, cwd, write);
1910
2315
  case 'brain':
1911
- return await cmdBrain(options, flags, cwd, write);
2316
+ return await cmdBrain(options, flags, cwd, write, readStdin);
1912
2317
  case 'setup':
1913
2318
  return await cmdSetup(options, flags, cwd, write);
1914
2319
  case 'pretrain':