@dzhechkov/harness-core 0.7.12 → 0.8.2

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.
Files changed (105) hide show
  1. package/.dz-manifest.json +210 -70
  2. package/README.md +34 -1
  3. package/dist/book-kb.d.ts.map +1 -1
  4. package/dist/book-kb.js +17 -0
  5. package/dist/book-kb.js.map +1 -1
  6. package/dist/feature-adr-checkpoints.d.ts +61 -0
  7. package/dist/feature-adr-checkpoints.d.ts.map +1 -1
  8. package/dist/feature-adr-checkpoints.js +116 -2
  9. package/dist/feature-adr-checkpoints.js.map +1 -1
  10. package/dist/index.d.ts +16 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +13 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/loop-blobs.generated.js +2 -2
  15. package/dist/loop-blobs.generated.js.map +1 -1
  16. package/dist/mutation-gate.d.ts +31 -8
  17. package/dist/mutation-gate.d.ts.map +1 -1
  18. package/dist/mutation-gate.js +57 -16
  19. package/dist/mutation-gate.js.map +1 -1
  20. package/dist/named-lock.d.ts.map +1 -1
  21. package/dist/named-lock.js +11 -9
  22. package/dist/named-lock.js.map +1 -1
  23. package/dist/native-dep-probe.d.ts +11 -0
  24. package/dist/native-dep-probe.d.ts.map +1 -0
  25. package/dist/native-dep-probe.js +100 -0
  26. package/dist/native-dep-probe.js.map +1 -0
  27. package/dist/operations.d.ts.map +1 -1
  28. package/dist/operations.js +82 -1
  29. package/dist/operations.js.map +1 -1
  30. package/dist/parser-safe-region.d.ts +47 -0
  31. package/dist/parser-safe-region.d.ts.map +1 -0
  32. package/dist/parser-safe-region.js +47 -0
  33. package/dist/parser-safe-region.js.map +1 -0
  34. package/dist/patterns.d.ts.map +1 -1
  35. package/dist/patterns.js +10 -1
  36. package/dist/patterns.js.map +1 -1
  37. package/dist/profile.d.ts +202 -0
  38. package/dist/profile.d.ts.map +1 -0
  39. package/dist/profile.js +536 -0
  40. package/dist/profile.js.map +1 -0
  41. package/dist/publish.d.ts +5 -3
  42. package/dist/publish.d.ts.map +1 -1
  43. package/dist/publish.js +46 -8
  44. package/dist/publish.js.map +1 -1
  45. package/dist/qe-rounds.d.ts +75 -0
  46. package/dist/qe-rounds.d.ts.map +1 -0
  47. package/dist/qe-rounds.js +174 -0
  48. package/dist/qe-rounds.js.map +1 -0
  49. package/dist/registry.d.ts.map +1 -1
  50. package/dist/registry.js +5 -1
  51. package/dist/registry.js.map +1 -1
  52. package/dist/run-records.d.ts.map +1 -1
  53. package/dist/run-records.js +8 -1
  54. package/dist/run-records.js.map +1 -1
  55. package/dist/setup.d.ts +35 -4
  56. package/dist/setup.d.ts.map +1 -1
  57. package/dist/setup.js +86 -23
  58. package/dist/setup.js.map +1 -1
  59. package/dist/sign.d.ts +29 -24
  60. package/dist/sign.d.ts.map +1 -1
  61. package/dist/sign.js +183 -61
  62. package/dist/sign.js.map +1 -1
  63. package/dist/store-location.d.ts +56 -0
  64. package/dist/store-location.d.ts.map +1 -0
  65. package/dist/store-location.js +56 -0
  66. package/dist/store-location.js.map +1 -0
  67. package/dist/store-lock.d.ts +1 -0
  68. package/dist/store-lock.d.ts.map +1 -1
  69. package/dist/store-lock.js +7 -1
  70. package/dist/store-lock.js.map +1 -1
  71. package/dist/store-merge.d.ts +55 -0
  72. package/dist/store-merge.d.ts.map +1 -0
  73. package/dist/store-merge.js +77 -0
  74. package/dist/store-merge.js.map +1 -0
  75. package/dist/teach-target.d.ts +61 -0
  76. package/dist/teach-target.d.ts.map +1 -0
  77. package/dist/teach-target.js +105 -0
  78. package/dist/teach-target.js.map +1 -0
  79. package/dist/vector-tier.d.ts.map +1 -1
  80. package/dist/vector-tier.js +63 -20
  81. package/dist/vector-tier.js.map +1 -1
  82. package/package.json +13 -13
  83. package/sbom.json +424 -70
  84. package/src/book-kb.ts +17 -0
  85. package/src/feature-adr-checkpoints.ts +111 -2
  86. package/src/index.ts +39 -1
  87. package/src/loop-blobs.generated.ts +2 -2
  88. package/src/mutation-gate.ts +74 -17
  89. package/src/named-lock.ts +10 -8
  90. package/src/native-dep-probe.ts +118 -0
  91. package/src/operations.ts +77 -1
  92. package/src/parser-safe-region.ts +65 -0
  93. package/src/patterns.ts +10 -1
  94. package/src/profile.ts +593 -0
  95. package/src/publish.ts +43 -7
  96. package/src/qe-rounds.ts +226 -0
  97. package/src/registry.ts +5 -1
  98. package/src/run-records.ts +9 -1
  99. package/src/setup.ts +87 -23
  100. package/src/sign.ts +180 -60
  101. package/src/store-location.ts +76 -0
  102. package/src/store-lock.ts +8 -1
  103. package/src/store-merge.ts +91 -0
  104. package/src/teach-target.ts +126 -0
  105. package/src/vector-tier.ts +58 -9
