@dzhechkov/harness-core 0.8.34 → 0.8.36

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 (62) hide show
  1. package/.dz-manifest.json +61 -61
  2. package/README.md +255 -12
  3. package/dist/agentdb-index.d.ts +22 -1
  4. package/dist/agentdb-index.d.ts.map +1 -1
  5. package/dist/agentdb-index.js +156 -6
  6. package/dist/agentdb-index.js.map +1 -1
  7. package/dist/apply-leg.d.ts +180 -2
  8. package/dist/apply-leg.d.ts.map +1 -1
  9. package/dist/apply-leg.js +781 -38
  10. package/dist/apply-leg.js.map +1 -1
  11. package/dist/codex-hooks-assets.d.ts.map +1 -1
  12. package/dist/codex-hooks-assets.js +67 -5
  13. package/dist/codex-hooks-assets.js.map +1 -1
  14. package/dist/codex-hooks.d.ts +13 -1
  15. package/dist/codex-hooks.d.ts.map +1 -1
  16. package/dist/codex-hooks.js +13 -1
  17. package/dist/codex-hooks.js.map +1 -1
  18. package/dist/index.d.ts +8 -6
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +9 -4
  21. package/dist/index.js.map +1 -1
  22. package/dist/mutation-gate.d.ts +19 -0
  23. package/dist/mutation-gate.d.ts.map +1 -1
  24. package/dist/mutation-gate.js +37 -1
  25. package/dist/mutation-gate.js.map +1 -1
  26. package/dist/operations.d.ts +17 -1
  27. package/dist/operations.d.ts.map +1 -1
  28. package/dist/operations.js +88 -9
  29. package/dist/operations.js.map +1 -1
  30. package/dist/publish.d.ts +59 -7
  31. package/dist/publish.d.ts.map +1 -1
  32. package/dist/publish.js +205 -32
  33. package/dist/publish.js.map +1 -1
  34. package/dist/release-line.d.ts +16 -0
  35. package/dist/release-line.d.ts.map +1 -1
  36. package/dist/release-line.js +31 -0
  37. package/dist/release-line.js.map +1 -1
  38. package/dist/setup.d.ts.map +1 -1
  39. package/dist/setup.js +90 -14
  40. package/dist/setup.js.map +1 -1
  41. package/dist/skills.d.ts +87 -3
  42. package/dist/skills.d.ts.map +1 -1
  43. package/dist/skills.js +266 -15
  44. package/dist/skills.js.map +1 -1
  45. package/dist/vector-tier.d.ts +34 -3
  46. package/dist/vector-tier.d.ts.map +1 -1
  47. package/dist/vector-tier.js +117 -22
  48. package/dist/vector-tier.js.map +1 -1
  49. package/package.json +2 -2
  50. package/sbom.json +60 -60
  51. package/src/agentdb-index.ts +158 -7
  52. package/src/apply-leg.ts +824 -38
  53. package/src/codex-hooks-assets.ts +67 -5
  54. package/src/codex-hooks.ts +13 -1
  55. package/src/index.ts +15 -2
  56. package/src/mutation-gate.ts +58 -2
  57. package/src/operations.ts +91 -10
  58. package/src/publish.ts +247 -30
  59. package/src/release-line.ts +32 -0
  60. package/src/setup.ts +81 -16
  61. package/src/skills.ts +303 -14
  62. package/src/vector-tier.ts +147 -24
@@ -12,7 +12,7 @@
12
12
  * @packageDocumentation
13
13
  */
14
14
 
