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
doctor.ts's API-key health check tested process.env.CLAUDE_CODE, but
Claude Code actually sets CLAUDECODE (no underscore) — the check could
never detect a real Claude Code session via that variable. Add the
correct CLAUDECODE check alongside the existing fallbacks (#3023).
getNeuralStoreStats() in neural-tools.ts always reads/writes
.claude-flow/neural/models.json but self-reported its source as
patterns.json — a separate file owned by the ReasoningBank store in
memory/intelligence.ts. Corrected the label and removed the unused
PATTERNS_FILE constant that caused the confusion (#3025).
Fixes#3023, Fixes#3025
* fix(security): compare candidate and prefix in the same canonical form (#3010)
`validate()` resolved the candidate with `fs.realpath` but the allowed
prefixes were only `path.resolve()`d, so the two sides were compared in
different forms and nothing under a prefix reached through a symlink could
ever match. On macOS that is every temp directory — `os.tmpdir()` is
`/var/folders/...` and `/var` is a symlink to `/private/var` — so
`ruflo proxy install` rejected the binary it had just signature-verified.
Linux is unaffected because `/tmp` is real, which is why CI stayed green.
Keep both forms of every prefix and compare against the one matching how
the candidate was resolved. Canonicalizing the prefixes outright would have
broken `validateSync()` / `isWithinAllowed()`, which are documented as
lexical-only, in the mirror direction.
When the candidate's leaf does not exist, canonicalize its longest existing
ancestor instead of falling back to the lexical path — otherwise
`allowNonExistent` (the default) would regress under a symlinked prefix.
That also closes a hole this exposed: `<allowed>/escape/not-written-yet`,
where `escape` symlinks outside the prefix, was previously accepted because
`realpath` threw ENOENT on the missing leaf.
Escapes stay blocked, and the regression tests construct the symlink
explicitly rather than relying on the macOS quirk, so they hold on every
platform. 3 of the 6 fail without this change.
Co-Authored-By: RuFlo <ruv@ruv.net>
* fix(security): keep root prefixes usable and leave strict results untouched
Three defects found by an adversarial review of the previous commit.
`allowNonExistent: false` was still canonicalizing the candidate on ENOENT.
The call already fails, so the only effect was rewriting the `resolvedPath`
and `matchedPrefix` reported back — a silent shape change for any caller
reading those off a rejected result. Canonicalize only when non-existent
paths are actually allowed.
A root prefix (`/`, `C:\`, a UNC share root) already ends in a separator, so
anchoring the boundary as `prefix + path.sep` searched for `//` and rejected
every descendant. Pre-existing at all three comparison sites — this change
only widened how a prefix can end up being a root, since one can now
canonicalize to it. Extracted `isWithinPrefix`, which keeps the boundary
anchored so `/srv/app-secrets` is still outside `/srv/app`.
The guarded symlink tests returned early instead of skipping, so they
reported as passed on a host that cannot create links, and the flag was
never reset between cases. They now skip properly and reset per case.
Also documents that prefixes are canonicalized once, at construction: the
allowlist keeps denoting what it named when configured, rather than handing
whoever can rewrite a link the ability to redirect it later.
Co-Authored-By: RuFlo <ruv@ruv.net>
---------
Co-authored-by: RuFlo <ruv@ruv.net>
PathValidator.validate() canonicalizes the candidate path through
fs.realpath (when resolveSymlinks is on, the default), but the constructor
only ever path.resolve()d the allowed prefixes — never realpath'd them. On
any platform where a prefix is itself reached through a symlink (e.g.
macOS os.tmpdir() under /var, itself a symlink to /private/var), the
realpath'd candidate can never match the non-realpath'd prefix, and every
path under that prefix — including the prefix's own contents — is
rejected as "outside allowed directories".
This broke `ruflo proxy install` on macOS: the download and Ed25519
signature verification both succeed, then the defense-in-depth path check
in proxy/install.ts fails on the extracted binary, which lives under a
workDir created via os.tmpdir().
Fix: realpath the prefixes at construction time too, with the same
ENOENT-tolerant fallback to path.resolve() that validate() already uses
for candidates (a prefix directory that doesn't exist yet degrades
gracefully instead of throwing).
* fix(agent): propagate explicit provider/model config into agent execution (#2962)
`providers configure` and `agent spawn --provider/--model` persisted user
intent but the execution path (callAnthropicMessages, executeAgentTask,
determineAgentModel) only ever consulted env vars and a 5-alias model list,
silently discarding both. A local Ollama/OpenRouter setup with explicit
config would either fail closed or fall back to whatever provider the
env vars happened to select.
- determineAgentModel(): treat any non-alias config.model string as an
explicit selection via the existing modelId fast-path, instead of
falling through to task-based routing / agent-type defaults.
- agent_spawn: forward config.provider to the stored (and returned) agent
record when it's an unambiguous explicit choice ('ollama'/'openrouter';
'anthropic' is excluded since the CLI silently defaults to it when
--provider isn't passed).
- callAnthropicMessages(): accept an optional provider param and consult
the persisted `agents.providers` config for baseUrl/apiKey/model when
env vars are absent. A self-hosted Ollama baseUrl no longer requires
the undocumented OLLAMA_API_KEY='local' sentinel.
- executeAgentTask(): forward agent.provider into the first dispatch call.
Precedence: explicit per-agent flag > env vars > persisted config >
key-presence inference (unchanged, last resort).
Co-Authored-By: RuFlo <ruv@ruv.net>
* fix(ci): update #2042 smoke to match the widened OpenRouter branch shape
The #2042 regression smoke statically matched the literal token sequence
`useOpenRouter && openrouterKey`. #2962 widened that condition to
`if (useOpenRouter) { const apiKey = openrouterKey || persistedOpenRouter?.apiKey; ... }`
so a persisted `agents.providers` config can supply the key when no env
var is set — the smoke's ordering guarantee (OpenRouter branch reachable
before the Anthropic-key early-return) still holds, only the literal
pattern needed updating.
Co-Authored-By: RuFlo <ruv@ruv.net>
* fix(intelligence): stamp contentFingerprint in consolidate()'s graph write too
consolidate() (the session-end path) rebuilt graph-state.json unconditionally
per the #2920 fix in v3.38.6, but its write never included the
contentFingerprint field that init()'s cache-hit gate checks — only init()'s
own write set it. So every init() immediately following a consolidate saw
nodeCount match but contentFingerprint undefined !== a real hash, missed the
cache, and did a full rebuild even with zero content changes. Fail direction
was safe (extra rebuilds, never stale data) so it didn't block the v3.38.6
release, but it meant the 60s graph cache never actually fired across a
consolidate/restart boundary.
* fix(memory): stop misreporting bridge brute-force search as HNSW-accelerated (#2922)
getHNSWStatus() reported `available: true` whenever the AgentDB v3 bridge
was loaded, on the theory that "AgentDB v3 is HNSW-equivalent" — but the
bridge's actual default search path is a full-table SELECT + brute-force
cosine similarity loop over every embedding row. It never touches
@ruvector/core or an HNSW index, regardless of whether ruvector-core is
installed and resolvable. Same issue in bridgeGetHNSWStatus (unconditional
available: true) and in three misleadingly-named functions that do no HNSW
work at all: bridgeSearchHNSW, bridgeAddToHNSW, bridgeGetHNSWStatus.
- getHNSWStatus(): available=false + new algorithm field
('hnsw' | 'brute-force-cosine') when the bridge is the active path,
instead of a blanket available=true that implied acceleration that
was never in the request path.
- memory-bridge.ts: bridgeGetHNSWStatus -> bridgeGetVectorSearchStatus
(adds algorithm: 'brute-force-cosine' to its return shape),
bridgeSearchHNSW -> bridgeSearchBruteForceCosine,
bridgeAddToHNSW -> bridgeAddEmbedding. No behavior change — call sites
updated in memory-initializer.ts, no other source callers existed.
- memory-tools.ts: the `backend: 'sql.js + HNSW'` field on 7 MCP tool
responses (memory_store, memory_retrieve, memory_search, etc.) was a
hardcoded literal regardless of which path actually ran. Replaced with
describeBackend(), which reports the real algorithm via the fixed
getHNSWStatus() probe.
Actually wiring AgentDBBackend's HNSW into the bridge's default search
path (the issue's broader architectural question) is out of scope here —
this fixes the status/naming layer lying about what's already true, not
the search path itself.
consolidate() (the session-end path) rebuilt graph-state.json unconditionally
per the #2920 fix in v3.38.6, but its write never included the
contentFingerprint field that init()'s cache-hit gate checks — only init()'s
own write set it. So every init() immediately following a consolidate saw
nodeCount match but contentFingerprint undefined !== a real hash, missed the
cache, and did a full rebuild even with zero content changes. Fail direction
was safe (extra rebuilds, never stale data) so it didn't block the v3.38.6
release, but it meant the 60s graph cache never actually fired across a
consolidate/restart boundary.
initializeWorkerStates()'s restoration guard only checked
`originalConfig` (the constructor arg) before restoring
resourceThresholds from a saved daemon-state.json — a value explicitly
set via .claude-flow/config.json (e.g. minFreeMemoryPercent: 0, a
workaround for Darwin's os.freemem() undercounting reclaimable memory)
was not covered by that guard, so a stale daemon-state.json from
before the override existed kept winning on every restart. Same bug
class as #2661 (aiWorkersEnabled), which already documents the
"deliberately not restored from stale state" pattern this now follows
for resourceThresholds too — per-field, so an unrelated field with no
explicit source can still legitimately restore from saved state.
Also fixes a secondary diagnostic bug reported in the same issue:
readDaemonConfigFromFile() logged "Daemon config loaded from ..." via
this.log(), but runs from the constructor before this.config (and
therefore this.config.logDir) is assigned — the log() call threw
inside its own try/catch and the line never reached daemon.log,
making "is the config file even being read?" impossible to answer
from the log alone. The chosen path (or a YAML-parser-unavailable
warning) is now returned and logged once this.config exists.
Does not address this issue's primary Darwin os.freemem() memory-gate
bug — that needs vm_stat/memory_pressure-based measurement, a larger,
platform-specific change left for a follow-up.
* fix(intelligence): same-ID content edits were cached/dropped instead of refreshed (#2920)
Two related staleness bugs in the intelligence helper's graph/store
caching, both keyed on entry count instead of content:
- init()'s graph-cache-hit gate compared only graphState.nodeCount to
the deduped store length. Hand-editing a MEMORY.md section's body
while its heading (and therefore generated ID) stays the same
changes neither the count nor the ID set, so the stale cache kept
being served and ranked-context.json was never refreshed.
- consolidate()'s STORE_PATH persist gate
(`newEntries > 0 || store.length < preDedupCount`) only fired on
insight creation or a dedup-driven shrink. Any other in-memory
content change — e.g. assigning an id to a previously id-less entry
while building the graph — left entry count and the dedup outcome
unchanged, so the update was silently never written back to disk.
Fix: add a `storeFingerprint()` helper that folds every entry's own
content fingerprint into one aggregate value, used to (1) gate the
init() cache hit alongside nodeCount, and (2) make consolidate()
always persist the store — trivial cost given GRAPH_PATH and
RANKED_PATH are already rewritten unconditionally on every call.
Regenerated and re-signed helpers.manifest.json (ADR-174) for the
changed intelligence.cjs content via scripts/sign-helpers.mjs.
* fix(intelligence): re-sign helpers manifest with the correct auto-memory-hook.mjs hash
The previous commit's manifest captured a transient, incorrect hash for
auto-memory-hook.mjs — a file this change never touches — because a
concurrent Claude Code session briefly overwrote it with stale content
at the exact moment sign-helpers.mjs ran (the known helper-drift race
documented in CLAUDE.md). Re-signed with a before/after git-status
check bracketing the signing step to confirm no file changed underneath
it this time.
DualModeOrchestrator spawned both Claude and Codex workers with
stdio: ['pipe', 'pipe', 'pipe'] but never wrote to or closed proc.stdin.
Both platforms receive their prompt positionally/via flag, never over
stdin — claude -p ignores the open pipe, but codex exec blocks in
resolve_root_prompt waiting for stdin EOF that never arrives, hanging
every real Codex worker until the orchestrator's own multi-minute
timeout kills it.
Close stdin immediately after spawn for both platforms.
Regression test spawns a real child process standing in for codex exec
(waits for stdin EOF before exiting) and asserts the worker completes
well under a short configured timeout instead of failing via timeout.
The 20-char floor after the sk-/sk_live_/sk_test_ prefix missed
shorter-but-real keys, and the quote-adjacency anchor required a
quote literally next to the prefix, missing the common
`Authorization: "Bearer sk_live_..."` shape where the quote sits
next to "Bearer", not the key. Lookaround boundaries match the
prefix as a standalone token wherever it appears, with a lower
(but still non-trivial) length floor.
owner/repo were validated but never passed to gh, so `gh` silently
resolved the target repository from the cwd's git remote instead of
the caller-specified one. A caller supplying owner/repo expecting the
issue to land there had no guarantee it did, even on the "real"
gh-backed success path.
Ships #2989 (init --all-agents/--skip-claude/--only-claude/--cloud-mcp
were dead kebab-case flag reads) and #2991 (CLI hooks post-task
dropped the ADR-130 reinforced-by graph edge due to an unawaited
fire-and-forget write racing process.exit(0)).
Co-Authored-By: RuFlo <ruv@ruv.net>
`hooks post-task` (CLI) reliably dropped the "reinforced-by" graph
edge on every successful call. The write was a fire-and-forget async
IIFE the handler never awaited — invisible from the long-running MCP
server, but the CLI is a one-shot process that calls process.exit(0)
immediately after the command action resolves (#1552), killing the
still-pending write before its first tick, every time.
`--all-agents`, `--skip-claude`, `--only-claude`, and `--cloud-mcp` were
silent no-ops. Every long-flag write path in the parser normalizes
kebab-case to camelCase before storing (`--all-agents` -> `flags.allAgents`),
but `initClaudeAction` read the literal kebab-case keys, which the parser
never produces. Same bug class already fixed for `--no-global` (#2098A)
five lines above — never applied to these siblings.
Ships #2988: `hooks intelligence --train` now actually distills
(#2940) instead of printing a canned success message with no state
change behind it.
Co-Authored-By: RuFlo <ruv@ruv.net>
`--train` declared a `forceTraining` input on the `hooks_intelligence`
MCP tool but never read it — the handler always returned the same
read-only status dashboard, and the CLI action's --train branch was a
cosmetic 500ms sleep followed by an unconditional "Training cycle
completed". `lastAdaptation` (what `--status` reports as "Last
Training") never moved, so it could only ever age no matter how many
times --train ran.
Now `hooks_intelligence` calls the existing `distillLearning()` when
`forceTraining` is set and reports what actually happened: patterns
distilled, ran with nothing new to distill, or MCP unavailable — so a
real training cycle and a no-op are no longer the same output.