@@ -0,0 +1,118 @@
1
+ import { createRequire } from 'node:module';
2
+ import { join, resolve } from 'node:path';
3
+
4
+ export type NativeDepState = 'usable' | 'absent' | 'unusable';
5
+
6
+ export interface NativeDepVerdict {
7
+ readonly state: NativeDepState;
8
+ readonly pkg: string;
9
+ readonly reason?: string;
10
+ readonly path?: string;
11
+ }
12
+
13
+ const verdictCache = new Map<string, NativeDepVerdict>();
14
+ const exerciseIds = new WeakMap<Function, string>();
15
+ let nextExerciseId = 1;
16
+
17
+ function exerciseIdentity(exercise: unknown): string {
18
+ if (typeof exercise !== 'function') return `invalid-${typeof exercise}`;
19
+ const existing = exerciseIds.get(exercise);
20
+ if (existing) return existing;
21
+ const identity = `function-${nextExerciseId++}`;
22
+ exerciseIds.set(exercise, identity);
23
+ return identity;
24
+ }
25
+
26
+ function formatError(error: unknown): string {
27
+ try {
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ const oneLine = message.replace(/\s+/g, ' ').trim() || 'unknown error';
30
+ return oneLine.length > 400 ? `${oneLine.slice(0, 397)}...` : oneLine;
31
+ } catch {
32
+ return 'unknown error';
33
+ }
34
+ }
35
+
36
+ export function exerciseSqliteOpen(mod: unknown): void {
37
+ const defaultExport =
38
+ typeof mod === 'object' && mod !== null && 'default' in mod
39
+ ? (mod as { default: unknown }).default
40
+ : mod;
41
+ const Database = defaultExport as new (filename: string) => { close(): void };
42
+ const database = new Database(':memory:');
43
+ database.close();
44
+ }
45
+
46
+ export function probeNativeDep(
47
+ projectRoot: string,
48
+ pkg: string,
49
+ exercise?: (mod: unknown) => unknown,
50
+ exerciseId?: string,
51
+ ): NativeDepVerdict {
52
+ let cacheKey: string;
53
+ try {
54
+ const resolvedExerciseId =
55
+ exerciseId ?? (exercise === undefined ? undefined : exerciseIdentity(exercise));
56
+ cacheKey = `${resolve(projectRoot)}::${pkg}::${resolvedExerciseId ?? 'none'}`;
57
+ } catch {
58
+ return { state: 'absent', pkg };
59
+ }
60
+
61
+ const cached = verdictCache.get(cacheKey);
62
+ if (cached) return cached;
63
+
64
+ let resolvedPath: string;
65
+ let projectRequire: ReturnType<typeof createRequire>;
66
+ try {
67
+ projectRequire = createRequire(join(projectRoot, 'package.json'));
68
+ resolvedPath = projectRequire.resolve(pkg);
69
+ } catch {
70
+ const verdict: NativeDepVerdict = { state: 'absent', pkg };
71
+ verdictCache.set(cacheKey, verdict);
72
+ return verdict;
73
+ }
74
+
75
+ let verdict: NativeDepVerdict;
76
+ try {
77
+ const mod = projectRequire(resolvedPath);
78
+ const exerciseResult = exercise?.(mod);
79
+ const then =
80
+ exerciseResult !== null &&
81
+ (typeof exerciseResult === 'object' || typeof exerciseResult === 'function')
82
+ ? (exerciseResult as { then?: unknown }).then
83
+ : undefined;
84
+ if (typeof then === 'function') {
85
+ void Promise.resolve(exerciseResult).catch(() => {});
86
+ verdict = {
87
+ state: 'unusable',
88
+ pkg,
89
+ reason:
90
+ 'exercise returned a promise; probeNativeDep is synchronous and cannot await it',
91
+ path: resolvedPath,
92
+ };
93
+ } else {
94
+ verdict = { state: 'usable', pkg, path: resolvedPath };
95
+ }
96
+ } catch (error) {
97
+ verdict = {
98
+ state: 'unusable',
99
+ pkg,
100
+ reason: formatError(error),
101
+ path: resolvedPath,
102
+ };
103
+ }
104
+
105
+ verdictCache.set(cacheKey, verdict);
106
+ return verdict;
107
+ }
108
+
109
+ export function describeNativeDep(verdict: NativeDepVerdict): string {
110
+ if (verdict.state === 'usable') return '';
111
+ if (verdict.state === 'absent') {
112
+ if (verdict.pkg === 'agentdb' || verdict.pkg === 'better-sqlite3') {
113
+ return `${verdict.pkg} not installed in project (run: dz setup --memory agentdb)`;
114
+ }
115
+ return `${verdict.pkg} not installed in project (run: npm i ${verdict.pkg})`;
116
+ }
117
+ return `${verdict.pkg} is installed at ${verdict.path ?? 'an unknown path'}, but its native part will not load on this Node: ${verdict.reason ?? 'unknown error'} (run: npm rebuild ${verdict.pkg})`;
118
+ }
package/src/operations.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  type HooksListHookMetadata,
28
28
  type ManagedEntry,