15
- import { existsSync, mkdirSync, copyFileSync, realpathSync } from 'node:fs';
15
+ import { existsSync, mkdirSync, copyFileSync, realpathSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
16
16
  import { join, dirname, resolve, relative, isAbsolute, basename } from 'node:path';
17
17
  import { pathToFileURL } from 'node:url';
18
18
  import { createRequire } from 'node:module';
@@ -23,7 +23,7 @@ import { applyReadonlyPragmas } from './sqlite-read-helpers.js';
23
23
  import { rotatePreReindexSnapshotsUnlocked, type SnapshotRotationReport } from './agentdb-snapshot-rotation.js';
24
24
  import { snapshotSqliteDatabase, restoreSqliteSnapshot, type SnapshotMethod, type SnapshotDbCtor } from './agentdb-snapshot.js';
25
25
  import { withAgentdbSnapshotLock, writeReindexMarker, clearReindexMarker, markReindexMarkerRecoveryRequired, reindexMarkerPath, msFromBackupPath } from './agentdb-reindex-marker.js';
26
- import { NamedLockTimeoutError } from './named-lock.js';
26
+ import { NamedLockTimeoutError, withNamedLockSync } from './named-lock.js';
27
27
  import type { StoreLockOptions } from './store-lock.js';
28
28
  // The backlog dedup embed form (PURE, zero-dep — no cycle): dz-backlog rows must be embedded in the
29
29
  // SAME bounded form the dedup query uses, including through the reindex path.
@@ -48,10 +48,14 @@ export interface AgentdbRow {
48
48
  readonly avgReward?: number;
49
49
  }
50
50
 
51
- /** Outcome of {@link indexPatternsToAgentdb}. */
51
+ /** Outcome of {@link indexPatternsToAgentdb}. `generationBumped`/`generationReason` are present only
52
+ * when a store write actually happened (`indexed > 0`) — FR-4: a failed counter write NEVER fails
53
+ * the indexing call itself, it is only reported so a caller (`dz doctor`, telemetry) can see it. */
52
54
  export interface AgentdbIndexResult {
53
55
  readonly indexed: number;
54
56
  readonly error?: string | undefined;
57
+ readonly generationBumped?: boolean;
58
+ readonly generationReason?: string;
55
59
  }
56
60
 
57
61
  interface NativeDb {
@@ -135,6 +139,112 @@ export function ensureAgentdbSchema(projectRoot: string, dbPath?: string): { rea
135
139
  }
136
140
  }
137
141
 
142
+ /** `<dbFile>.generation` — a sidecar counter next to the store itself, so it travels with any copy
143
+ * of `.dz/agentdb.db` (backup, mirror sync) without a separate path to keep in sync. */
144
+ function generationFilePath(dbFile: string): string {
145
+ return `${dbFile}.generation`;
146
+ }
147
+
148
+ /** AM-5 (fix-round): the ONLY shape {@link readStoreGeneration} trusts — one or more ASCII digits,
149
+ * nothing else. `Number.parseInt` alone accepts a leading-numeric-with-trailing-junk string like
150
+ * `"12junk"` as `12`; that reads a corrupt sidecar as a plausible generation instead of degrading to
151
+ * the documented `0` compatibility floor. */
152
+ const STRICT_GENERATION = /^\d+$/;
153
+
154
+ /**
155
+ * FR-2/FR-3 (`store-generation-counter`): the store's write-generation counter, read back. A
156
+ * missing file (a store that predates this feature, or one that has never been written through
157
+ * {@link bumpStoreGeneration}) reads as `0` — the compatibility floor {@link getOrOpenEngine}'s
158
+ * caller compares against, never an error. A corrupt/non-numeric file degrades the same way (best
159
+ * effort — a bad counter must never crash a read path), never a throw. AM-5: the content must match
160
+ * {@link STRICT_GENERATION} exactly — `Number.parseInt`'s leading-digits-only tolerance is NOT used
161
+ * to decide validity, only to convert an already-validated string.
162
+ */
163
+ export function readStoreGeneration(projectRoot: string, dbPath?: string): number {
164
+ try {
165
+ const raw = readFileSync(generationFilePath(resolveAgentdbPath(projectRoot, dbPath)), 'utf8').trim();
166
+ if (!STRICT_GENERATION.test(raw)) return 0;
167
+ const n = Number.parseInt(raw, 10);
168
+ return Number.isFinite(n) && n >= 0 ? n : 0;
169
+ } catch {
170
+ return 0;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * FR-1 (`store-generation-counter`): monotonically advance the store's write-generation counter by
176
+ * one (absent file ⇒ starts at 1), atomically (tmp + rename — the same durability discipline every
177
+ * other sidecar file in this module uses; a reader can never observe a half-written counter) AND
178
+ * under mutual exclusion (AM-2, fix-round after Codex review). A bare read→compute→rename with no
179
+ * lock lets two concurrent writers both read the same current value and both publish `current+1` —
180
+ * one bump is lost — or lets a DELAYED writer overwrite a later value with an earlier one (the
181
+ * counter briefly goes backwards on disk). `withNamedLockSync` (`named-lock.ts`, the repo's
182
+ * advisory lock for a read-modify-write file store — `.claude/rules/cross-runtime-concurrency.md`)
183
+ * serializes the critical section; the counter is RE-READ from disk *inside* the lock (never trusted
184
+ * from before acquisition), so the sequence every process observes is strictly monotonic
185
+ * (`agentdb-reindex-marker.ts`'s `withAgentdbSnapshotLock` is the precedent this mirrors — a lock
186
+ * addressed by `dirname(dbFile)`, a pure function of the store's own directory, never of
187
+ * `process.cwd()`).
188
+ *
189
+ * NEVER throws (FR-4): a write failure (read-only `.dz`, a full disk, a permissions error, an
190
+ * unresolvable path, OR a lock that could not be acquired by its deadline) is reported honestly as
191
+ * `{ok:false, error}` so the caller can log it — the store write it accompanies must stay successful
192
+ * regardless, telemetry is never a gate. AM-4: {@link resolveAgentdbPath} itself now runs INSIDE this
193
+ * function's outer `try` — an unresolvable path can no longer throw OUT of `bumpStoreGeneration`
194
+ * either; "never throws" now covers the whole function, not just the file-write tail.
195
+ */
196
+ /** Codex round-2 (NEW HIGH): most mutators discard the bump result, so a failed bump must be
197
+ * VISIBLE on its own — one stderr line, written by the helper itself. Telemetry never throws. */
198
+ function reportBumpFailure(error: string): { readonly ok: false; readonly error: string } {
199
+ try { process.stderr.write(`dz: store generation not bumped — ${error}\n`); } catch { /* telemetry never throws */ }
200
+ return { ok: false, error };
201
+ }
202
+
203
+ export function bumpStoreGeneration(
204
+ projectRoot: string,
205
+ dbPath?: string,
206
+ ): { readonly ok: true; readonly generation: number } | { readonly ok: false; readonly error: string } {
207
+ try {
208
+ const dbFile = resolveAgentdbPath(projectRoot, dbPath);
209
+ const genFile = generationFilePath(dbFile);
210
+ return withNamedLockSync(
211
+ dirname(dbFile),
212
+ 'store-generation',
213
+ (): { readonly ok: true; readonly generation: number } | { readonly ok: false; readonly error: string } => {
214
+ // AM-2: re-read the CURRENT value from disk while holding the lock — a value observed before
215
+ // acquisition may already be stale, another holder may have advanced it in the meantime.
216
+ let current = readStoreGeneration(projectRoot, dbPath);
217
+ const tmp = `${genFile}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
218
+ try {
219
+ // Codex round-2 (AM-2 residual): a sidecar that EXISTS but is corrupt reads as 0 and would
220
+ // reset the counter to 1 — a value an engine-cache entry may already be keyed on. Floor a
221
+ // corrupt value at a wall-clock stamp instead: still monotonic (ms since epoch exceeds any
222
+ // count reached by bumping) and never colliding with an earlier generation. Absent file ⇒ 1.
223
+ if (current === 0 && existsSync(genFile)) {
224
+ const raw = readFileSync(genFile, 'utf8').trim();
225
+ if (raw !== '0' && !STRICT_GENERATION.test(raw)) current = Date.now();
226
+ }
227
+ const next = current + 1;
228
+ mkdirSync(dirname(genFile), { recursive: true });
229
+ writeFileSync(tmp, String(next), { encoding: 'utf8', flag: 'wx' });
230
+ renameSync(tmp, genFile);
231
+ return { ok: true, generation: next };
232
+ } catch (err) {
233
+ // Best-effort cleanup of a half-written temp file (e.g. rename failed after a successful
234
+ // write) so it never lingers as clutter — never lets a cleanup failure mask the real error.
235
+ try { if (existsSync(tmp)) unlinkSync(tmp); } catch { /* best-effort only */ }
236
+ return reportBumpFailure(`store generation bump failed: ${err instanceof Error ? err.message : String(err)}`);
237
+ }
238
+ },
239
+ );
240
+ } catch (err) {
241
+ // AM-2/FR-4: a lock that could not be acquired by its deadline (`NamedLockTimeoutError`) — and
242
+ // any other failure reaching this point (an unresolvable path, AM-4) — degrades to the same
243
+ // honest `{ok:false, error}` shape; it never throws into the store write it accompanies.
244
+ return reportBumpFailure(`store generation bump failed: ${err instanceof Error ? err.message : String(err)}`);
245
+ }
246
+ }
247
+
138
248
  /**
139
249
  * Index `rows` into the shared AgentDB vector store. Returns `{indexed:0}` for an empty input and
140
250
  * `{indexed:0, error}` when `agentdb`/`better-sqlite3` cannot be resolved from the project.
@@ -218,8 +328,19 @@ export async function indexPatternsToAgentdb(
218
328
  return prepared.length;
219
329
  });
220
330
  const indexed = commit() as number;
331
+ // AM-3 (fix-round): bump the generation IMMEDIATELY after the commit — BEFORE
332
+ // `writeEmbedManifest` — not after it. The store already changed on disk the instant `commit()`
333
+ // returned; if the manifest write throws (a jammed manifest path, a full disk), the OLD order
334
+ // left the DB changed with no generation ever published — a stale-cache read would then serve
335
+ // an engine that never saw this write, with no signal anywhere that anything went wrong. Moving
336
+ // the bump here makes "the store changed" and "the generation reflects it" atomic in effect:
337
+ // whichever of the two calls below throws, the generation is already correct for the rows that
338
+ // are already on disk.
339
+ const bump = bumpStoreGeneration(projectRoot, opts.dbPath);
221
340
  writeEmbedManifest(dbFile, currentEmbedManifest(model, guard.manifest.version, 'agentdb'));
222
- return { indexed };
341
+ return bump.ok
342
+ ? { indexed, generationBumped: true }
343
+ : { indexed, generationBumped: false, generationReason: bump.error };
223
344
  } finally {
224
345
  db.close();
225
346
  }
@@ -726,6 +847,12 @@ export async function importVectorsToAgentdb(
726
847
  return imported;
727
848
  });
728
849
  const imported = commit();
850
+ // AM-1 (fix-round): `importVectorsToAgentdb` is the write-half of `dz vector import` — it
851
+ // changes `pattern_embeddings`/`reasoning_patterns` exactly like `indexPatternsToAgentdb`, so
852
+ // it must bump the SAME counter (the whole point of a single "did the store change" signal is
853
+ // that every writer feeds it, not just one). Ordered before `writeEmbedManifest`, same AM-3
854
+ // rationale: the rows are already on disk by the time `commit()` returns.
855
+ bumpStoreGeneration(projectRoot, opts.dbPath);
729
856
  writeEmbedManifest(dbFile, currentEmbedManifest(model, guard.manifest.version, 'agentdb'));
730
857
  return { imported };
731
858
  } finally {
@@ -773,7 +900,14 @@ export function clearAgentdbQuarantine(
773
900
  }
774
901
  return cleared;
775
902
  });
776
- return { cleared: tx() };
903
+ const cleared = tx();
904
+ // AM-1 (fix-round): a quarantine clear mutates `metadata` on rows the hook daemon reads
905
+ // straight from this mirror (see the doc comment above) — it changes what a query returns, so
906
+ // it must bump too, exactly like every other mutator. Only when something actually changed
907
+ // (`cleared > 0`) — same "no write, no bump" discipline as `indexPatternsToAgentdb`'s
908
+ // empty-rows case (AC-2).
909
+ if (cleared > 0) bumpStoreGeneration(projectRoot, opts.dbPath);
910
+ return { cleared };
777
911
  } finally {
778
912
  db.close();
779
913
  }
@@ -829,7 +963,11 @@ export function deleteAgentdbByDzIds(
829
963
  }
830
964
  return deleted;
831
965
  });
832
- return { deleted: tx() };
966
+ const deleted = tx();
967
+ // AM-1 (fix-round): a DELETE removes rows from a query's result set exactly as surely as an
968
+ // INSERT adds them — it must bump too. Only when rows actually left the store (`deleted > 0`).
969
+ if (deleted > 0) bumpStoreGeneration(projectRoot, opts.dbPath);
970
+ return { deleted };
833
971
  } finally {
834
972
  db.close();
835
973
  }
@@ -874,7 +1012,13 @@ export function bumpAgentdbUses(
874
1012
  }
875
1013
  return bumped;
876
1014
  });
877
- return { bumped: tx() };
1015
+ const bumped = tx();
1016
+ // AM-1 (fix-round): `uses`/`avg_reward` feed reward-weighted ranking — a change here is a real
1017
+ // store mutation, so it bumps the SAME counter (deliberately NOT named `bump` — that identifier
1018
+ // is this function's own return value; the counter helper is called by its full name below to
1019
+ // avoid the collision). Only when a row actually changed (`bumped > 0`).
1020
+ if (bumped > 0) bumpStoreGeneration(projectRoot, opts.dbPath);
1021
+ return { bumped };
878
1022
  } finally {
879
1023
  db.close();
880
1024
  }
@@ -1157,6 +1301,13 @@ export async function reindexAgentdbRows(
1157
1301
  if (!(err instanceof NamedLockTimeoutError)) throw err;
1158
1302
  snapshots = { kept: [], removed: [], removedBytes: 0, keep: keepSnapshots, errors: [`lock busy: ${err.message}`] };
1159
1303
  }
1304
+ // AM-1 (fix-round): `reindexAgentdbRows` is itself a mutator (the DELETE above rebuilds the
1305
+ // owned task types) — it must not rely SOLELY on `indexPatternsToAgentdb`'s own internal bump,
1306
+ // because that call is a no-op (and bumps nothing) when `rows` is empty, yet the DELETE it ran
1307
+ // just above unconditionally changed the store. Bumping again here when `rows` was non-empty
1308
+ // (the common case, already bumped once inside `indexPatternsToAgentdb`) is harmless — the
1309
+ // counter is a monotonic "did anything change" signal, not a per-operation tally.
1310
+ bumpStoreGeneration(projectRoot, opts.dbPath);
1160
1311
  return {
1161
1312
  reembedded: indexed.indexed,
1162
1313
  model: model.model,