@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.
- package/.dz-manifest.json +61 -61
- package/README.md +255 -12
- package/dist/agentdb-index.d.ts +22 -1
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +156 -6
- package/dist/agentdb-index.js.map +1 -1
- package/dist/apply-leg.d.ts +180 -2
- package/dist/apply-leg.d.ts.map +1 -1
- package/dist/apply-leg.js +781 -38
- package/dist/apply-leg.js.map +1 -1
- package/dist/codex-hooks-assets.d.ts.map +1 -1
- package/dist/codex-hooks-assets.js +67 -5
- package/dist/codex-hooks-assets.js.map +1 -1
- package/dist/codex-hooks.d.ts +13 -1
- package/dist/codex-hooks.d.ts.map +1 -1
- package/dist/codex-hooks.js +13 -1
- package/dist/codex-hooks.js.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/mutation-gate.d.ts +19 -0
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +37 -1
- package/dist/mutation-gate.js.map +1 -1
- package/dist/operations.d.ts +17 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +88 -9
- package/dist/operations.js.map +1 -1
- package/dist/publish.d.ts +59 -7
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +205 -32
- package/dist/publish.js.map +1 -1
- package/dist/release-line.d.ts +16 -0
- package/dist/release-line.d.ts.map +1 -1
- package/dist/release-line.js +31 -0
- package/dist/release-line.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +90 -14
- package/dist/setup.js.map +1 -1
- package/dist/skills.d.ts +87 -3
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +266 -15
- package/dist/skills.js.map +1 -1
- package/dist/vector-tier.d.ts +34 -3
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +117 -22
- package/dist/vector-tier.js.map +1 -1
- package/package.json +2 -2
- package/sbom.json +60 -60
- package/src/agentdb-index.ts +158 -7
- package/src/apply-leg.ts +824 -38
- package/src/codex-hooks-assets.ts +67 -5
- package/src/codex-hooks.ts +13 -1
- package/src/index.ts +15 -2
- package/src/mutation-gate.ts +58 -2
- package/src/operations.ts +91 -10
- package/src/publish.ts +247 -30
- package/src/release-line.ts +32 -0
- package/src/setup.ts +81 -16
- package/src/skills.ts +303 -14
- package/src/vector-tier.ts +147 -24
package/src/agentdb-index.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|