29
29
  } from './codex-hooks.js';
30
+ import { sweepSkillDrift } from './skill-drift.js';
30
31
  import { generateCodexHelpers } from './codex-hooks-assets.js';
31
32
  import {
32
33
  classifyVetoProbe,
@@ -997,6 +998,40 @@ export async function runDoctor(options: { projectRoot: string }): Promise<Docto
997
998
  detail: '.claude/mcp.json is NOT loaded by Claude Code — re-run dz setup to register agentdb in .mcp.json (project root)',
998
999
  });
999
1000
  }
1001
+ // 8x. agentdb MCP/writer store SEPARATION (ADR-001 agentdb-setup-shared-store-fix, 2026-08-26).
1002
+ // A shared store is a measured data-loss path (2026-07-09: 5 of 20 samples zero bytes, 4 torn) —
1003
+ // the same predicate setup.ts's `agentdb wiring` step uses, imported, never re-implemented.
1004
+ // Deliberately OUTSIDE the `.dz/agentdb-writer.mjs` branch below: a registration can be wrong
1005
+ // before any writer is deployed, and that is exactly when it is cheapest to fix.
1006
+ const mcpConfigPath = join(root, '.mcp.json');
1007
+ if (existsSync(mcpConfigPath)) {
1008
+ try {
1009
+ const { agentdbStoreSeparationProblem } = await import('./setup.js');
1010
+ const mcp = JSON.parse(readFileSync(mcpConfigPath, 'utf-8')) as {
1011
+ mcpServers?: Record<string, { env?: Record<string, string> }>;
1012
+ };
1013
+ const pinned = mcp.mcpServers?.['agentdb']?.env?.['AGENTDB_PATH'];
1014
+ // Only speak when an agentdb server is actually registered — a project without one keeps
1015
+ // its doctor output noise-free, exactly as before this check existed.
1016
+ // A REGISTERED agentdb server with NO AGENTDB_PATH is not a quiet project — it is the
1017
+ // protection DELETED. Without the pin the server resolves its store from its own cwd, which is
1018
+ // exactly the unpinned arrangement that produced an empty orphan store in this repo. The first
1019
+ // version of this check skipped that case, so removing the guard produced no finding at all
1020
+ // (cross-family QE, Codex gpt-5.6-sol). Silence on a deleted protection is the failure mode
1021
+ // this whole feature exists to end.
1022
+ const registered = mcp.mcpServers?.['agentdb'] !== undefined;
1023
+ if (registered && pinned === undefined) {
1024
+ checks.push({
1025
+ name: 'agentdb store separation',
1026
+ ok: false,
1027
+ detail: 'an agentdb MCP server is registered with NO AGENTDB_PATH — it will resolve its store from its own cwd, so nothing guarantees it stays off the session-hook writer store; re-run dz setup --memory agentdb',
1028
+ });
1029
+ } else if (pinned !== undefined) {
1030
+ const problem = agentdbStoreSeparationProblem(root, pinned);
1031
+ if (problem) checks.push({ name: 'agentdb store separation', ok: false, detail: problem });
1032
+ }
1033
+ } catch { /* doctor never throws on a diagnostic */ }
1034
+ }
1000
1035
  // Flat dz hook entries (dz ≤0.3.43 emitted `{type,command}` without the matcher-group wrapper)
