@dzhechkov/harness-cli 0.8.24 → 0.8.26
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 +12 -12
- package/README.md +323 -27
- package/dist/cli.d.ts +44 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +910 -193
- package/dist/cli.js.map +1 -1
- package/dist/known-flags.d.ts.map +1 -1
- package/dist/known-flags.js +7 -0
- package/dist/known-flags.js.map +1 -1
- package/package.json +2 -2
- package/sbom.json +11 -11
- package/src/cli.ts +893 -145
- package/src/known-flags.ts +7 -0
package/.dz-manifest.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
"path": "README.md",
|
|
12
|
-
"sha256": "
|
|
12
|
+
"sha256": "61e9c0cf5c85f1a9841af35346e6ff6351b8cdb95d11aa3b5b7c0bd82d0a6a5e"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"path": "dist/bin.d.ts",
|
|
@@ -45,19 +45,19 @@
|
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
"path": "dist/cli.d.ts",
|
|
48
|
-
"sha256": "
|
|
48
|
+
"sha256": "1452c5529edf1c4d57711ae9c6610cf37c7619a88024c3874c499060fc4d1c31"
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
"path": "dist/cli.d.ts.map",
|
|
52
|
-
"sha256": "
|
|
52
|
+
"sha256": "e750aecbc3e43a94e490b21d675f12f14dd24ddc364fb40ad811edc35e98af94"
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"path": "dist/cli.js",
|
|
56
|
-
"sha256": "
|
|
56
|
+
"sha256": "b1a441c645ff9bcbc592069eab87742744fd6f6826cfa560f7b8203117ab927f"
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
"path": "dist/cli.js.map",
|
|
60
|
-
"sha256": "
|
|
60
|
+
"sha256": "c14e8a4542ac690b6033e69b01edf22554d6b9ac4e431a39ee0910445059e319"
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
63
|
"path": "dist/command-inventory.d.ts",
|
|
@@ -129,15 +129,15 @@
|
|
|
129
129
|
},
|
|
130
130
|
{
|
|
131
131
|
"path": "dist/known-flags.d.ts.map",
|
|
132
|
-
"sha256": "
|
|
132
|
+
"sha256": "db7b4dead9a5ca4054c1e1e7df8c56eed65a9fa31771ab0b4930c67e228f326c"
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"path": "dist/known-flags.js",
|
|
136
|
-
"sha256": "
|
|
136
|
+
"sha256": "77eac32f2c0e552e0417205a43f40d18feacb951a768b9303360f0321fd79f6b"
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
"path": "dist/known-flags.js.map",
|
|
140
|
-
"sha256": "
|
|
140
|
+
"sha256": "abf5332fb08f5403d244232cf7528be8163fad17de90ec48fb0d5dc5c995ec03"
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
143
|
"path": "keys/README.md",
|
|
@@ -149,7 +149,7 @@
|
|
|
149
149
|
},
|
|
150
150
|
{
|
|
151
151
|
"path": "package.json",
|
|
152
|
-
"sha256": "
|
|
152
|
+
"sha256": "15747dd6aec74f1ea5202fb43ecc81ded2f8f1a7e7471fd26561c863b4ee3ac8"
|
|
153
153
|
},
|
|
154
154
|
{
|
|
155
155
|
"path": "src/bin.ts",
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
},
|
|
162
162
|
{
|
|
163
163
|
"path": "src/cli.ts",
|
|
164
|
-
"sha256": "
|
|
164
|
+
"sha256": "d1140c5f8c02f9eaf912a6c75d161de0be2413d9c2eec84446f3129caec6d2b4"
|
|
165
165
|
},
|
|
166
166
|
{
|
|
167
167
|
"path": "src/command-inventory.ts",
|
|
@@ -181,9 +181,9 @@
|
|
|
181
181
|
},
|
|
182
182
|
{
|
|
183
183
|
"path": "src/known-flags.ts",
|
|
184
|
-
"sha256": "
|
|
184
|
+
"sha256": "31473bd65a04071f13b7a5994db1b0005b85494f8b8abcc4321c8f5927270fe0"
|
|
185
185
|
}
|
|
186
186
|
]
|
|
187
187
|
},
|
|
188
|
-
"signature": "
|
|
188
|
+
"signature": "eCd/+2pUENEEaUOCGFqCLA0FRD18kvDSmNY4YNaJTSmFeT+QI+UO54LdRm7FlUvzAvUkigu+hZDx0pfdC2h1AQ=="
|
|
189
189
|
}
|
package/README.md
CHANGED
|
@@ -1321,6 +1321,40 @@ dz mutation-gate --json # machine contract {baseline, results, sum
|
|
|
1321
1321
|
npm run test:mutation # the package's own alias for the full run
|
|
1322
1322
|
```
|
|
1323
1323
|
|
|
1324
|
+
**Scope the gate to what a FEATURE actually touched — `--touched` / `--added-since`.** A whole-registry
|
|
1325
|
+
sweep does not fit a per-feature Step 8: on this repo's core package it is 358 entries, 30-40 minutes,
|
|
1326
|
+
and hit the timeout wall INCONCLUSIVE every time (MEASURED 2026-09-12) — yet a feature usually owns only
|
|
1327
|
+
the files it edited and the entries it just added. `--touched <path[,path]>` selects entries whose `file`
|
|
1328
|
+
matches one of the given paths — accepted in package-relative POSIX form (the plain case), with a leading
|
|
1329
|
+
`./`, as an ABSOLUTE path inside the package, as a REPO-relative path (package prefix stripped, so a
|
|
1330
|
+
Step-7 change list in repo-relative form works unmodified), or backslash-separated (a Windows-authored
|
|
1331
|
+
change list handed to a POSIX runner); all forms normalize to package-relative POSIX before matching
|
|
1332
|
+
(fix-round 1, AM-1). A path that resolves OUTSIDE the package is not silently dropped — it is counted
|
|
1333
|
+
and reported as `<K> outside package` in the `selected N of M` line. `--added-since <git-ref>` selects
|
|
1334
|
+
entries whose `id` is NOT present in the registry as `git show <ref>:<registry path>` read it: a registry
|
|
1335
|
+
genuinely ABSENT at that ref means every current entry counts as added, and the run says so explicitly
|
|
1336
|
+
(`base registry absent at <ref> — all M entries count as added`); an unresolvable ref, any OTHER git
|
|
1337
|
+
failure, or a base registry that is present but INVALID/malformed at that ref is a usage error (exit 2),
|
|
1338
|
+
never folded into "absent" (fix-round 1, AM-3 — collapsing all three used to hide real failures behind a
|
|
1339
|
+
silently-too-generous selection). The two selectors UNION with each other and INTERSECT with `--only`
|
|
1340
|
+
when both are given — same set algebra as any filter chain — and the run prints exactly how it selected.
|
|
1341
|
+
`--json` always carries a `selection` object (`{selected, total, touched, addedSince, base,
|
|
1342
|
+
outsidePackage}`) on every scoped run, not only the empty-selection case (fix-round 1, AM-4):
|
|
1343
|
+
|
|
1344
|
+
```bash
|
|
1345
|
+
dz mutation-gate --touched src/cli.ts,src/mutation-gate.ts # only entries on these two files
|
|
1346
|
+
dz mutation-gate --added-since HEAD # only entries this run just added
|
|
1347
|
+
dz mutation-gate --touched src/cli.ts --added-since HEAD --only gate-touched-selector
|
|
1348
|
+
# → mutation-gate: selected 1 of 81 entries (touched: 1; added-since HEAD: 1)
|
|
1349
|
+
```
|
|
1350
|
+
|
|
1351
|
+
An empty match is SAID, never silent, and exits 0 without spending a scratch-copy run:
|
|
1352
|
+
`mutation-gate: 0 entries match touched: 0 — nothing to run`. **Recommended for Step 8 of `/feature-adr`**:
|
|
1353
|
+
scope to `--touched <this run's file list> --added-since <base ref>` for the per-feature gate; run the
|
|
1354
|
+
gate WITHOUT selectors as a separate, periodic regular task (not per feature) to keep the whole registry
|
|
1355
|
+
honest — the scoped run proves the feature's own protections, the full sweep proves the registry hasn't
|
|
1356
|
+
drifted anywhere else.
|
|
1357
|
+
|
|
1324
1358
|
Expected output (abridged from a real run — MEASURED 2026-08-07, reproducer: `cd
|
|
1325
1359
|
packages/@dzhechkov/health-advisor && dz mutation-gate`, 18 entries over a 484-test `node --test`
|
|
1326
1360
|
suite; wall-clock 9m32s with the default per-entry re-baseline, 4m53s with `--rebaseline final` —
|
|
@@ -1358,6 +1392,36 @@ Four rules the gate itself obeys — these are what distinguish it from a green-
|
|
|
1358
1392
|
4. **The gate carries its own discrimination proof**: a fixture package with a deliberately
|
|
1359
1393
|
undefended property lives in this repo's test suite, and the gate MUST fail on it — a gate that
|
|
1360
1394
|
cannot fail cannot pass.
|
|
1395
|
+
5. **A suite timeout is a VERDICT, never a fabricated exit code** (mutation-gate-timeout-verdict,
|
|
1396
|
+
2026-09-14). node's `spawnSync` sets `error.code === 'ETIMEDOUT'` on a real timeout regardless of
|
|
1397
|
+
whether the killed process manages to report its own numeric exit status first — MEASURED: a
|
|
1398
|
+
script that traps the kill signal and calls `process.exit(1)` makes `spawnSync` report
|
|
1399
|
+
`status: 1` AND `error.code: 'ETIMEDOUT'` at once. Before this fix, that numeric status silently
|
|
1400
|
+
outranked the timeout flag and the gate printed `baseline suite RED (exit 1)` for a suite that
|
|
1401
|
+
never actually ran to completion — indistinguishable from a real red suite. Now every ETIMEDOUT
|
|
1402
|
+
is reported `INCONCLUSIVE`, naming the elapsed time, the killed child's own exit/signal, and a
|
|
1403
|
+
`--timeout` suggestion (`ceil(2× elapsed)`).
|
|
1404
|
+
The suite-run ceiling itself resolves **`--timeout` flag > the registry's own `timeoutMs` field >
|
|
1405
|
+
the 300000ms default**, printed in the header as `mutation-gate: timeout: <ms> ms (<source>)`. A
|
|
1406
|
+
package whose real baseline run is longer than the default (this repo's `harness-core`, whose
|
|
1407
|
+
`test/mutation-registry.json` declares `"timeoutMs": 900000` — MEASURED ≈5–8 min baseline) needs
|
|
1408
|
+
no `--timeout` flag at all: a bare `dz mutation-gate` in that package now succeeds where it used
|
|
1409
|
+
to need a manually-remembered flag.
|
|
1410
|
+
6. **A suite runs at vitest's DEFAULT worker count (= cpu cores) unless capped, and an uncapped full
|
|
1411
|
+
suite can take the machine down** (mutation-gate-baseline-honesty, 2026-09-15). MEASURED: under
|
|
1412
|
+
this repo's `harness-core` embedding-daemon tests (0.7–3.5 GB/process), an 8-core/16GB box hit
|
|
1413
|
+
load 62–358 and 0.4–1.8 GB free — three full overnight gate runs died. The same suite at
|
|
1414
|
+
`--maxWorkers=2` passed 6909/6909. The worker ceiling resolves **`--max-workers` flag > the
|
|
1415
|
+
registry's own `maxWorkers` field > `min(4, max(1, floor(cpus/2)))`**, is injected as
|
|
1416
|
+
`--maxWorkers=<n>` into a `vitest run` testCommand (unless the command already names the flag —
|
|
1417
|
+
never double-injected) and sets `VITEST_MAX_WORKERS=<n>` in the env unconditionally (a vitest
|
|
1418
|
+
command reached indirectly through a wrapper script is still capped). Printed as
|
|
1419
|
+
`mutation-gate: workers: <n> (<flag|registry|default>)`, or
|
|
1420
|
+
`mutation-gate: workers: n/a — test command is not vitest` for a non-vitest command. Also
|
|
1421
|
+
fixed the same session: vitest 3 prints an optional POOL LABEL between `FAIL` and the file path
|
|
1422
|
+
(`FAIL |serial| test/x.test.ts …`) — the baseline-attribution parser used to capture the label
|
|
1423
|
+
itself as "the file" and report a perfectly parseable red run as `unparseable from runner
|
|
1424
|
+
output`; the regex now skips the optional label.
|
|
1361
1425
|
|
|
1362
1426
|
**Where the full output of a RED baseline/rebaseline line lives** (gate-stability, 2026-09-12,
|
|
1363
1427
|
fix-round-1 2026-09-12): the bounded 3-line/20-line tail in the verdict is a teaser, and under a
|
|
@@ -1455,7 +1519,25 @@ is not a false gate; `--audit-dev` widens; an audit that *cannot run* also block
|
|
|
1455
1519
|
`STALE_DIST`, never checked as-is; a package that declares a `build` script but has **no** dist JS is
|
|
1456
1520
|
`MISSING_DIST` — an unbuilt package cannot ship), (4) smoke-boot every bin via `node <bin> --help` in a
|
|
1457
1521
|
throwaway cwd with a timeout. Packages without a `test` script are **named skips** in the report — never
|
|
1458
|
-
silent passes; a template-only pack (no build, no artifacts, no bin) is a named `SKIP_NO_ARTIFACTS`.
|
|
1522
|
+
silent passes; a template-only pack (no build, no artifacts, no bin) is a named `SKIP_NO_ARTIFACTS`. The
|
|
1523
|
+
packed-install smoke's `pack` steps run against a **staged** `package.json` — `workspace:*` sibling
|
|
1524
|
+
deps rewritten to the exact sibling version, `scripts.prepublishOnly` dropped, written and restored
|
|
1525
|
+
atomically (temp file + rename), one package at a time — the same staging `dz publish`'s preview
|
|
1526
|
+
applies, so `npm install` on the resulting tarballs does not fail on a literal `workspace:*` spec. A
|
|
1527
|
+
restore that fails is reported as a FAILED step (the tree is left staged and says so), never as a pass.
|
|
1528
|
+
A red `tests`/`syntax`/`smoke` step names the failing test(s) when the output is a runner shape it
|
|
1529
|
+
recognises (vitest summary/FAIL/×/❯ lines, or node:test/TAP `not ok N …` + `# fail N`) — colour
|
|
1530
|
+
codes are stripped first, so a coloured runner is recognised too; otherwise it falls back to the
|
|
1531
|
+
first output line, marked `(no test-runner summary recognised)` so the difference from a parsed
|
|
1532
|
+
summary is visible. Beneath the reason it prints the tail **per stream**, labelled `stdout:` /
|
|
1533
|
+
`stderr:` — the two streams are captured independently, so their relative order (as the process
|
|
1534
|
+
actually interleaved them) is **not** reconstructed, only each stream's own tail order. Both
|
|
1535
|
+
streams appear the same way in the report, the `--json` output (a `tails: {stdout, stderr}` object
|
|
1536
|
+
on every executed tests/syntax/smoke/timeout failure — present with empty strings when a stream
|
|
1537
|
+
produced nothing), and the best-effort `gh issue`, where every tail is additionally **redacted**
|
|
1538
|
+
(`token`/`secret`/`password` values, `Bearer …`, `npm_…`/`ghp_…`/`sk-…`/`AKIA…` tokens, and long
|
|
1539
|
+
opaque base64/hex-ish blobs all become `[redacted]`) and the whole issue body is capped at 60KB,
|
|
1540
|
+
shrinking every tail evenly rather than dropping some whole while leaving others untouched.
|
|
1459
1541
|
|
|
1460
1542
|
```bash
|
|
1461
1543
|
dz release --dry-run # full gate plan, zero commands executed
|
|
@@ -2174,7 +2256,7 @@ probe that both selects the workspace build or PATH fallback and prints the sele
|
|
|
2174
2256
|
dz runs [--settle] [--stall-minutes N] [--json] [--project <dir>] (read the run registry: live / stalled / orphaned / inconclusive / finished)
|
|
2175
2257
|
dz runs-record --event started|heartbeat|finished [--run-id <id>] [--kind <kind>] [--slug <slug>] [--pid <pid|host>] [--parent-run-id <id>] [--outcome <text>] [--project <dir>] [--json] (append one run event)
|
|
2176
2258
|
dz runs-clean [--apply] [--retention-days N] [--project <dir>] [--json] (plan cleanup of old clean merged worktrees and dead/finished registry histories; apply explicitly)
|
|
2177
|
-
dz mutation-gate [--package <dir>] [--registry <file>] [--test-cmd "<cmd>"] [--only <id[,id]>] [--timeout <ms>] [--rebaseline per-entry|final] [--keep-scratch] [--json] # the mutation gate: for each NAMED protection in a declarative registry, copy the package to a scratch dir (shadow-repo layout, node_modules symlinked, git-initialized), verify the baseline is green, apply the entry's exact {find, replace} mutation, run the suite, REQUIRE red, restore — and require the red to be ATTRIBUTABLE to the protection: a mutated file that no longer parses is MUTATION_UNPARSEABLE, a failing count far above the entry's bound (maxFailing, default from observed) is OVER_FAILING, and a restored tree that does not reproduce green makes the entry INCONCLUSIVE (flaky suite). A mutation that does not apply, a green suite, or an inconclusive run is a FAILURE — never a skip. exit 0 all proven / 1 gate failed / 2 setup error
|
|
2259
|
+
dz mutation-gate [--package <dir>] [--registry <file>] [--test-cmd "<cmd>"] [--only <id[,id]>] [--touched <path[,path]>] [--added-since <git-ref>] [--timeout <ms>] [--max-workers <n>] [--rebaseline per-entry|final] [--keep-scratch] [--json] # the mutation gate: for each NAMED protection in a declarative registry, copy the package to a scratch dir (shadow-repo layout, node_modules symlinked, git-initialized), verify the baseline is green, apply the entry's exact {find, replace} mutation, run the suite, REQUIRE red, restore — and require the red to be ATTRIBUTABLE to the protection: a mutated file that no longer parses is MUTATION_UNPARSEABLE, a failing count far above the entry's bound (maxFailing, default from observed) is OVER_FAILING, and a restored tree that does not reproduce green makes the entry INCONCLUSIVE (flaky suite). A mutation that does not apply, a green suite, or an inconclusive run is a FAILURE — never a skip. `--touched <path[,path]>` / `--added-since <ref>` scope the run to a feature's own touched files and newly added entries (they UNION with each other and INTERSECT with `--only`; an empty selection prints `selected 0 of M entries (…)` and exits 0 without running the suite — never a silent skip) — MEASURED: an unscoped sweep of 358 entries on this repo's core package took 30-40 minutes and hit the timeout wall every time. `--timeout` resolves flag > the registry's own `timeoutMs` field > a 300000ms default (printed in the header); a real ETIMEDOUT is always INCONCLUSIVE, never read as the killed child's own numeric exit code. `--max-workers` resolves flag > the registry's own `maxWorkers` field > `min(4, max(1, floor(cpus/2)))`, injects `--maxWorkers=<n>` into a `vitest run` testCommand (unless it already names the flag) and always sets `VITEST_MAX_WORKERS=<n>` in the env — printed as `mutation-gate: workers: <n> (<flag|registry|default>)`, or `n/a — test command is not vitest` when the command isn't recognised as vitest; MEASURED: an uncapped full-suite baseline (vitest's default worker count = cpu cores) hit load 62-358 and <2GB free on an 8-core/16GB box under this repo's core-package embedding-daemon tests, killing full overnight gate runs — `--maxWorkers=2` passed 6909/6909. exit 0 all proven / 1 gate failed / 2 setup error
|
|
2178
2260
|
dz delivery-check --slug <slug> [--context-only] [--findings <f.json>] [--strict] [--author <model>] [--json] # portable Step-10 Delivery Gate: the `manual` form that travels to every shell target — prints the 4-plane review brief (regressions ‖ security ‖ code-quality ‖ product-honesty) + artifact probes; --findings classifies a fed-back review into a fail-closed ready|blocked hand-off (only cross-validated BLOCKER/HIGH count) and writes features/<slug>/10_delivery_review.md; --strict exits 1 on blocked
|
|
2179
2261
|
dz skills-verify [--dir <project>] [--expect a,b] [--static] [--strict] [--timeout <s>] [--json] # does .claude/skills/ actually REGISTER? --static = instant layout scan (CI-safe, no session): flags dirs that can never register; default also starts a real session and reads the authoritative system/init listing. exit 0 pass / 1 fail / 2 inconclusive — an unobservable registration is NEVER a pass
|
|
2180
2262
|
dz compounding [--project <dir>] [--json] # honest learning-loop payoff report: pool/replay/guard instrumentation plus the monthly eligible→attempted→accepted→executions funnel. A missing source is NOT MEASURED; only a non-empty→empty named edge across three consecutive measured months is a funnel finding; text/JSON carry the same facts and no learning-health verdict
|
|
@@ -3234,16 +3316,33 @@ earlier one failed):
|
|
|
3234
3316
|
|
|
3235
3317
|
1. **Sibling drift.** For every workspace `S` a batch package depends on (`dependencies`,
|
|
3236
3318
|
`peerDependencies` AND `optionalDependencies` — all three pin and ship identically) that is NOT
|
|
3237
|
-
itself part of the batch, the gate
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
`
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3319
|
+
itself part of the batch, the gate compares `S`'s WORKSPACE copy against its published inventory,
|
|
3320
|
+
naming which mechanism produced BOTH sides on every result (`inventorySource: 'pnpm-pack' |
|
|
3321
|
+
'npm-pack' | 'readdir-approximation'`, never left implicit). In production the CLI packs `S`'s
|
|
3322
|
+
workspace dir with `pnpm pack` — the SAME transport the live publish uses — into a per-run scratch
|
|
3323
|
+
dir, unpacks it, and hands the unpacked tree to core, which hashes it with the same full recursive
|
|
3324
|
+
walk it applies to the already-unpacked registry tarball: symmetric by construction, so a file the
|
|
3325
|
+
transport synthesises into the tarball (pnpm copies the workspace-root LICENSE into a package whose
|
|
3326
|
+
own tree has none) is on BOTH sides. MEASURED 2026-09-14 on the hub: with an `npm pack --dry-run
|
|
3327
|
+
--json` path listing instead, two siblings unchanged since publication (harness-presets, scout)
|
|
3328
|
+
read as "LICENSE only in the published copy" — a false drift, because npm's dry-run never lists
|
|
3329
|
+
the file pnpm adds. That npm listing survives only behind a test seam (`publishNpmPackRunner`,
|
|
3330
|
+
parsed by core's pure `parseNpmPackInventory`, source `'npm-pack'`) — the seam path and the
|
|
3331
|
+
production path are NOT the same code, and that is named here rather than implied. Packed ONCE
|
|
3332
|
+
per package directory per `dz publish` RUN (not re-run per dependent package that happens to
|
|
3333
|
+
share the sibling); the scratch dir is removed after the drift loop. With no provider injected
|
|
3334
|
+
(an unusual, degraded mode), both sides fall back to the same `dist`/`files`/`bin` approximation
|
|
3335
|
+
— cruder, but still symmetric by construction. Both
|
|
3336
|
+
inventories are hashed together with a normalized `package.json`
|
|
3337
|
+
(version/gitHead/`_*`/`imports`/`browser`/`sideEffects`/`man` compared, not just entry points). A
|
|
3338
|
+
mismatch blocks; anything this gate cannot build — a fetch that fails (offline, 404), a
|
|
3339
|
+
failed `pnpm pack` (or a shapeless seam listing) on the workspace side, an unreadable/invalid
|
|
3340
|
+
`package.json` on either side, or a `workspace:`-spec'd name it does not recognize — is
|
|
3341
|
+
`unavailable` and blocks too; `unavailable` is never silently treated as a pass, and NEVER goes
|
|
3342
|
+
unlogged (an `unavailable` outcome writes an audit record whether or not `--allow-sibling-drift`
|
|
3343
|
+
was used). `--include-drifted` auto-extends the batch and RE-CHECKS the expanded batch's own new
|
|
3344
|
+
edges until nothing new drifts (a fixed point over transitive drift, capped at the package count)
|
|
3345
|
+
— a folded-in sibling that itself depends on a drifted sibling is not missed.
|
|
3247
3346
|
2. **Packed-install smoke.** The batch's `.tgz` files are installed TOGETHER into a clean directory
|
|
3248
3347
|
(siblings outside the batch resolve from the registry — exactly like a fresh user), then every
|
|
3249
3348
|
declared `bin` — including one whose target file turns out NOT to exist after the install,
|
|
@@ -3251,11 +3350,31 @@ earlier one failed):
|
|
|
3251
3350
|
n/a — runs `--version` and must exit 0 with non-empty stdout. On a **live** publish this gate
|
|
3252
3351
|
packs each package's tarball exactly ONCE, right after its own version bump — the SAME bytes are
|
|
3253
3352
|
then smoke-tested and handed to `npm publish <tgz>`; their sha256 is printed
|
|
3254
|
-
(`tarball <pkg>@<ver> sha256:<hex>`)
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3353
|
+
(`tarball <pkg>@<ver> sha256:<hex>`). `dz release --dry-run` plans the same steps inside its
|
|
3354
|
+
`smoke` gate (`smoke:packed-install:*`) and `dz release`'s own execution judges the `--version`
|
|
3355
|
+
step through the identical rule — the two doors apply one rule, for real, not only on paper.
|
|
3356
|
+
|
|
3357
|
+
Both gates write to the SAME append-only, hash-chained `.dz/guard-audit.jsonl` `dz guard` reads —
|
|
3358
|
+
**exactly ONE JSONL record per package per rule per run**, never one record per SIBLING a package
|
|
3359
|
+
happens to depend on: every sibling's outcome for a package (same/drift/unavailable, overridden or
|
|
3360
|
+
not) is aggregated first, then written once, with a detail that names every sibling and its status
|
|
3361
|
+
and a verdict of `block` if any sibling stands blocked, else `warn` if the only issues were resolved
|
|
3362
|
+
via `--allow-sibling-drift`, else `pass` (a package with no external sibling to check still gets a
|
|
3363
|
+
`pass — no external siblings` record, so the rule is never silently absent for a package). Each
|
|
3364
|
+
note/violation names `<rule>: <pkg>@<ver> sha256:<hex> — <detail>`, with `sha256:n/a` stated
|
|
3365
|
+
explicitly whenever the verdict is a preview that ran before anything was packed. The write itself is
|
|
3366
|
+
durable: `openSync` append → a LOOP of `writeSync` calls until every byte of the record is confirmed
|
|
3367
|
+
written (checking the returned length on every call — a short write is resumed, not silently
|
|
3368
|
+
accepted) → `fsyncSync(fd)` → `closeSync`. Two FURTHER fsyncs are each REQUIRED, not best-effort, and
|
|
3369
|
+
each failure alone is enough to report the whole write as not logged: an `fsyncSync` of `.dz` itself
|
|
3370
|
+
when THIS call is the one that created the audit file (so its new directory entry survives a crash),
|
|
3371
|
+
and an `fsyncSync` of `.dz`'s PARENT when THIS call is the one that created `.dz` (so `.dz`'s own
|
|
3372
|
+
directory entry survives a crash). Every printed verdict line says so: `(logged)` appears only after
|
|
3373
|
+
every required fsync actually succeeded, and a failed write — the file's own fsync, either directory
|
|
3374
|
+
fsync, or a writer that makes no progress — prints `(audit NOT logged: <reason>)` instead, never a
|
|
3375
|
+
false `(logged)`. The signature gate got the matching honesty fix: a `.dz-manifest.json` that PARSES
|
|
3376
|
+
but is not a JSON object (`null`, an array, a bare string) is reported `unavailable` — "the manifest is
|
|
3377
|
+
malformed", not "the pack carries no signature", because the fix for each is different.
|
|
3259
3378
|
|
|
3260
3379
|
```bash
|
|
3261
3380
|
dz publish --filter harness-cli # ✓ sibling drift: none / ✓ packed install smoke, or BLOCKED with a fix-it command
|
|
@@ -3272,7 +3391,7 @@ dz publish: BLOCKED harness-cli — sibling drift: @dzhechkov/memory@0.2.20 on t
|
|
|
3272
3391
|
dz publish: refusing to publish (1 sibling-drift violation(s))
|
|
3273
3392
|
|
|
3274
3393
|
$ dz publish --filter harness-cli --yes
|
|
3275
|
-
dz publish: tarball @dzhechkov/harness-cli@0.8.
|
|
3394
|
+
dz publish: tarball @dzhechkov/harness-cli@0.8.26 sha256:9f2c…e10a
|
|
3276
3395
|
dz publish: ✓ packed install smoke
|
|
3277
3396
|
✓ @dzhechkov/harness-cli 1.0.0 → 1.0.1 published (confirmed by registry after 1 probes)
|
|
3278
3397
|
sha256:9f2c…e10a
|
|
@@ -3582,7 +3701,12 @@ Every run also prints where the backend came from, e.g. `memory backend: agentdb
|
|
|
3582
3701
|
`dz doctor` cross-checks the two truths too: a new `memory hooks match config` row goes red when
|
|
3583
3702
|
`.dz/config.json`'s `memory.backend` and the ACTUAL `SessionStart`/`SessionEnd`/`PreCompact` hooks in
|
|
3584
3703
|
`.claude/settings.json` disagree in either direction, naming the exact fix (`run: dz setup --target
|
|
3585
|
-
claude-code --memory agentdb`, or the jsonl equivalent).
|
|
3704
|
+
claude-code --memory agentdb`, or the jsonl equivalent). When the two truths agree, the row prints an
|
|
3705
|
+
explicit `[OK]` naming the backend and the three events (e.g. `memory.backend=agentdb —
|
|
3706
|
+
SessionStart/SessionEnd/PreCompact all invoke .dz/agentdb-writer.mjs`) — it stays silent only when
|
|
3707
|
+
there is no `.dz/config.json` at all (never ran `dz setup`); a `.claude/settings.json` that exists
|
|
3708
|
+
but cannot be parsed or inspected (invalid JSON, or a malformed `hooks` shape) is reported red as
|
|
3709
|
+
"cannot be compared", never as a green receipt.
|
|
3586
3710
|
|
|
3587
3711
|
### The apply leg — `dz setup --memory agentdb` installs the whole loop, not two of three
|
|
3588
3712
|
|
|
@@ -3592,7 +3716,7 @@ Self-learning is a three-leg loop: **collect** (session hooks write into the sto
|
|
|
3592
3716
|
Before this feature `dz setup` shipped the first two legs only — the apply leg's files existed
|
|
3593
3717
|
solely in this repo's own `.claude/helpers/`, so every OTHER project that ran `dz setup --memory
|
|
3594
3718
|
agentdb` got collection and ranking, but never automatic recall (MEASURED: a clean install wrote no
|
|
3595
|
-
`UserPromptSubmit` entry at all, on 0.8.10 and 0.8.
|
|
3719
|
+
`UserPromptSubmit` entry at all, on 0.8.10 and 0.8.26 alike, with or without `--memory agentdb`).
|
|
3596
3720
|
|
|
3597
3721
|
`dz setup --target claude-code --memory agentdb` now installs all three, additively (a repeat run
|
|
3598
3722
|
changes nothing; a foreign hook you wrote yourself is left exactly where it is):
|
|
@@ -3601,7 +3725,7 @@ changes nothing; a foreign hook you wrote yourself is left exactly where it is):
|
|
|
3601
3725
|
|---|---|---|
|
|
3602
3726
|
| `recall-hook.cjs` | `.claude/helpers/` | `UserPromptSubmit` hook — asks the embed daemon for relevant lessons, injects them as prompt context, silent when nothing clears the relevance floor |
|
|
3603
3727
|
| `dz-embed-daemon.mjs` | `.claude/helpers/` | Resident embedding daemon behind a unix socket — keeps the ~1.5 s model-load cost off every single prompt |
|
|
3604
|
-
| `UserPromptSubmit` entry | `.claude/settings.json` | Runs the recall hook (`node ".../recall-hook.cjs"
|
|
3728
|
+
| `UserPromptSubmit` entry | `.claude/settings.json` | Runs the recall hook (`node ".../recall-hook.cjs" \|\| true` — a broken hook never blocks a prompt; stderr is NOT redirected — see "The leg never fails silently" below) |
|
|
3605
3729
|
| `SessionStart` entry | `.claude/settings.json` | Spawns the embed daemon detached (`sh -c 'nohup node ".../dz-embed-daemon.mjs" ... & exit 0'`) |
|
|
3606
3730
|
|
|
3607
3731
|
The hook resolves your harness-core installation by an ABSOLUTE PATH baked in at `dz setup` time
|
|
@@ -3610,6 +3734,38 @@ The hook resolves your harness-core installation by an ABSOLUTE PATH baked in at
|
|
|
3610
3734
|
global install elsewhere). Project-local candidates (`node_modules/`, a monorepo checkout) stay as
|
|
3611
3735
|
fallbacks after it.
|
|
3612
3736
|
|
|
3737
|
+
**Installed at `$HOME` (a common single-machine layout)? The leg now finds ITSELF, not whatever
|
|
3738
|
+
project the current session happens to be in.** Both generated files used to resolve their store
|
|
3739
|
+
from `CLAUDE_PROJECT_DIR || cwd()` — the SESSION's project. A user-level install
|
|
3740
|
+
(`dz setup --target claude-code --memory agentdb --project $HOME`, expecting the leg everywhere)
|
|
3741
|
+
silently looked up a DIFFERENT project's `.dz/` from every other session, and the `UserPromptSubmit`
|
|
3742
|
+
command itself broke down to `Cannot find module` (swallowed silently) whenever `project === $HOME`
|
|
3743
|
+
and a session's own `CLAUDE_PROJECT_DIR` pointed elsewhere (issue #2, MEASURED on 0.8.26). Fixed: the
|
|
3744
|
+
hook and daemon now resolve their own INSTALL location first (`path.resolve(__dirname, '..', '..')`
|
|
3745
|
+
for the hook — the precedent already used by the destructive-guard hook), and `dz setup` writes the
|
|
3746
|
+
`UserPromptSubmit`/`SessionStart` commands as an ABSOLUTE path to that install root instead of the
|
|
3747
|
+
old `${CLAUDE_PROJECT_DIR:-.}`-relative form. A re-`dz setup` over a pre-feature relative entry
|
|
3748
|
+
upgrades it in place — never a second entry. The one-line reproducer:
|
|
3749
|
+
|
|
3750
|
+
```bash
|
|
3751
|
+
# temp $HOME layout, matching the owner's real one
|
|
3752
|
+
export HOME=$(mktemp -d) && mkdir -p "$HOME/other-checkout"
|
|
3753
|
+
dz setup --target claude-code --memory agentdb --project "$HOME"
|
|
3754
|
+
dz teach "install-root beacon lesson" --project "$HOME"
|
|
3755
|
+
|
|
3756
|
+
# invoke the RECORDED command from a DIFFERENT checkout, CLAUDE_PROJECT_DIR pointing at it too —
|
|
3757
|
+
# the beacon still reaches additionalContext, because the hook found ITS OWN install, not $CLAUDE_PROJECT_DIR
|
|
3758
|
+
( cd "$HOME/other-checkout" && CLAUDE_PROJECT_DIR="$HOME/other-checkout" \
|
|
3759
|
+
node "$HOME/.claude/helpers/recall-hook.cjs" <<< '{"prompt":"install-root beacon lesson"}' )
|
|
3760
|
+
# → one line of JSON, hookSpecificOutput.additionalContext containing the beacon
|
|
3761
|
+
```
|
|
3762
|
+
|
|
3763
|
+
One named limit: from a foreign project's session the hook now injects the INSTALL ROOT's lessons,
|
|
3764
|
+
not the session project's — that is the requested one-store-per-install behavior, not a bug; a
|
|
3765
|
+
per-project store layered on top of a user-level one is a separate feature. The Codex host's own hook
|
|
3766
|
+
resolves its root from `payload.cwd`/`PWD`/`cwd()` — the SAME class of weakness — and is not fixed by
|
|
3767
|
+
this change; it is named in the harness-core README and the feature's own backlog entry.
|
|
3768
|
+
|
|
3613
3769
|
**On the jsonl backend** (no `--memory agentdb`) the step reports `skipped` with the reason named —
|
|
3614
3770
|
the embed daemon needs agentdb's transitive `@huggingface/transformers`/`@xenova/transformers`
|
|
3615
3771
|
dependency, which jsonl mode never installs. This is an honest boundary, not a missing feature: run
|
|
@@ -3677,18 +3833,128 @@ dz teach "acceptance check lesson" --allow-cold-start # → "↳ mirrored to v
|
|
|
3677
3833
|
# 5. The embed daemon comes up (spawned by SessionStart; give it up to ~20s to load the model).
|
|
3678
3834
|
timeout 20 sh -c 'until [ -S .dz/embed.sock ]; do sleep 1; done' && echo "daemon socket is up"
|
|
3679
3835
|
|
|
3680
|
-
# 6. The recall hook actually injects context for a relevant prompt, and
|
|
3681
|
-
# that clears no relevance floor — never a hang, never a stray print on stdout.
|
|
3836
|
+
# 6. The recall hook actually injects context for a relevant prompt, and STAYS SILENT ON STDOUT for
|
|
3837
|
+
# one that clears no relevance floor — never a hang, never a stray print on stdout. stderr now
|
|
3838
|
+
# NAMES the no-hits case (apply-leg-never-silent, FR-1) instead of staying blank.
|
|
3682
3839
|
echo '{"prompt":"acceptance check lesson"}' | node .claude/helpers/recall-hook.cjs # → one line of JSON, hookSpecificOutput.additionalContext non-empty
|
|
3683
|
-
echo '{"prompt":"unrelated weather report"}' | node .claude/helpers/recall-hook.cjs # → empty stdout, exit 0
|
|
3840
|
+
echo '{"prompt":"unrelated weather report"}' | node .claude/helpers/recall-hook.cjs # → empty stdout, exit 0, stderr: [dz-recall] skipped reason=no-hits root=…
|
|
3684
3841
|
|
|
3685
|
-
# 7. Doctor confirms the leg is
|
|
3842
|
+
# 7. Doctor confirms the leg is ALIVE AND ACTUALLY INJECTING, not merely installed.
|
|
3686
3843
|
dz doctor # → "apply-leg alive (embed daemon): embed socket present at <path> — recall injection can run"
|
|
3844
|
+
# → "apply-leg injects (live probe): live probe injected its beacon lesson in <N>ms"
|
|
3687
3845
|
```
|
|
3688
3846
|
|
|
3689
3847
|
Every step above is a REPRODUCER, not a claim: run it on a scratch project before trusting a `dz
|
|
3690
3848
|
setup --memory agentdb` install on a new machine.
|
|
3691
3849
|
|
|
3850
|
+
#### One recall engine for the hook and the CLI (`hook-recall-hybrid-parity`, ADR-001)
|
|
3851
|
+
|
|
3852
|
+
Before this feature the per-prompt hook and `dz recall` used TWO DIFFERENT engines: the hook's embed
|
|
3853
|
+
daemon ran its own brute-force cosine loop over the mirror; `dz recall` called core's `recallHybrid`
|
|
3854
|
+
(FTS5 lexical + semantic + reciprocal-rank-fusion). MEASURED (record 097ca040): 41.8% of taught
|
|
3855
|
+
lessons went unretrieved by either path over 48 days, and an exact lexical match with cosine 0.39 was
|
|
3856
|
+
silently dropped by the hook's cosine floor — a lexical hit `dz recall` would have surfaced.
|
|
3857
|
+
|
|
3858
|
+
The daemon's `op: recall` now calls `recallHybrid` itself — the SAME engine, SAME store, SAME
|
|
3859
|
+
ranking `dz recall` uses — under a time budget (`HOOK_RECALL_BUDGET_MS`, default 500 ms, below the
|
|
3860
|
+
hook's own 800 ms socket timeout). Every reply now carries an honest `engine` field:
|
|
3861
|
+
|
|
3862
|
+
- `engine: 'hybrid'` — `recallHybrid` answered inside the budget; `hits[].score` is an RRF rank
|
|
3863
|
+
normalized into `[0,1]` (`score / (2 / (RRF_K + 1))`, clamped — RRF_K=60 mirrors `vector-tier.ts`'s
|
|
3864
|
+
own constant).
|
|
3865
|
+
- `engine: 'cosine-fallback'` — the budget was exceeded, `recallHybrid`'s engine errored, or no core
|
|
3866
|
+
module was resolvable; the daemon falls back to TODAY'S brute-force cosine (unchanged, byte for
|
|
3867
|
+
byte) and reports `reason` alongside (e.g. `"budget exceeded (500 ms)"`).
|
|
3868
|
+
|
|
3869
|
+
The hook prints `engine`/`reason` to **stderr only** (`[dz-recall] engine=… reason=…`) — it never
|
|
3870
|
+
rides into `additionalContext`, so the model reading the prompt never sees it. The relevance floor
|
|
3871
|
+
the hook applies also depends on `engine`: a `cosine-fallback` reply keeps today's cosine-calibrated
|
|
3872
|
+
`DEFAULT_RECALL_FLOORS` (`recall-hook-policy.ts`) untouched; a `hybrid` reply is judged against its
|
|
3873
|
+
OWN placeholder floor (`DZ_RECALL_HOOK_SCORE_FLOOR`, currently `0.01`, deliberately permissive) —
|
|
3874
|
+
calibrating that floor on the real 281-pattern store is tracked as a follow-up measurement, named as
|
|
3875
|
+
a `TODO(08, FR-5)` at the constant's definition site.
|
|
3876
|
+
|
|
3877
|
+
**Recall-mode `'hook'` never moves the bandit's exposure counters.** The daemon calls
|
|
3878
|
+
`recallHybrid(PROJECT, prompt, { limit, mode: 'hook', deferExposures: true })` and never calls the
|
|
3879
|
+
returned `commitExposures(...)` — so a per-prompt recall (which would otherwise fire on every single
|
|
3880
|
+
turn) never counts as a bandit "exposure" the way an interactive `dz recall` does. `mode: 'hook'`
|
|
3881
|
+
itself ranks identically to `'hybrid'` (no weight change); it exists purely so the daemon's intent is
|
|
3882
|
+
visible end to end, alongside `deferExposures`, which is the field that actually withholds the write.
|
|
3883
|
+
|
|
3884
|
+
**The vector engine is now cached per process** (`getOrOpenEngine`, `vector-tier.ts`, FR-3): a
|
|
3885
|
+
long-lived caller like the daemon no longer re-runs the native-dependency probe
|
|
3886
|
+
(`isPackageInstalled`/`probeNativeDep`) on every single request — the resolved engine is kept keyed
|
|
3887
|
+
by `(projectRoot, mtime of .dz/agentdb.db)` and re-resolved only when that mtime changes (a `dz
|
|
3888
|
+
teach`/`consolidate` landed between requests). A short-lived CLI invocation is unaffected — the cache
|
|
3889
|
+
just gets populated once and discarded with the process, byte-identical to before this feature.
|
|
3890
|
+
|
|
3891
|
+
**`dz doctor`'s "apply-leg alive (embed daemon)" row now names the live engine** (FR-6): when the
|
|
3892
|
+
socket is alive it sends one `op: recall` probe (bounded at 1000 ms — comfortably above the 500 ms
|
|
3893
|
+
production budget default, see the honest NFR-1 discussion below) and appends `(engine: hybrid)` or
|
|
3894
|
+
`(engine: cosine-fallback)` to the detail line. A non-live fixture (no daemon actually listening)
|
|
3895
|
+
fails the probe near-instantly and the line is unchanged from before this feature.
|
|
3896
|
+
|
|
3897
|
+
**Honest NFR-1 finding (measured, not assumed).** The target was p95 daemon-answer ≤ 500 ms over 100
|
|
3898
|
+
real prompts from `.dz/recall-usage.jsonl`. MEASURED on this machine against the REAL 743-pattern
|
|
3899
|
+
production store, under the documented DEFAULT `HOOK_RECALL_BUDGET_MS=500` (script + numbers in
|
|
3900
|
+
`features/hook-recall-hybrid-parity/07_code_changes/change_manifest.md`): every single reply fell
|
|
3901
|
+
back to `engine: 'cosine-fallback'` — `resolveAgentdbEmbedder` (agentdb-index.ts, `EmbeddingService`)
|
|
3902
|
+
re-initializes the transformers pipeline on EVERY call with no cross-call caching (MEASURED
|
|
3903
|
+
standalone: 2–3.6 s per call, no warm-up across 3 repeated calls in one process), so a cold semantic
|
|
3904
|
+
leg routinely costs well over the 500 ms budget — NFR-1 is **not met** by the current architecture on
|
|
3905
|
+
this machine. Caching that embedder lives one layer below this feature's touched files
|
|
3906
|
+
(`agentdb-index.ts`, out of `hook-recall-hybrid-parity`'s scope) and is named as follow-up work in the
|
|
3907
|
+
manifest, not silently left unmeasured.
|
|
3908
|
+
|
|
3909
|
+
### The leg never fails silently — `dz doctor`/`dz parity` prove injection, not presence (`apply-leg-never-silent`)
|
|
3910
|
+
|
|
3911
|
+
Issue #2's second half, MEASURED: the recall hook exited 0 with NO stderr on every early-return path
|
|
3912
|
+
— dead store, dead socket, unresolvable core module, an empty prompt, no relevant lesson — and `dz
|
|
3913
|
+
setup`'s own UserPromptSubmit command swallowed even a `Cannot find module` behind
|
|
3914
|
+
`2>/dev/null || true`. `dz doctor` printed three green checks (`apply-leg installed`,
|
|
3915
|
+
`apply-leg alive`, `memory hooks match config`) and `dz parity` printed
|
|
3916
|
+
`✓ Self-learning … via UserPromptSubmit hook (auto recall)` on a project where the leg injected
|
|
3917
|
+
nothing in every session but one — both instruments were reading FILE PRESENCE as proof of FUNCTION.
|
|
3918
|
+
|
|
3919
|
+
**The hook now names every silent exit, on stderr, before returning:**
|
|
3920
|
+
|
|
3921
|
+
```
|
|
3922
|
+
[dz-recall] skipped reason=<store-not-found|socket-absent|core-unavailable|empty-prompt|no-hits> root=<path> (<source>) session=<path>
|
|
3923
|
+
```
|
|
3924
|
+
|
|
3925
|
+
Exit code stays 0 — a broken hook must never fail a prompt. `dz setup`'s UserPromptSubmit command no
|
|
3926
|
+
longer redirects that stream (`node ".../recall-hook.cjs" || true` — `2>/dev/null` is gone,
|
|
3927
|
+
`|| true` stays). **This is not for Claude Code's own transcript**: MEASURED against the real
|
|
3928
|
+
`claude` binary's own hook-reference table (`UserPromptSubmit`'s entry), exit 0 there is documented
|
|
3929
|
+
as `stdout shown to Claude` — stderr is named nowhere for that exit code, so a "verbose mode" hope of
|
|
3930
|
+
seeing it in-session does not hold. It is for two readers who read a spawned child's stderr directly:
|
|
3931
|
+
`dz doctor`/`dz parity`'s own live probe below, and a human running the hook by hand from a terminal.
|
|
3932
|
+
|
|
3933
|
+
**`dz doctor` and `dz parity` now measure whether the leg actually injects, not whether its files
|
|
3934
|
+
exist.** Both call the SAME `probeApplyLeg(root)`: it spawns the REAL configured hook command (read
|
|
3935
|
+
back from `.claude/settings.json`, not reconstructed) from a TEMPORARY cwd with `CLAUDE_PROJECT_DIR`
|
|
3936
|
+
pointing at that same temp dir — the shape of a real session — asks it to recall a throwaway beacon
|
|
3937
|
+
lesson written into the store just for the call, and is green ONLY when the beacon's own token comes
|
|
3938
|
+
back inside `additionalContext`. The beacon is removed immediately after, success or failure —
|
|
3939
|
+
proven by a count-before == count-after test, not merely claimed.
|
|
3940
|
+
|
|
3941
|
+
```bash
|
|
3942
|
+
dz setup --target claude-code --memory agentdb # installs all three legs
|
|
3943
|
+
dz doctor # → "apply-leg injects (live probe): live probe injected its beacon lesson in 143ms — the recall hook actually finds this project's store"
|
|
3944
|
+
dz parity --target claude-code # → "✓ Self-learning: automatic apply-leg via UserPromptSubmit hook (auto recall)"
|
|
3945
|
+
|
|
3946
|
+
# now kill the daemon and re-check — files are still there, but the leg is silently dead:
|
|
3947
|
+
pkill -f dz-embed-daemon.mjs; rm -f .dz/embed.sock
|
|
3948
|
+
dz doctor # → "apply-leg injects (live probe): installed but silent: socket-absent (probed in 4ms)" — RED, with the reason
|
|
3949
|
+
dz parity --target claude-code # → "◐ Self-learning: automatic apply-leg via manual dz recall before a task — installed but silent: socket-absent"
|
|
3950
|
+
```
|
|
3951
|
+
|
|
3952
|
+
`dz doctor` and `dz parity` cannot disagree about WHY a leg is dead: both read the same `reason`
|
|
3953
|
+
string off the same `probeApplyLeg` result, the same discipline `applyLegReasonMessage` already
|
|
3954
|
+
enforces for `stale-version`/`unreadable`. `dz doctor`/`dz parity` pay one probe's worth of wall time
|
|
3955
|
+
(measured: well under 200 ms when the leg is dead; ~100-200 ms once warm when it is alive) whenever
|
|
3956
|
+
the leg is wired — named here, not hidden.
|
|
3957
|
+
|
|
3692
3958
|
### AgentDB self-learning algorithms
|
|
3693
3959
|
|
|
3694
3960
|
When using `--memory agentdb`, the following algorithms automatically tune search quality:
|
|
@@ -5124,8 +5390,38 @@ refusal as the honest answer.
|
|
|
5124
5390
|
|
|
5125
5391
|
## Status
|
|
5126
5392
|
|
|
5127
|
-
`harness-core v0.8.
|
|
5128
|
-
|
|
5393
|
+
`harness-core v0.8.35` · `harness-cli v0.8.26` · `memory v0.2.22` — **this release (night 14→15.09, 8 features, each
|
|
5394
|
+
cross-family reviewed by Codex): the recall hook resolves its store from the INSTALL root, never from the session's cwd,
|
|
5395
|
+
and is never silent; the skill walker follows symlinks, drops build junk and names everything it skipped; one-character
|
|
5396
|
+
recall terms are searchable; and the mutation gate stays honest under load.**
|
|
5397
|
+
(a) `apply-leg-install-root` + `apply-leg-never-silent` (issue #2): the `UserPromptSubmit` recall hook computes
|
|
5398
|
+
`INSTALL_ROOT` from its own location and `SESSION_ROOT` from `CLAUDE_PROJECT_DIR`, settings commands are absolute and
|
|
5399
|
+
POSIX-quoted, a dead leg prints `[dz-recall] skipped reason=… root=…`, and `dz doctor`/`dz parity` report the leg
|
|
5400
|
+
green ONLY after a live injection probe (`apply-leg injects (live probe)`).
|
|
5401
|
+
(b) `skills-walk-symlinks-and-junk`: `walkFiles` → `{files, skipped}` — a symlink resolves via `stat`, a cycle guard
|
|
5402
|
+
tracks the ANCESTOR chain (aliases both walk), a symlink escaping the skill directory is skipped and named, an
|
|
5403
|
+
escaping `SKILL.md` refuses the whole skill in EVERY public reader, junk (`__pycache__`, `node_modules`, `.git`,
|
|
5404
|
+
`__MACOSX`, `.pytest_cache`, `.mypy_cache`, `*.pyc/*.pyo/*.swp/*.swo`, `.#*`, `.DS_Store`, `Thumbs.db`) is a published
|
|
5405
|
+
contract, and `dz install` prints `skills: skipped N junk entr(y|ies)` only when N > 0.
|
|
5406
|
+
(c) `recall-short-terms` (memory 0.2.22): ONE shared `tokenize()` for both backends, no length floor, diacritics folded
|
|
5407
|
+
for Latin script only (one combining mark — exactly FTS5 `unicode61 remove_diacritics=1`, measured), `hasSearchableTerms`
|
|
5408
|
+
/ `noSearchableTermsReason` re-exported through core, and `dz recall ",,"` prints `reason: no-searchable-terms`.
|
|
5409
|
+
(d) `mutation-gate-baseline-honesty` + `--touched/--added-since`: the attribution regex reads vitest 3's pool label
|
|
5410
|
+
(`FAIL |serial| path`), `maxWorkers` lives in the registry (hub: 2) and is honoured by `--max-workers`, and a feature
|
|
5411
|
+
gates only its own entries. (e) `store-generation-counter`: the daemon's engine cache invalidates on a generation bump
|
|
5412
|
+
written under a named lock. (f) `sandbox-copy-remaining-sites` + `full-suite-flake-fixes-2`: measured budgets
|
|
5413
|
+
(≥ 2× p95 under load), spawn timeouts strictly below the test budget, `waitForSocketReady` for daemon tests.
|
|
5414
|
+
Previous release (v0.8.34 / v0.8.26): the recall hook and `dz recall` share ONE hybrid
|
|
5415
|
+
engine, the embedder is cached per process, and every publish-gate verdict is a durable per-package audit record.
|
|
5416
|
+
(0) Hook/CLI recall parity (`hook-recall-hybrid-parity`): the embed daemon answers `op: recall` through core's
|
|
5417
|
+
`recallHybrid` under a 500 ms budget (measured p95 100–190 ms on the real 743-pattern store, 99–100 of 100
|
|
5418
|
+
requests hybrid) with an honest cosine fallback labelled `engine`/`reason`, a warm-up at start, an in-flight cap,
|
|
5419
|
+
and an engine cache used ONLY by the hook (`dz recall` itself resolves fresh); `dz doctor` prints the engine.
|
|
5420
|
+
The agentdb embedder is cached per process (`agentdb-embedder-cache`: cold 2117 ms → warm 1 ms). Sibling-drift
|
|
5421
|
+
inventories come from `pnpm pack` — the live transport — so the LICENSE pnpm synthesises from the workspace root
|
|
5422
|
+
no longer reads as drift; every gate verdict is one fsync'd audit record per package with `sha256:<hex|n/a>`.
|
|
5423
|
+
Previous release (v0.8.33 / v0.8.26): `dz publish` refuses a broken pair, `dz setup`
|
|
5424
|
+
reads the memory backend from config, and the embed daemon says "ready" only with a socket that exists.
|
|
5129
5425
|
(1) Sibling-drift gate + packed install smoke: before any live `npm publish`, every `workspace:*` sibling on
|
|
5130
5426
|
the registry is compared with the workspace (dist/files/bin + the shipping fields of package.json); a
|
|
5131
5427
|
drifted sibling BLOCKS the batch (`add <sibling> to the batch or publish it first`), and each package that
|
package/dist/cli.d.ts
CHANGED
|
@@ -32,6 +32,13 @@ export type MutationGateRunner = (command: string, options: {
|
|
|
32
32
|
readonly timeoutMs: number;
|
|
33
33
|
readonly phase: 'baseline' | 'mutation' | 'rebaseline' | 'final-rebaseline';
|
|
34
34
|
readonly entryId?: string;
|
|
35
|
+
/**
|
|
36
|
+
* mutation-gate-baseline-honesty FR-3: the extra env vars the REAL executor would set on top of
|
|
37
|
+
* `process.env` for this run — currently just `VITEST_MAX_WORKERS`, set unconditionally
|
|
38
|
+
* regardless of whether the test command is recognised as vitest (a non-vitest command still
|
|
39
|
+
* gets the env var; only the command-string injection is vitest-gated).
|
|
40
|
+
*/
|
|
41
|
+
readonly env: Readonly<Record<string, string>>;
|
|
35
42
|
}) => MutationGateRunnerObservation;
|
|
36
43
|
/** Output sink + working directory — injectable so the CLI is testable. */
|
|
37
44
|
export interface CliIo {
|
|
@@ -139,6 +146,20 @@ export interface CliIo {
|
|
|
139
146
|
timeout?: number | undefined;
|
|
140
147
|
env?: NodeJS.ProcessEnv | undefined;
|
|
141
148
|
}) => string;
|
|
149
|
+
/**
|
|
150
|
+
* Test seam for `dz publish`'s gate-audit writer (feature `publish-gate-audit-durable`, FR-2):
|
|
151
|
+
* overrides the fs primitives `appendPublishGateAudit` uses for its durable append (production
|
|
152
|
+
* leaves it unset → the real `node:fs` functions). Lets a test make `fsyncSync` throw to prove
|
|
153
|
+
* `(audit NOT logged: …)` is printed and the write is reported as failed, without touching any
|
|
154
|
+
* other seam's filesystem.
|
|
155
|
+
*/
|
|
156
|
+
readonly publishGateAuditFsLayer?: PublishGateAuditFsLayer;
|
|
157
|
+
/**
|
|
158
|
+
* AM-5 (feature publish-gate-audit-durable): test seam for the sibling-drift gate's `npm pack
|
|
159
|
+
* --dry-run --json` call (production leaves it unset → real `execFileSync`). Takes the package
|
|
160
|
+
* dir, returns raw stdout, or throws to simulate a real `npm` failure without spawning anything.
|
|
161
|
+
*/
|
|
162
|
+
readonly publishNpmPackRunner?: (dir: string) => string;
|
|
142
163
|
/**
|
|
143
164
|
* Test seam for `dz install`: overrides the `npm install` subprocess (production leaves
|
|
144
165
|
* it unset → real `execSync`, stdio piped). A stub runner that pre-stages a fixture
|
|
@@ -246,6 +267,29 @@ export declare function codexHooksSummary(report: CodexHooksSyncReport, label?:
|
|
|
246
267
|
export declare function deliverCodexHooks(input: CodexHooksSyncInput, sync?: (options: Parameters<typeof runSyncCodexHooks>[0]) => CodexHooksSyncReport, label?: string): CodexHooksSummary & {
|
|
247
268
|
readonly report: CodexHooksSyncReport;
|
|
248
269
|
};
|
|
270
|
+
/**
|
|
271
|
+
* fs primitives `appendPublishGateAudit` needs for its durable write (FR-2), injectable so a test
|
|
272
|
+
* can make `fsyncSync` throw without touching the real filesystem underneath every OTHER seam this
|
|
273
|
+
* function shares with production. Left unset in production → the real `node:fs` functions above.
|
|
274
|
+
*/
|
|
275
|
+
interface PublishGateAuditFsLayer {
|
|
276
|
+
readonly existsSync: (path: string) => boolean;
|
|
277
|
+
readonly mkdirSync: (path: string, opts: {
|
|
278
|
+
recursive: boolean;
|
|
279
|
+
}) => void;
|
|
280
|
+
readonly openSync: (path: string, flags: number) => number;
|
|
281
|
+
/**
|
|
282
|
+
* AM-2 (Codex round-1 review, finding 2, high): `Buffer`, not `string` — a SHORT write must
|
|
283
|
+
* resume at the exact BYTE it stopped at, and a string-based API cannot express that safely once
|
|
284
|
+
* the data contains any multi-byte UTF-8 character (re-encoding a slice of an already-partial
|
|
285
|
+
* string can silently produce different bytes than the ones actually pending). The real
|
|
286
|
+
* `node:fs.writeSync` accepts a `Buffer` directly (no re-encoding), so this changes nothing about
|
|
287
|
+
* what production writes.
|
|
288
|
+
*/
|
|
289
|
+
readonly writeSync: (fd: number, data: Buffer) => number;
|
|
290
|
+
readonly fsyncSync: (fd: number) => void;
|
|
291
|
+
readonly closeSync: (fd: number) => void;
|
|
292
|
+
}
|
|
249
293
|
export declare function boundedMutationGateOutputTail(output: string): string | undefined;
|
|
250
294
|
/** Test seam for the chokepoint: NEW-C4's proof needs to call it with a hostile pid. */
|
|
251
295
|
export declare function __wfSignalChildTestSeam(child: unknown, signal: string, detached: boolean): boolean;
|