Three optimizations to the ruflo-side AgentBBS federation integration
(ADR-164), all in v3/@claude-flow/cli/src/mcp-tools/agentbbs-tools.ts.
1. PeerHello dedup (register). `federation_bbs_register` appended a fresh
PeerHello envelope on EVERY call even though the registry entry and nodeId
were already idempotent — so a node retrying register N times emitted N
near-identical hellos (observed live: one node published 8). Now a hello is
emitted only for a genuinely new registration, or as a heartbeat refresh once
FEDERATION_BBS_HELLO_WINDOW_SECS (default 60) has elapsed. Returns
`helloEmitted` so the caller can see whether one was written (a silent
suppression would otherwise be unverifiable).
2. Watch dedup. `federation_bbs_watch` now collapses duplicate envelopeIds
before applying the limit, so a relay replay or a double-append cannot
surface the same envelope twice to a watcher. Order preserved.
3. O(1) seq append. `nextSeq` re-parsed the whole room log on every
publish/register (O(n) as the log grows). It now scans only the last 64 KiB
to find the last seq, with a full-read fallback for the rare huge-line case.
Monotonic seq (ADR-164 §3.2.2) is preserved and tested past the tail window.
Decision helpers (`shouldEmitHello`, `dedupEnvelopes`, `nextSeq`,
`lastPeerHelloMs`) are exported and unit-tested deterministically — the tool
handlers early-return `{degraded:true}` when the optional `agentbbs` package is
absent, so the pure logic is tested directly rather than through the wrapper.
Tests: 7 new cases; full file 15 passed / 4 skipped (the 4 are agentbbs-present
happy-path, skipped when the optional dep is absent). tsc --noEmit clean on the
changed file. Structural + degraded contract unchanged.
Scope: this is the ruflo-side integration only. Claim leases, signed result
receipts, and node reputation are agentbbs/relay-authority concerns (external
pkg + x.ruv.io gateway) — a separate track.
Co-Authored-By: RuFlo <ruv@ruv.net>
* fix(security): make the fast-uri CVE fix actually reach shipped packages (#3222)
3.39.1 bumped fast-uri only in the ROOT package.json. Post-publish validation
caught that this never reached users: a fresh `npm install ruflo@3.39.1`
still resolved fast-uri@3.1.5, squarely inside the advisory range
(GHSA-5jgf-p345-68v8, high, CVSS 7.5, >=3.1.3 <3.1.6).
Root cause: v3/@claude-flow/cli/package.json carried its own hard pin
"fast-uri": "3.1.5" in dependencies, and that is the package that actually
ships. The root bump did not override it -- the #2112 lesson recorded in
CLAUDE.md, that root overrides do NOT propagate to the published ruflo
wrapper, applies to the CLI package too.
- v3/@claude-flow/cli: fast-uri 3.1.5 -> ^3.1.6 (the shipping dependency)
- ruflo: add "fast-uri": ">=3.1.6" override so the wrapper cannot regress
independently of the CLI
Verified by fresh-installing the published tarball rather than trusting the
lockfile: that is what surfaced the gap in the first place.
Refs #3222, #2112
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01JJnDvpYYk2rTvjkudQUoCk
* fix(ci): refresh v3 pnpm lockfile for the fast-uri bump
The 3.39.2 fast-uri change touched v3/@claude-flow/cli/package.json but only
the npm lockfile was regenerated. v3/ is a separate pnpm workspace and CI runs
pnpm install --frozen-lockfile with working-directory: v3, which rejected the
mismatch (ERR_PNPM_OUTDATED_LOCKFILE) and cascaded into 34 failing checks --
every one an install failure, not a real regression.
Diff is 6 lines, fast-uri only: specifier 3.1.5 -> ^3.1.6, resolved 3.1.7.
Verified pnpm install --frozen-lockfile now exits 0.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01JJnDvpYYk2rTvjkudQUoCk
* dream(swarm): #3242 fix AgentPool health checks that could never detect a dead agent
performHealthChecks() stamped lastHeartbeat = now on every "still healthy"
tick, and updateAgentHeartbeat() (the only other legitimate way to advance
that field) has zero callers anywhere in v3/ — so the check's own tick was
the sole thing keeping every pooled agent's heartbeat fresh, meaning
timeSinceLastActivity could never exceed one health-check interval and
replaceUnhealthyAgent() could never fire for any agent, in any pool, ever.
Fixes the self-stamp; matches the correct pattern already implemented in
UnifiedCoordinator.checkHeartbeats() in the same package. New deterministic
test file (fake timers): baseline fails 2/3 tests, candidate passes 3/3,
223/223 full package suite, tsc clean.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01TAzfKzdv9uRVjKt5TeBgkv
* dream(swarm): fold in adversarial-critique caveats, re-verify, ACCEPT-scoped
Independent critic (separate context) confirmed the fix and re-derived
everything itself (zero-callers grep, full suite, tsc, stash-isolated
baseline/candidate). Two disclosed, non-blocking caveats folded into the
gist: (1) @claude-flow/swarm is not currently a dependency of the shipped
@claude-flow/cli, so tonight's fix is real within the swarm package's own
UnifiedSwarmCoordinator/tests but has no reachable path through the actual
CLI today; (2) health still optimistically recovers +0.1/tick inside the
grace window even with zero real heartbeats (pre-existing, out-of-scope,
mirrors UnifiedSwarmCoordinator's own no-tick-recovery design). Also fixes
a cosmetic class-name typo in the source comment (UnifiedCoordinator ->
UnifiedSwarmCoordinator). Re-ran full suite (223/223) and tsc (clean) after
the comment edit; recomputed the witness stamp over final content.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01TAzfKzdv9uRVjKt5TeBgkv
* dream(swarm): update LEDGER.md for #3242/#3243
Records tonight's row plus verified GitHub state for the trailing nights
(MCP tools, not inferred) and the 2026-09-08 gap note (same
ledger-rows-only-reach-main-via-merge structural issue tracked since
2026-08-19).
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01TAzfKzdv9uRVjKt5TeBgkv
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(memory): stop reporting a gated native bridge as the active backend (#3228)
On Windows the #3024 kill-switch makes getRegistry() return null, so every
memory read/write silently falls back to the sql.js store (memory.db) instead
of the AgentDB corpus (agentdb-memory.db) the bridge would have opened. The
reporter measured the consequence: a canary store/retrieve round-trip SUCCEEDS
because both halves use the same unintended file, while an existing live-store
key returns found:false against 31,673 real rows.
The status surface hid the substitution. getHNSWStatus() gated its bridge
branch on whether the bridge MODULE was loaded -- which it is, even when the
registry is gated off -- so describeBackend() printed "sqlite (bridge,
brute-force cosine)" for a store the bridge never touched.
Gate on whether the bridge is permitted to be the active path rather than on
whether the module resolved. shouldDisableNativeBridge() is sync and a pure
function of platform + env, so it is a faithful discriminator and needs no
warm registry. describeBackend() now names the gate and its reason, so an
upgrade that changes which file receives writes says so.
This does not fix the opt-in allocation abort (#2948) or reroute the fallback
to the AgentDB corpus; both need Windows validation. It makes the silent case
loud, which is what the report asks for first.
Also bumps fast-uri 3.1.5 -> ^3.1.6 (resolves 3.1.7), clearing
GHSA-5jgf-p345-68v8 (high, host confusion via skipped IDN canonicalization)
and the ajv high that inherited it. Same change as #3222 by @aeonframework,
applied here because CI was never authorized to run on that PR.
Refs #3228, #3222, #2948, #3024
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01JJnDvpYYk2rTvjkudQUoCk
* chore(release): 3.39.0 -> 3.39.1
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01JJnDvpYYk2rTvjkudQUoCk
Ships the ADR-322C receipt number-domain fix (#3229 / #3235).
MINOR rather than patch: receipt output changes shape (fractional
candidatePolicy values become scale-12 decimal strings) and
policySchemaVersion moves v1 -> v2, so pre-fix receipts refuse promotion
with 'policy schema changed'. Not MAJOR because no CLI surface or MCP tool
signature changed.
Existing signed receipts stay valid as history and are deliberately NOT
migrated — re-encoding changes the content, hence the ID, hence invalidates
the signature. Re-establish a champion through the explicit reset path,
which already requires confirmation and a recorded reason.
Validated end to end against the built dist before publishing: a receipt
carrying the exact candidatePolicy from #3229 encodes to
{"alpha":"0.3","subjectWeight":1,"mmrLambda":"0.5","bodyWeight":"1.5",
"typePenaltyFactor":"0.5"}, contains zero fractional JSON numbers anywhere
in the payload, and verifies VALID.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01J2s8oQsAZjJtJ6inoYRFPj
`ruflo metaharness flywheel run` wrote receipts whose candidatePolicy
carried binary floats — {"alpha": 0.3, "mmrLambda": 0.5} — contradicting
the contract each receipt claims to satisfy:
witness-receipt-contract.md rule 2 "Fractional values are decimal
strings, never binary floats"
conformance-checklist A3 "Every fractional value is a
canonical decimal string"
ADR-322C L22 "Signed policy fractions use
schema-quantized decimal strings"
The spec's own example strings it: {"hnswEf": 128, "hybridWeight": "0.65"}.
Confirmed in the PUBLISHED 3.38.23 artifact, not just source:
dist/src/services/harness-flywheel.js:219 `candidatePolicy: candidate`.
Two releases shipped after the 3.38.21 report without fixing it.
Root cause was two-part, and the second half is what let it survive:
1. Producer: `candidate as unknown as Record<string, unknown>` — a DOUBLE
cast (a single one will not compile) that erased RetrievalConfig's
`number` fields and laundered floats into the receipt.
2. Verifier: `assertJsonValue` accepted any finite non--0 number. That was
its entire number rule, so ruflo verified its own non-conforming
receipts while autogenous's stricter verifier correctly rejected them
(ruvnet/autogenous#15). The JSON Schema pins `decimalString` on ten
named fields and cannot express it inside `candidatePolicy`, which is
opaque by design — so the rule lived only in prose.
Fix:
- `encodePolicyFractions` encodes non-integers to scale-12 decimal strings
and leaves integers as JSON numbers, matching the contract's example.
Recurses through nested objects and arrays.
- `policyCandidateId` encodes at the hashing boundary, so the content ID is
always over the canonical form. Encoding is idempotent, so
policyCandidateId(raw) === policyCandidateId(encoded) and `verify` can
still recompute the ID from the payload.
- `assertReceiptNumberDomain` enforces the rule at the RECEIPT boundary, on
produce (before signing) and on verify. This is the half that closes the
class: without it the next writer drifts again invisibly.
- The producer's type-erasing double cast is gone.
- policySchemaVersion v1 -> v2, because fixing the encoding changes the
candidate content ID. Pre-fix receipts now refuse promotion with an
ACCURATE 'policy schema changed' instead of a confusing 'stale baseline'
hash mismatch. Signed receipts are deliberately NOT migrated: re-encoding
changes the content, hence the ID, hence invalidates the signature — a
migration would mean minting new receipts claiming to be old ones. They
stay valid as history and unpromotable; the champion is re-established
through the existing confirm-and-reason reset path.
Scope note worth keeping: a first attempt put the rule inside
`canonicalizeJcs` and broke the proposer envelope and the promotion ledger,
which share that canonicalizer and which the contract does not govern. The
full suite caught it; a test now pins that the shared canonicalizer stays
permissive.
Mutation-verified: disabling the encoder fails 16 tests, disabling the
enforcement fails 1, and dropping the boundary encoding in
policyCandidateId fails 13.
Measured A/B on one installed+built tree — strictly better, nothing broken:
baseline 40 files / 83 tests failed, 2801 passed
with fix 39 files / 82 tests failed, 2811 passed
The remaining failures are @ruvector/*-wasm optional native deps absent on
this host, present identically on origin/main.
Claude-Session: https://claude.ai/code/session_01J2s8oQsAZjJtJ6inoYRFPj
Patch release covering the six commits landed since 3.38.21:
- #3221 gate EnhancedModelRouter's forwarded modelId on tier match
- #3169 wire embedding-cosine into SmartRetrieval's MMR step
- #3184 wire real cold-start measurement, drop the fabricated V2-vs-V3 benchmark
- #3177 bound two ReDoS patterns, stateless PII regexes, O(P) confidence pass
- #3204 fix two stale main-tree Test Suite failures
- #3203 add v3/@claude-flow/mcp to the root npm workspaces
3.38.22 is intentionally skipped: @claude-flow/cli's publish for that version
stuck in npm's "staged" state (published umbrellas, cli 404) and the registry
refused to republish over it even after unpublish, so all three packages moved
to .23 together to restore version lockstep.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01Ff2xRKvYrqXJhefvcapfE1
Merged after independent re-verification: 109/109 CI green, the earlier '168 failures' finding traced to an unbuilt-sibling-package local artifact (unrelated to changed files), and a subsequent CI flake confirmed unrelated via clean re-run. Overriding the PR's self-merge gate on explicit user (ruvnet) authorization.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013XaRQntabVwGT9SvnW8cKK
* dream(memory): wire embedding-cosine into SmartRetrieval's MMR step (evaluated, ACCEPT)
smart-retrieval.ts's MMR diversity re-ranking used token-Jaccard text
overlap as its "similarity to already-selected" term, even though
memory-initializer.ts's searchEntries() already computes real
ONNX-embedding cosine similarity for the primary relevance score and
discards it before returning. 2025-2026 production practice (LangChain's
reference maximal_marginal_relevance, Qdrant's native Mmr query shipped
Sept 2025, Weaviate's MMR reranker shipped in 1.37) universally uses
embedding-cosine for this term; token overlap misses low-token-overlap
paraphrases embeddings correctly flag as near-duplicates.
Threads the embedding through: searchEntries()'s two live compute sites
(RaBitQ rerank, brute-force SQL) now return it instead of discarding it;
the two CLI/MCP bridge mappers (memory-tools.ts, commands/memory.ts) pass
it through; SearchCandidate gains an optional `embedding` field;
mmrRerank uses cosine when both candidates have one, falling back to
token-Jaccard otherwise (existing callers/tests without embeddings are
unaffected). controller-registry.ts's separate toCands()/applyMMR path
had the same gap for a different reason (embedding available on
r.entry.embedding but never copied) — fixed the same way.
Evaluated via deterministic Vitest, zero LLM calls, $0 cost. New
discriminating test: a low-token-overlap paraphrase (near-1.0 cosine to
the seed) vs. a genuinely different topic (near-0 cosine). Baseline
(git stash-isolated): picks the paraphrase second (Jaccard can't tell
"low overlap because paraphrase" from "low overlap because unrelated").
Candidate: correctly picks the different topic second. Full
@claude-flow/memory suite: 460/461 passing identically with/without the
candidate (1 pre-existing chmod-based environmental failure, same class
documented since 2026-08-15). tsc --noEmit: zero new errors (verified
@claude-flow/cli's 455 pre-existing errors, from unbuilt workspace
packages, are identical with and without this diff).
Darwin: skipped, scope mismatch (correctness/similarity-metric fix with
a discriminating unit test, not a continuous parameter with a
gold-labeled corpus).
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01Qho5K4p5ACBt4yzCWjkBGe
* dream(memory): address review remediation — split CI, real benchmark, hardening
Response to ruvnet's REJECT review on #3169:
1. Split — dream-cycle-backlog-guard.yml moved out to #3205; this branch
now carries only the MMR fix, rebased onto main (which already
includes the merged #3203 CI fix).
2. Real benchmark (new src/mmr-benchmark.test.ts): 18-doc/6-topic labeled
corpus (1 canonical + 2 low-token-overlap paraphrases per topic), run
through the actual smartSearch/mmrRerank code, swept across mmrLambda
in {0.3, 0.5, 0.7, 0.9} rather than one favorable value. Honest
finding: at diversity-heavy lambda, candidate's recall/nDCG drop
sharply vs. baseline, because cosine correctly suppresses same-topic
near-duplicates that Jaccard fails to detect at all (and so
"accidentally" keeps full recall). At smart-retrieval.ts's own default
(lambda=0.7), recall@6/nDCG@6 are identical to baseline (1.000/0.777
both) while topic-diversity improves 0.472->0.667 and ground-truth
duplicate-rate drops 0.289->0.200. Reports latency (sub-ms both paths
at this corpus size) and a memory-overhead estimate.
3. Malformed/dimension-mismatch: found and fixed a real bug while writing
these cases. A NaN/Infinity-poisoned embedding component reached
cosineSimilarity unguarded; the poisoned NaN score meant `mmr > bestMmr`
was never true (NaN comparisons are always false in JS), which could
silently break MMR's selection loop and under-fill results below the
requested limit. Added isWellFormedEmbedding() so malformed/empty
embeddings fall back to token-Jaccard, same as absent/mismatched ones.
3 new cases (NaN/Infinity, empty array, explicit dimension-mismatch)
plus a test mirroring the two real CLI/MCP output whitelists
(memory-tools.ts:559-569, commands/memory.ts:633-639) to assert
`embedding` cannot appear in either shape.
4. Determinism: new test runs the same input through smartSearch 5 times
concurrently, asserts byte-identical result ordering every time,
including a genuine same-embedding tie.
Full @claude-flow/memory suite: 465/466 (1 pre-existing, unrelated
environmental failure). tsc --noEmit: zero errors.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01Qho5K4p5ACBt4yzCWjkBGe
---------
Co-authored-by: Claude <noreply@anthropic.com>
* docs(dream-cycle): backfill LEDGER rows for 2026-08-24..09-03
Ledger-append step silently failed again for 10 consecutive nights
(3rd occurrence of this failure class, first flagged 2026-08-19).
Verified via git ls-remote + GitHub MCP issue/PR search before
concluding this (all 10 nights ran to completion: real branch, issue,
and draft PR each) rather than inferring failure from a sparse table.
2026-08-20..08-23 is a separate, confirmed-genuine 4-night gap where
the pipeline did not run at all.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01YQUz3bkMpqgtLXgkYa1LFF
* dream(performance): wire real cold-start measurement, remove fabricated V2-vs-V3 benchmark (evaluated, ACCEPT-scoped)
cli-cold-start.bench.ts's own measureColdStart() (a real,
spawn-based process timer) was never called anywhere in the file.
Every reported number instead came from setTimeout() delays,
including a "V2 vs V3 Speedup: 5.00x" that was guaranteed by
construction (setTimeout(100) vs setTimeout(20), no code between
them) regardless of any real behavior.
- Export measureColdStart(), add runRealColdStartMeasurement() and
CLI_BIN_PATH; wire the real spawn-based measurement into the suite.
- Remove the fabricated V2-vs-V3 comparison outright (no V2 binary
remains to honestly compare against).
- Add a deterministic Vitest regression test (4 assertions); fails
against baseline (functions not exported, fabricated pattern still
present), passes against candidate — confirmed via git-stash
isolation. Full @claude-flow/performance suite: 99/99 passing.
tsc --noEmit: clean.
- Real measured number: node bin/cli.js --version, 5 runs, mean
54.65ms (min 51.94 / max 57.92) — well under the 500ms target.
Caveat disclosed in-code: --version is the one subcommand that
succeeds without a built dist/ in this worktree; this is not a
full-CLI cold-start measurement.
- Correct the two stale "Achieved" claims this fed in
ADR-STATUS-SUMMARY.md's Performance Targets table (CLI Startup,
and the already-known-false HNSW 150x-12,500x figure one row up).
Darwin: skipped, scope mismatch (dead-code-wiring fix, not a
tunable parameter). Flywheel: no signed bundle, deterministic
Vitest evidence retained instead. Follow-ups documented in tonight's
gist: same anti-pattern in agent-spawn/mcp-server-init/cli-warm-start
bench files (not touched, kept to one conceptual change); a Raft
election safety gap and a plugin supply-chain enforcement gap found
by tonight's scan roles (hive-mind, security) but out of scope for
the performance DEEP surface.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01YQUz3bkMpqgtLXgkYa1LFF
* docs(dream-cycle): append 2026-09-05 ledger row
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01YQUz3bkMpqgtLXgkYa1LFF
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(aidefence): bound two quadratic patterns, make PII regexes stateless, hoist the indicator count out of the per-match loop
Three findings from the 2026-09-04 upstream AIDefence review (ruvnet/midstream
PR #105, corpus of 55 bypass / 30 legitimate inputs), fixed here in the
vendored detector that the aidefence_scan MCP tool runs:
- ReDoS: `\[\[.*?\]\]|<<.*?>>|\{\{.*?\}\}` and `\bDAN\b.*\bmode\b|…` were
measured at 1.3–1.4 s and 0.65–1.0 s per 100 KB adversarial input. Bounded
to one line / 200 chars; the regression test requires a full detect() under
200 ms on 100 KB of `[[` and of `DAN …` repeats.
- Stateful PII: PII_PATTERNS carried /g (and /gi) but were only used with
.test(); a shared RegExp's lastIndex made detectPII alternate true/false
across calls on the same service instance. Flags dropped. The email class
`[A-Z|a-z]` typo (matched `|` as a TLD char) is corrected to `[A-Za-z]`.
- O(P²): calculateConfidence re-tested every pattern for every match. The
indicator count is now computed once per detect() and passed in; the boost
semantics are unchanged (test asserts the multi-indicator input scores at
least the single-indicator one).
Tests: 19 passed (16 existing + 3 regressions).
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013PKv3picsfzoQJDKLLW7Rt
* fix(aidefence): bound the email PII regex (RFC 5321 lengths) and pin it with a timing test
Review of this PR (2026-09-05 mission, agent pr-review-ruflo) found the
fix left a worse quadratic behind: the email PII pattern's unbounded
`[A-Za-z0-9._%+-]+@` local part took 3.8 s on 'a.' × 50 000, and
detect() calls detectPII on every scan — the same aidefence_scan path
this PR hardens. Bounded to RFC 5321 lengths ({1,64} local, {1,253}
domain, {2,24} TLD); a regression test runs three 100 KB dotted/dashed
inputs under the 200 ms budget and checks a real address and a
limit-length one are still found.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_019xHM4rAH4aaShb4DTr1n6s
* test(context-hook): assert the hash embedding width is EMBEDDING_DIM (384), not the legacy 768
tests/context-persistence-hook.test.mjs still expected createHashEmbedding
to return 768 values; the hook has defaulted to 384 (the ONNX
all-MiniLM-L6-v2 width, EMBEDDING_DIM) since February so hash-fallback
blobs stay comparable with ONNX vectors. The mismatch was hidden while
the Test Suite job died at ETARGET (fixed in #3203) and surfaced on the
first PR runs after it (#3177, #3184). Assert EMBEDDING_DIM and pin it
to 384; fix the stale "768-dim" docstring on the blob store.
node --test tests/context-persistence-hook.test.mjs: 66 pass, 0 fail.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_019xHM4rAH4aaShb4DTr1n6s
* fix(hooks): restore hook-handler's require.main guard and test exports dropped by the July helper sync
tests/hook-handler-runwithtimeout.test.cjs failed with "runWithTimeout is
not a function" on the first Test Suite runs after #3203 (#3177, #3184).
cb1e93e8d (2026-06-15) had guarded the hook dispatch behind
`require.main === module` and exported { runWithTimeout,
INTELLIGENCE_TIMEOUT_MS } for that test; the 2026-07-04 helper sync
(a5f86ad0a) replaced both copies of hook-handler.cjs with the packaged
build and dropped the guard and the exports, so require()-ing the helper
ran main() (stdin read + process.exit) and exposed nothing.
Both copies (repo dogfood + v3/@claude-flow/cli) restored identically;
direct invocation still prints usage and exits 0. The helpers manifest
is re-signed at publish (prepublishOnly), as the guard workflow expects.
node --test tests/hook-handler-runwithtimeout.test.cjs
tests/context-persistence-hook.test.mjs: all pass, 0 fail.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_019xHM4rAH4aaShb4DTr1n6s
* chore(helpers): re-sign helpers manifest after hook-handler.cjs test-surface restore
The packaged hook-handler.cjs changed in this branch (require.main guard +
runWithTimeout exports), so helpers.manifest.json still carried main's hash
and helper-signing.test.ts ("hashes match the actual shipped helper files")
failed in Test Suite (ubuntu-latest), run 33972961703. Re-signed with the
ruflo helpers key via RUFLO_HELPERS_SIGNING_SECRET; no other file changed.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_019xHM4rAH4aaShb4DTr1n6s
CI's test ratchet flagged agentic-flow-agent.test.ts as an unexpected new
failure (run 32091740244, same commit fa13ee4a that passed the day before).
result.duration is Date.now()-delta over a ~1ms setTimeout; under coarse CI
clock resolution the two samples can land in the same millisecond, making
duration legitimately 0. Relax the two affected assertions from
toBeGreaterThan(0) to toBeGreaterThanOrEqual(0).
Fixes#3155. `initializeMemoryDatabase()` passed its just-resolved
sql.js-facing `memory.db` path straight through to
`activateControllerRegistry()` -> `bridge.getControllerRegistry(dbPath)`.
That seeded the process-wide ControllerRegistry singleton with `memory.db`
as AgentDB's own native better-sqlite3 database, instead of the dedicated
`agentdb-memory.db` sibling `getAgentDbPath()` exists specifically to
provide (#2786).
Because this activation only runs on a database's first-ever init (an
already-initialized `.swarm/` skips it via the #1791.6 idempotent no-op
branch), a bridge started against a brand-new directory would activate
against `memory.db` and work fine for that process's lifetime. A
restarted bridge process, whose `.swarm/memory.db` already exists, would
skip that call entirely, and its first real bridge operation would
activate the registry against `agentdb-memory.db` instead (via the
`dbPath || getAgentDbPath()` fallback in memory-bridge.ts). That file
never received the earlier writes, so every read after a restart came
back `found:false` / an empty list, silently, with no error anywhere.
`activateControllerRegistry()` no longer forwards the sql.js dbPath to
the bridge at all — it calls `bridge.getControllerRegistry()` with no
argument, so both the init-time warm-up and every later bridge call
(bridgeStoreEntry, bridgeGetEntry, ...) resolve the same file via
`getAgentDbPath()`, regardless of whether this is a fresh directory or a
restart against an already-initialized one.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01MND33UDrtWPRH851k9w8M3
Closes#3091.
`intelligencePct` had exactly one assignment in the whole file, inside
`buildLocalFallback()`. Everywhere else it was only read. So the segment
showed either whatever the CLI returned or a literal 0 -- and since
`applyLocalOverlays()` repairs adrs/agentdb/tests/hooks/integration/security
from disk but never touched `system`, any CLI failure left the brain at 0%
sitting beside segments that were still live. Nothing distinguished the two,
so an absent measurement rendered exactly like a real one.
That is reachable from ordinary conditions, not just exotic ones. Both
candidates fail together on a normal setup: Claude Code's plugin marketplace
git-clones the repo with no install step, so `bin/cli.js` exists while
`@claude-flow/cli-core` does not (the case resolveCliBinCandidates() already
documents), and the npx backstop dies with `npm error Invalid Version:` in any
npm-workspaces root holding a member with no version field.
Three changes:
- `getLocalIntelligence()` joins the overlay set, deriving the percentage from
`.claude-flow/neural/patterns.json` (project, then home) with the formula the
sibling statusline.js already used. A CLI value greater than zero still wins.
- `countPatternsCached()` keys the parse on the store's mtime+size. The store
is routinely large -- 14.5MB / 1277 patterns on the machine this was
diagnosed on -- and the statusline re-renders every prompt, so parsing it
each time was not an option. Measured: 57.2ms cold, 0.2ms warm.
- Unknown now renders as `—` rather than `0%`, and the fallback constant is
`null` instead of `0`. A metric that silently pins to zero when a subprocess
dies is indistinguishable from a metric that is genuinely zero, which is what
made this read as a display bug rather than an outage.
This is the same failure `getLocalAgentDB()` already carries a comment about --
"the statusline showed Vectors 0 despite thousands of real vectors" -- in a
different segment.
Applied to both committed copies. The CLI package's copy is the generator's
single source of truth (#2679) and the one the drift guard byte-compares, so
editing it keeps generator output and artifact in step by construction; the
repo-root copy is CRLF and its endings are preserved.
Verified by execution, both copies:
1277-pattern store -> 100% (previously 0%)
no store anywhere -> — (previously 0%)
Claude-Session: https://claude.ai/code/session_016QSCkKnxDjqU49NVVpWMK5
A subagent I spawned earlier (intended only to relay a status message)
went out of scope and independently ran its own full fix/verify/publish
cycle in parallel with mine on this same checkout, racing my edits. Between
its publish and a set of stale-cached `claude-flow daemon` background
processes self-healing package.json mid-write (#3005), @claude-flow/cli
was published twice in a row (3.38.17, 3.38.18) with a broken
`@claude-flow/mcp` dependency pin, and 3.38.18 additionally shipped
entirely missing the `@claude-flow/memory` dependency. claude-flow and
ruflo were published at 3.38.18 too, inheriting the same defect.
3.38.19 is the verified-correct cut: @claude-flow/mcp repinned to
^3.0.0-alpha.9 (published, has the #2990 fixes), @claude-flow/memory
present at ^3.0.0-alpha.23 (has the #2977 fix). Deprecating 3.38.17/3.38.18
on npm as a follow-up; this commit does not touch the doc/metadata files
that raced independently of the version bump.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_018ZkgSrVRWNMcXmaFiMZ7Z8
3.38.17 was published by a concurrent session mid-race with a stale
package.json (the @claude-flow/mcp pin had been reverted to the broken
3.0.0-alpha.10 by a background helper-refresh process just before publish;
see #3005). npm forbids republishing over an existing version, so cutting
3.38.18 with the verified-correct pin instead of trying to fix 3.38.17.
Also discovered @claude-flow/memory is NOT in @claude-flow/cli's
bundleDependencies (unlike @claude-flow/mcp/codex/security/plugin-agent-federation)
-- it's resolved from the registry via a normal ^3.0.0-alpha.22 semver range,
and that exact version is what's currently published, without the #2977 fix
from 8a8500194. Bumping @claude-flow/memory to 3.0.0-alpha.23 and re-pinning
@claude-flow/cli to ^3.0.0-alpha.23 so the fix actually reaches users --
otherwise 3.38.18 would ship pointing at an unfixed memory dependency.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_018ZkgSrVRWNMcXmaFiMZ7Z8
Same content as b5b12a751, which only landed the two new proven-config files
-- the other six staged files dropped out of that commit, most likely a git
index race from a concurrent session against this same checkout. Re-applying
now; working tree already held the correct content.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_018ZkgSrVRWNMcXmaFiMZ7Z8
- README (root + ruflo/): the "no MCP server on plugin-only install" claim
was inaccurate for ruflo-core, which ships its own .mcp.json; correct the
comparison table and the plugin-install walkthrough to reflect the real
mcp__plugin_ruflo-core_ruflo__* tool names. Fix the `claude mcp add`
example to use the server name actually registered (claude-flow).
- catalog-manifest.json: regenerate (agents 164->165, current gitSha).
- helpers/.helpers-version, helpers.manifest.json, statusline.cjs,
.proven-config-version, proven-config.json: refresh to match current
release metadata (forward version bump, not a revert).
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_018ZkgSrVRWNMcXmaFiMZ7Z8
Fixes#2990's root cause at the publish layer: v3/@claude-flow/cli and the
claude-flow umbrella both pinned @claude-flow/mcp to 3.0.0-alpha.10, a
version that was never published (npm registry tops out at alpha.9). Since
@claude-flow/mcp is bundled (bundleDependencies) via a pnpm workspace link,
this didn't affect the monorepo build, but it broke plain `npm install`
against the published packages and left package.json/pnpm-lock.yaml
out of sync with what's actually resolvable. Repinned to ^3.0.0-alpha.9
(the real latest, which already carries the protocolVersion-string fix)
and regenerated v3/pnpm-lock.yaml accordingly.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_018ZkgSrVRWNMcXmaFiMZ7Z8
- #2992: pin windows-latest -> windows-2022 for the Build V3 matrix job.
windows-latest moved to VS2026, which node-gyp can't detect, breaking
native builds for hnswlib-node and better-sqlite3.
- #2977: remove dead agentdb import attempts in controller-registry.ts for
8 exports no installable agentdb range provides. Promote the working
tieredMemoryFallback/createConsolidationStub implementations to
first-class for hierarchicalMemory/memoryConsolidation; the other 6
controllers (semanticRouter, mutationGuard, attestationLog, gnnService,
rvfOptimizer, guardedVectorBackend) have no fallback and now return null
directly instead of attempting an import that never resolves. The
consolidation stub now reports source:'stub' and an explicit note so
agentdb_consolidate reads as "did not run" rather than "nothing to do".
- #2968: add a read-only doctor check (checkMemoryPersistenceDriver) that
reports whether the active SQLite driver is native better-sqlite3
(durable, WAL-capable) or the sql.js fallback (silently drops
wal_checkpoint writes), using the table-count signal from the issue
(~47 native vs ~10 fallback). Warns, never fails; does not touch install
behavior.
#2990 (MCP HTTP transport protocolVersion/tools-list/IPv6 bind) required
no code change — all three fixes are already present on main; the bug in
published ruflo/@claude-flow/cli only persists because no release has been
cut since they landed.
#3002 (ruvocal hidden-tab stream freeze) also required no code change —
already fixed on main.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_018ZkgSrVRWNMcXmaFiMZ7Z8
Cuts a release train off main after the 3 dream cycles landed unversioned:
- #3062 dream(swarm): bound MessageBus retry attempts, fix unreachable
message.failed (closes#3061)
- #3057 dream(memory): hybridSearch controller reachable via explicit
opt-in — was silently null-returning despite config flag
(closes#3056)
- #3049 dream(intelligence): discounted Thompson sampling for
model-router bandit — opt-in prior decay recovers faster after
workload shift (closes#3048)
No code changes in this commit — just the version bump across all three
publishable package.jsons.
Claude-Session: https://claude.ai/code/session_0118jMsYhwHD5dx2vStENsEB
* dream(intelligence): discounted Thompson sampling for model-router bandit
Adds an opt-in `priorDecay` config field (default 1 = disabled, fully
backward compatible) to ModelRouter's Beta-Bernoulli bandit. When enabled,
every bucket/model's alpha/beta decays geometrically once per
recordOutcome() call before that round's reward is added (arXiv
2305.10718 discounted Thompson sampling), so accumulated routing history
from a long-running persisted state file no longer permanently dominates
the posterior after a real-world model-quality shift.
Benchmark (n=30 paired trials, seeded PRNG, identical stream baseline vs
candidate): non-stationary regime-shift recovery 26.5 -> 21.9 rounds
(-17.6%, t=7.24); post-shift correct-routing rate +1.3pp (t=6.10);
stationary-workload invariant held (delta +0.02pp, not a regression).
Receipt: v3/@claude-flow/cli/benchmarks/results/prior-decay-receipt.json.
Mirrors the epsilon-decay pattern q-learning-router.ts already applies to
its own exploration rate -- an internal-consistency fix, not an imported
pattern. Evaluation and adversarial critique in progress; this commit is
the checkpointed, test-passing candidate + its baseline/candidate receipt.
Co-Authored-By: RuFlo <ruv@ruv.net>
* dream(intelligence): #3048 fix adversarial-critique findings, add gist + ledger
Independent adversarial critique found and this commit fixes: a paired-t
statistics bug (population vs sample stddev inflated every t-value by
n/(n-1)), an unguarded priorDecay input (NaN/negative could silently poison
persisted .swarm/model-router-state.json forever, bypassing sampleBeta's
own defensive fallback), and a single-bucket benchmark blind spot (now
covers low + med complexity buckets -- the med bucket shows no significant
effect, disclosed rather than hidden).
Also adds the research gist and backfills 3 missing ledger rows for
2026-08-14/15/16 (real PRs/issues existed for those nights but the ledger
table wasn't updated) plus tonight's own row.
Co-Authored-By: RuFlo <ruv@ruv.net>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* dream(memory): #3056 hybridSearch reachable via explicit opt-in without memoryService
The ADR-125 Phase 5 three-arm (dense+sparse+entity) RRF/MMR fusion
controller was fully built and tested but unreachable from any
production call site: its explicit-enable config override worked at
the isControllerEnabled() gate but the createController() factory
still unconditionally required a hand-built UnifiedMemoryService,
silently returning null otherwise. Falls back to the existing
this.backend when it type-guards as AgentDBAdapter-compatible,
reachable only via explicit controllers: { hybridSearch: true } --
default auto-enable behavior is unchanged.
New deterministic $0 benchmark quantifies the feature's actual
retrieval-quality effect for the first time (previously only
functional/unit tests existed): +0.267 aggregate recall@10, with a
disclosed, statistically significant regression on pure-paraphrase
queries where fusion noise dilutes an already-perfect dense signal.
Independent adversarial critic (fresh subagent): CONFIRMED-SAFE,
reproduced bit-for-bit. 75/75 tests passing in touched files, 458/459
in the full package (1 pre-existing environmental failure, unrelated).
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_016T1JRz8NBeGGmxQJVPReWa
* dream(memory): #3056 update ledger with 2026-08-14..18 nights + PR #3057
Reconstructs the v2 live-entries table from PR/issue history since each
night's own LEDGER.md edit lives only on its unmerged branch and never
reaches main (0% merge rate). Adds tonight's row (memory, hybridSearch
reachability fix, ACCEPT scoped, witness b28714fb...).
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_016T1JRz8NBeGGmxQJVPReWa
---------
Co-authored-by: Claude <noreply@anthropic.com>
* dream(swarm): recover 5 missing live ledger rows (2026-08-14..18)
The v2 live-entries section of docs/dream-cycle/LEDGER.md was never
appended to for 5 consecutive nights despite each night running to
completion (branch + draft PR + issue verified to exist for all 5 via
git ls-remote and GitHub search before concluding this, per the ledger's
own anti-inference rule). Backfilled directly from the PRs/issues.
Root cause of the append gap not diagnosed tonight; flagged in the
gist/issue as a candidate finding for a future automation/meta scan.
* dream(swarm): bound MessageBus retry attempts, fix unreachable message.failed
Given a subscriber whose callback always throws, MessageBus.handleDeliveryError()
incremented the queue entry's attempts counter but addToQueue() always
constructed a fresh entry with attempts:0 on re-queue, discarding it. Retries
were unbounded (96-98 invocations / 0 failures observed in 500ms, git-stash-
verified pre-fix) instead of capped at config.retryAttempts, and the
message.failed branch was unreachable.
Fix: addToQueue() takes an optional existingAttempts parameter; the retry
path in handleDeliveryError() now threads entry.attempts through instead of
resetting it. Fresh-message call sites are unaffected (default 0).
Candidate evaluated: exactly 3 invocations / 2 retries / 1 failure, stable
thereafter. 220/220 tests passing (2 new, 0 regressions). Independently
re-verified by an adversarial critic (fresh session, no authoring context):
CONFIRMED-WITH-CAVEATS — fix and scope match the claim; flagged (and now
disclosed in-code) a separate pre-existing, unfixed broadcast-retry queue-key
bug this diff does not touch.
Full research/evaluation writeup: docs/dream-cycle/dream-gist-2026-08-19.md
* dream(swarm): append 2026-08-19 ledger row
---------
Co-authored-by: Claude <noreply@anthropic.com>
Cuts a release train off main after the following PRs landed unversioned:
- #3060 fix(cli): honor MCP tool selection when listing (closes#3055)
- #3076 fix: resolve Claude Code launches on Windows (closes#3071)
- #3077 fix(hooks): persist real session-end state (closes#3063)
- #3044 dream(security): #3043 advisory scanner for untrusted settings.json
hooks/allow-rules — CVE-2025-59536-class defense-in-depth,
advisory-only (non-blocking), hardened against 6 concrete critic
bypasses.
No code changes in this commit — just the version bump across all three
publishable package.jsons.
Claude-Session: https://claude.ai/code/session_0118jMsYhwHD5dx2vStENsEB
* dream(security): #3043 advisory scanner for untrusted settings.json hooks/allow-rules carried forward by ruflo init/--upgrade
mergeSettingsForUpgrade() and writeSettings() both read a target
project's pre-existing .claude/settings.json and spread its hooks /
permissions.allow entries into the merged output verbatim — the same
trust shape as CVE-2025-59536 (settings.json hook payload achieving
command execution with no review step), reached via ruflo init's own
merge logic rather than Claude Code's loader. Neither of last night's
flagged CVEs (2025-59536, 2025-6514) applies to Ruflo's own hook
dispatch or OAuth client directly, but this distinct gap in the same
family was genuinely unaddressed.
Adds a static, advisory-only (non-blocking) scanner that inspects
hook commands and Bash allow-rules for known-dangerous patterns
before the merge and surfaces findings as CLI warnings. Merge/write
behavior itself is completely unchanged. Hardened after an
independent adversarial-critic pass found 6 concrete bypasses
(intermediate-pipe-stage downloads, two-step download+exec,
eval-wrapped base64, interpreter-mediated dangerous commands,
absolute-path-prefixed commands, whitespace-evasive allow rules) —
all fixed and pinned as a held-out regression set. Also fixes an
ANSI/control-character injection risk in the warning output itself,
self-identified during review.
87/87 relevant tests green (1 pre-existing environmental failure
elsewhere in the package, unrelated — unbuilt sibling package).
Dream Cycle 2026-08-16, security surface. Full research, evaluation
receipts, and adversarial critique: issue #3043 and
docs/dream-cycle/dream-gist-2026-08-16.md.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01NVdeUqt73Bv7vnPxYpZy7i
* docs(dream-cycle): update ledger — backfill 2026-08-14/2026-08-15, add 2026-08-16
2026-08-14 (swarm, REJECT) and 2026-08-15 (performance, REJECT) both had
fully-run pipelines whose live ledger rows never reached main (every
dream-cycle PR to date is unmerged, so branch-local ledger commits never
merge forward) — verified via git ls-remote / search_pull_requests before
backfilling, not assumed.
2026-08-16 (security, ACCEPT): advisory scanner for untrusted settings.json
hooks/allow-rules carried forward by ruflo init/--upgrade. Issue #3043,
PR #3044.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01NVdeUqt73Bv7vnPxYpZy7i
---------
Co-authored-by: Claude <noreply@anthropic.com>
Root cause: bridgeStoreEntry's post-write cache set stored only a partial
entry shape — { id, key, namespace, content, embedding } — with no `tags`,
no timestamps, no metadata, no access_count. The DB row itself had the
tags JSON-serialized correctly. But any subsequent bridgeGetEntry hit the
cache first and returned `tags: cached.tags || []` = `[]`, so
`memory_store({ tags })` → `memory_retrieve()` looked like it silently
dropped tags.
Fix: replace the partial write-through with cache invalidation. The next
read then re-fetches the row via the DB reader (which correctly parses
the tags JSON column) and repopulates the cache from the full shape.
Cleaner than expanding the write-through payload — future entry-shape
fields don't need to remember to update the cache-write signature.
Regression test verifies: store {tags:[a,b,c]} → get returns {tags:[a,b,c]}.
Proven to catch the bug: pre-fix the test fails (tags: [] returned);
post-fix it passes.
The same defect would have caused reads to see access_count stuck at 0
and drifted createdAt/updatedAt timestamps from the moment the cache
was first populated until eviction; cache invalidation fixes those too.
Release
- Bumps @claude-flow/cli, claude-flow, ruflo: 3.38.13 → 3.38.14 (PATCH).
Claude-Session: https://claude.ai/code/session_0118jMsYhwHD5dx2vStENsEB
Bug fixes
- #3045 statusline.cjs: getGitInfo() runs the 5-command git chain per render;
on large repos with concurrent Claude Code sessions this queued subprocesses
faster than they finished (reporter observed hundreds of orphan children +
load-avg in the hundreds). Added a per-cwd tmp file cache with 5s TTL —
dirty status still feels live, pileup is bounded.
- #3064 hooks post-task: the narrow ad-hoc regex /^[a-zA-Z0-9_-]+$/ silently
dropped every colon-namespaced plugin agent (ruflo-core:reviewer,
feature-dev:code-explorer, ...) — i.e. every Claude Code plugin agent. The
canonical validateIdentifier() upstream already allows ':' and '.', so the
redundant regex is removed. Regression test locks in 4 agent-shape cases,
proven to catch the bug: 2 pass / 2 fail on revert, 4 pass with fix.
- #3065 harness-gepa SKILL.md: unquoted colon in `description` broke YAML
parsing in `npx skills add`. Quoted the description; rephrased the bare
"(default:" to avoid the leading colon.
New plugin: ruflo-deepseek-harness
- plugins/ruflo-deepseek-harness/: sibling to ruflo-metaharness (ADR-150
shape). Two skills: `deepseek-chat` (non-reasoning) and `deepseek-reason`
(surfaces reasoning_content separately). Reads DEEPSEEK_API_KEY from env;
degrades gracefully (exit 0 with `{status: 'degraded', reason, hint}`)
when the key is missing or the API is unreachable. `--alert-on-error`
flag opts into hard exit 1 for CI gates. Smoke-tested locally.
Release
- Bump @claude-flow/cli, claude-flow, ruflo: 3.38.12 → 3.38.13 (PATCH:
bug fixes; the new plugin is scaffolding under plugins/ and not part of
the npm-published CLI packages).
Not fixed
- #3051 memory_store tags: current source's memory_store handler passes
tags straight through to storeEntry; a maintainer already verified live
round-trip works on fa13ee4ad. Reporter has not yet supplied the
ruflo/@claude-flow/cli version that hosted the affected MCP server,
so the affected release path is unknown. Left as-is pending that reply.
Claude-Session: https://claude.ai/code/session_0118jMsYhwHD5dx2vStENsEB
createFlywheelReceipt ran the bootstrap over full-precision score/delta
doubles while the payload stored them as scale-12 decimal strings, and
verifyFlywheelReceipt recomputes from those strings — the only values a
verifier ever has. Any input that does not terminate within twelve
decimals (e.g. a corpus mean of 0.7687083333333332, stored as
"0.768708333333") shifted relativeLift by one ULP on re-decode, so an
honest, correctly signed receipt failed its own verification with
"statistical decision does not recompute" — indistinguishable from
tampering.
Encode baselineScore, candidateScore, heldOutDeltas, and
frozenAnchorRegression first and feed the decoded encoded values into
computePromotionStatistics, making producer and verifier bit-identical
by construction. This implements the ADR-322C amendment in #3067: the
statistical decision is computed from the encoded values of record.
Regression test at the reproduction mean fails against the previous
producer and passes with the fix.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_012Jib2gQyJpqCoo2xYAbb4X
ADR-322C requires that unknown fields fail verification for a given schema
version. verifyFlywheelReceipt did not enforce it: a signature is valid
over whatever the producer canonicalized, so a producer-emitted field the
contract never defined verified cleanly. Enforcement lived only in
out-of-band JSON Schema validation, which is a step a consumer may or may
not run, while verifyFlywheelReceipt is what every consumer calls.
This matters now because rvm#35 and autogenous#10 are about to anchor
promotion receipts into a witness chain and verify them across repo
boundaries; a permissive verifier makes the anchor silently permissive too.
Closes the class, not just the instance. verifyFlywheelLedger had the same
weakness - a commit hashes consistently over whatever fields it carries, so
an undefined field chained cleanly - and now runs the same check.
Contract-open objects stay open, with a test pinning that: candidatePolicy
is owned by policySchemaVersion, gates is a caller-named term map, and
evidence.verification / evidence.canary carry evidence-specific payloads.
Errors read "unknown field: <path>" so a caller can tell an unrecognized
field from a tampered one, per the issue's triage requirement.
Both regression tests were confirmed to fail with the checks disabled and
pass with them enabled. Full flywheel suite: 37 passed (receipt 10,
transaction 11, proposer 6, sequential-evidence 10).
Strictness is versioned: a v1 verifier will reject a future receipt that
adds fields. That is correct fail-closed behavior for a security property,
and is now stated in ADR-322C's Update section (see #3069, PR #3067) so
consumers plan for it rather than discover it.
Refs #3068
Co-Authored-By: claude-flow <ruv@ruv.net>
Adds a `@claude-flow/watermark/web` ESM entry (wasm-pack `--target web`) so the
package works in browsers, Deno, and bundlers — not just Node. Instantiate once
with `await init()` (auto-fetches the wasm in a browser; accepts bytes/URL/
Response), then the same ergonomic API (Watermarker, detect, detectSelfSync,
detectExact) as the Node build.
- package.json: conditional exports (`.` = Node CJS/ESM, `./web` = browser ESM,
`./package.json` re-exported); web/ marked ESM via a nested package.json.
- build:wasm now builds both nodejs and web targets.
- Added test/smoke-web.mjs; `npm test` runs Node + web. Both verified, plus a
fresh dual-entry tarball install (node z=64.7, web z=64.7).
Bumps to 0.2.0 (new capability, backward-compatible). No removal tooling.
Claude-Session: https://claude.ai/code/session_01VYDa3Hah5VJLS2ceEuTLKz
Widens the ruflo-watermark WASM surface (src/wasm.rs) to select schemes by
name ("tournament" | "tournament_nd" | "gumbel") and adds the indel-robust
self-sync and exact-Gamma short-text detectors to the bindings.
Adds v3/@claude-flow/watermark: a Node (CommonJS) npm package wrapping the
wasm-pack-generated bindings behind an ergonomic API (Watermarker, detect,
detectSelfSync, detectExact), with a clean .d.ts, a committed 37KB .wasm so
`npm install` needs no Rust toolchain, a regenerate script (build:wasm), a
README, and a smoke test. Verified: all 3 schemes generate+detect, wrong-key
and null streams rejected, and the specialized detectors fire — both in-repo
and from a fresh tarball install.
No watermark-removal tooling is exposed.
Claude-Session: https://claude.ai/code/session_01VYDa3Hah5VJLS2ceEuTLKz