1001
1036
  // are silently IGNORED by Claude Code — the writer never fires. Detect and point at the fix.
1002
1037
  try {
@@ -1097,12 +1132,53 @@ export async function runDoctor(options: { projectRoot: string }): Promise<Docto
1097
1132
  checks.push({
1098
1133
  name: 'agentdb version drift',
1099
1134
  ok: false,
1100
- detail: `local agentdb@${localVer} != MCP pin ${pinned} (alpha schema drift risk on the shared DB) — re-run dz setup`,
1135
+ detail: `local agentdb@${localVer} != MCP pin ${pinned} (alpha schema drift risk) — re-run dz setup`,
1101
1136
  });
1102
1137
  }
1103
1138
  } catch { /* either side absent — covered by other checks */ }
1104
1139
  }
1105
1140
 
1141
+ // 8a-bis. DEV-TREE SKILL LAG — advisory, never blocking.
1142
+ //
1143
+ // The `no-skill-drift` HARD rule sweeps scope 'installs', which DELIBERATELY excludes the
1144
+ // hand-edited `.claude/skills/` dev tree: those copies are allowed to lag a published bump, and
1145
+ // holding them to byte-identity would make the gate red-on-arrival. That reasoning is sound and
1146
+ // is NOT changed here.
1147
+ //
1148
+ // What was missing is that nobody SAW the lag. MEASURED 2026-08-26: `.claude/skills/idea2prd-manual`
1149
+ // was a month behind its package copy — missing the entire v3 modernization that landed in
1150
+ // 0.1.3/0.1.4 — and `.claude/skills/observability` had never received the 2026-08-24 tightening of
1151
+ // its dashboard gate. Those dev copies are exactly what the agents in THIS repo read, so "legitimately
1152
+ // lagging" quietly meant "the agents here have been running a stale skill for a month". A narrow
1153
+ // gate plus zero visibility is how a lag becomes a silent regression.
1154
+ //
1155
+ // So: report the DIFFERENCE between the wide sweep and the gated one — i.e. only skills whose sole
1156
+ // divergence is the dev tree. Never `ok:false` for a lag alone; the HARD rule keeps owning what
1157
+ // blocks. `dz drift-check --all` remains the detailed view.
1158
+ try {
1159
+ const allowlist = ((): string[] => {
1160
+ const ap = join(root, '.dz', 'drift-allowlist.json');
1161
+ if (!existsSync(ap)) return [];
1162
+ try {
1163
+ const raw = JSON.parse(readFileSync(ap, 'utf8')) as unknown;
1164
+ const arr = Array.isArray(raw) ? raw : Array.isArray((raw as { skills?: unknown })?.skills) ? (raw as { skills: unknown[] }).skills : [];
1165
+ return arr.map((e) => (typeof e === 'string' ? e : (e as { name?: string })?.name)).filter((n): n is string => typeof n === 'string' && n.length > 0);
1166
+ } catch { return []; }
1167
+ })();
1168
+ const wide = sweepSkillDrift(root, { scope: 'all', allowlist });
1169
+ const gated = new Set(sweepSkillDrift(root, { scope: 'installs', allowlist }).drifted.map((d) => d.name));
1170
+ // Only what the HARD rule does NOT already own: these differ ONLY because of the dev tree.
1171
+ const devOnly = wide.drifted.filter((d) => !gated.has(d.name));
1172
+ if (devOnly.length > 0) {
1173
+ const names = devOnly.slice(0, 5).map((d) => `${d.name} (${d.driftFiles} file(s))`).join(', ');
1174
+ checks.push({
1175
+ name: 'dev-tree skill lag',
1176
+ ok: true, // ADVISORY: the gated scope decides what blocks; this only makes the lag visible.
1177
+ detail: `${devOnly.length} skill(s) differ ONLY in the .claude/skills dev tree: ${names}${devOnly.length > 5 ? ', …' : ''} — a dev copy may legitimately lag a published bump, but it is what the agents here READ, so a month-old copy is a silent regression. Inspect with: dz drift-check --all`,
1178
+ });
1179
+ }
1180
+ } catch { /* doctor never throws on a diagnostic */ }
1181
+
1106
1182
  // 8b. EVIDENCE-CHAIN INTEGRITY (feature event-chain, ADR-001). `.dz/recall-usage.jsonl` and
1107
1183
  // `.dz/guard-audit.jsonl` are what `dz compounding` and `dz guard promote` decide on; a rewrite
1108
1184
  // that loses or duplicates a record there is a wrong verdict with no symptom. Deliberately OUTSIDE
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The `feature-adr.js` workflow's PARSER-SAFE REGION — the shared predicate, not a re-implementation.
3
+ *
4
+ * The Workflow runtime's parser is STRICTER than `node --check`: inside the routing block that runs
5
+ * from `const MODELS =` to `const ROUTER =` a template literal is a hard parse error, so no backtick
6
+ * may appear there. That constraint used to be enforced by a `not.toContain('`')` assertion written
7
+ * inline in one test (`feature-adr-model-routing.test.ts:142`), which proves only that the SHIPPED
8
+ * file is currently clean — delete the assertion and nothing goes red.
9
+ *
10
+ * ADR-001 / AM-1 asked for the other half: a guard that is PROVEN to fire on a real violation. Making
11
+ * the predicate a shared export is what buys that — the same function decides the shipped file and the
12
+ * deliberately mutated copy, so a test can inject one backtick into the region and watch it refuse.
13
+ * Deleting the guard now deletes the thing the mutation test calls.
14
+ *
15
+ * Pure and dependency-free: string in, verdict out. No file I/O, no process state.
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+
20
+ /** Opening marker of the parser-safe region (the first line of the inlined routing block). */
21
+ export const PARSER_SAFE_REGION_START = 'const MODELS =';
22
+ /** Closing marker of the parser-safe region (the first declaration after the routing block). */
23
+ export const PARSER_SAFE_REGION_END = 'const ROUTER =';
24
+
25
+ /** Why a source could not be cleared — `null` when the region is clean. */
26
+ export type ParserSafeRegionRefusal =
27
+ /** a marker is missing or the two markers are out of order: the region cannot be located at all */
28
+ | 'region-not-found'
29
+ /** a backtick sits between the two markers — the Workflow parser rejects the file */
30
+ | 'backtick-in-region';
31
+
32
+ export interface ParserSafeRegionVerdict {
33
+ /** true only when the region was LOCATED and contains no backtick. */
34
+ readonly ok: boolean;
35
+ /** the refusal reason, or `null` on a clean verdict. */
36
+ readonly refusal: ParserSafeRegionRefusal | null;
37
+ /** absolute character offset of the first offending backtick, or `null`. */
38
+ readonly index: number | null;
39
+ /** 1-based line number of the first offending backtick, or `null`. */
40
+ readonly line: number | null;
41
+ }
42
+
43
+ /**
44
+ * Decide whether `source` respects the parser-safe region.
45
+ *
46
+ * A source whose markers cannot be located is NOT quietly cleared — an unlocatable region is
47
+ * `region-not-found`, never `ok`. That asymmetry is deliberate: a refactor that moves or renames the
48
+ * markers must be loud, because a silently-skipped check is indistinguishable from a passing one.
49
+ */
50
+ export function checkParserSafeRegion(source: string): ParserSafeRegionVerdict {
51
+ const start = source.indexOf(PARSER_SAFE_REGION_START);
52
+ const end = source.indexOf(PARSER_SAFE_REGION_END);
53
+ if (start === -1 || end === -1 || end <= start) {
54
+ return { ok: false, refusal: 'region-not-found', index: null, line: null };
55
+ }
56
+ const offset = source.slice(start, end).indexOf('`');
57
+ if (offset === -1) return { ok: true, refusal: null, index: null, line: null };
58
+ const index = start + offset;
59
+ return {
60
+ ok: false,
61
+ refusal: 'backtick-in-region',
62
+ index,
63
+ line: source.slice(0, index).split('\n').length,
64
+ };
65
+ }
package/src/patterns.ts CHANGED
@@ -31,6 +31,7 @@ import { resolveAgentdbPath } from './agentdb-index.js';
31
31
  import type { VectorEntry } from './vector-tier.js';
32
32
  import { rankLessonsByDelta, type LessonHistory } from './safla-delta.js';
33
33
  import { withStoreLock, withStoreLockSync, StoreLockTimeoutError, StoreLockCompromisedError } from './store-lock.js';
34
+ import { describeNativeDep, exerciseSqliteOpen, probeNativeDep } from './native-dep-probe.js';
34
35
 
35
36
  /** A learned pattern as written by `dz teach` to `.dz/patterns.jsonl`. */
36
37
  export interface PatternRecord {
@@ -1272,7 +1273,11 @@ function countNoiseVectors(projectRoot: string): { removed: number; error?: stri
1272
1273
  db.close();
1273
1274
  }
1274
1275
  } catch (err) {
1275
- return { removed: 0, error: `vector mirror: ${err instanceof Error ? err.message : String(err)}` };
1276
+ const verdict = probeNativeDep(projectRoot, 'better-sqlite3', exerciseSqliteOpen, 'sqlite-open');
1277
+ const message = verdict.state === 'unusable'
1278
+ ? describeNativeDep(verdict)
1279
+ : err instanceof Error ? err.message : String(err);
1280
+ return { removed: 0, error: `vector mirror: ${message}` };
1276
1281
  }
1277
1282
  }
1278
1283
 
@@ -1314,6 +1319,10 @@ function pruneNoiseVectors(projectRoot: string): { removed: number; error?: stri
1314
1319
  db.close();
1315
1320
  }
1316
1321
  } catch (err) {
1322
+ const verdict = probeNativeDep(projectRoot, 'better-sqlite3', exerciseSqliteOpen, 'sqlite-open');
1323
+ if (verdict.state === 'unusable') {
1324
+ return { removed: 0, error: `vector mirror: ${describeNativeDep(verdict)}` };
1325
+ }
1317
1326
  return { removed: 0, error: `vector prune failed: ${err instanceof Error ? err.message : String(err)}` };
1318
1327
  }
1319
1328
  }