mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-14 14:01:28 +08:00
docs/chatgpt-plugin-reference
247 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
13880e3aa4 |
fix(memory,daemon): stop destroying a user index, honour autoStart:false, and finish the sibling disclosure (#3282)
Three from the current issue backlog, all confirmed in source before touching anything, and all with a test that fails without the fix. #3224 — curateIndex() rebuilt MEMORY.md from the bridge's own topic files and overwrote whatever was there. The #1556 guard only skips the write when NOTHING matched, but the bridge writes the first topic file itself, so after the first insight the guard stops applying and the rebuild replaces a hand-maintained index with a stub: 75 curated lines and 49 links down to 6 in the reported case. The bridge only knows its own topic files, so everything else looks like nothing to it. A curate may now grow or reorder the index and never shrink it; when the generated view is smaller it is written beside the file as MEMORY.generated.md and the bridge emits index:preserved rather than going quiet. #3278 — the autostart disable check read claude-flow.config.json with the key daemon.autostart, while init generates .claude/settings.json with claudeFlow.daemon.autoStart. A different file AND a different capital S, so the one setting a fresh project actually ships was never consulted. It is written false under the comment "Opt-in only — prevents unintended token consumption (#1427, #1330)", which the runtime then ignored. Both files and both spellings are now read: someone who wrote autostart false anywhere sensible meant it. #3228 (half) — 3.41.1 disclosed the sibling AgentDB store from `memory list` only. retrieve, search and an empty list kept returning a clean negative while the rows sat in the file this interface does not read; the reported Windows install has 31,673 rows in that other store answering found:false. A confident negative is worse than an error, because nothing prompts anyone to look further. That was a gap in my own fix, not a new defect. Verified the tests are worth having by reverting each fix and re-running: #3224 fails 1/2, #3278 fails 2/4, and all six pass again restored. Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67 |
||
|
|
e6f116b5c0 |
fix(memory): honour an explicit database path, and stop counting one store as if it were all of them (#3196) (#3271)
* fix(memory): honour an explicit database path, and stop counting one store as if it were all of them (#3196) Two writes, both reporting success, landing in two files neither interface could see whole. Reproduced on 3.41.0 in a clean directory: .swarm/memory.db CLI-written key 1, MCP-written key 0 .swarm/agentdb-memory.db CLI-written key 0, MCP-written key 1 The cause is a cache, not a path calculation. getRegistry() kept one global instance, so the first caller to touch the bridge decided the file for the whole process; every later caller's dbPath was accepted and then silently ignored, because the function returned the existing instance without ever comparing paths. `--path` and CLAUDE_FLOW_DB_PATH have always advertised that they select a database. Now they do: the cache is keyed by resolved path, so two paths in one process are two registries, and a relative path and its absolute form are one. The second half is worse than the split and is fixed separately. `memory list` printed "Showing 1 of 1 entries" — not a warning, not a partial result, a confident total describing one file as though it described the memory. A downstream integrator measured 51 rows in one store and 16,671 in its sibling, with 50 keys existing only in the smaller one. Silence would have been recoverable; a wrong total is not, because nothing prompts anyone to look further. The CLI now names the store it did not read, and how to read it. Deliberately NOT done: merging the two files. #2786 gave AgentDB its own plaintext file so native better-sqlite3 does not open an encrypted memory.db, where it fails with "file is not a database" and silently disables the learning system. Unifying the path would re-break encryption at rest. Both corpora stay, and both stay readable. The sibling count is read-only, opens nothing it cannot open read-only, and returns null when the file is missing or unreadable — an unreadable store is not evidence of an empty one, so the CLI stays silent rather than claim zero. Verified against the original reproduction: the MCP-written rows are now disclosed by name and count after the total. Three regression tests cover sibling identity, path resolution, and silence on an unreadable store. Reported-by: pacphi (pacphi/agentic-kit#213) Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67 * fix(memory): make the bridge test seam override every path, not two guessed keys Keying the registry cache by path broke #2968's fixture. That test installs a fake registry through __setMemoryBridgeRegistryForTests and then calls bridgeStoreEntry with its own temp dbPath, so an override installed under the two keys the seam could guess was never consulted: the call resolved a third path, missed the cache, and tried a real init instead of the fake. A test seam means 'use this registry', not 'use it if the caller passes the path I assumed'. It is now a separate override that getRegistry() returns first, kept deliberately outside the path cache so the two concerns cannot collide again. Verified: #2968 and the #3196 identity tests pass together, and the bridge dbPath, durability, purge and row-visibility suites are unchanged. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67 |
||
|
|
90bff10f15 |
feat(x-gateway): public and private swarm channels (ADR-386) (#3261)
Coordination on x.ruv.io was one flat topic: every member read every message,
and there was no way to keep a stream separate — or confidential — without
standing up another relay tenant.
Channels are one tag on the existing events, in two visibilities:
public c=pub:<name> plaintext, any relay member reads it
private c=prv:<16 hex> NIP-44 v2 ciphertext, k=enc hides the message type
The private channel key is generated client-side and cached at
~/.ruflo/channels.json (0600). Access is granted by sealing that key to a
member's pubkey over ECDH and publishing it as a ChannelGrant only they can
open. The gateway holds no channel keys and cannot decrypt — a hosted gateway
that encrypted on your behalf would be a custodian of every private channel on
the service, which is the thing worth not building.
Not hidden, and said so in the ADR: the relay sees that a channel exists, its
opaque id, who published and when. There is no revocation either; removing
someone means rotating the channel.
The channel tag is 'c', not the obvious 'h'. buzz-relay already implements
NIP-29-style server-side group membership on : an h-tagged event publishes
fine and the matching REQ comes back CLOSED "restricted: not a channel member",
so the author cannot read back their own message. Measured every other
single-letter tag against the live relay — c/d/g/l/m/r/x/y/z are all indexed and
unrestricted.
Surfaces: gateway channel_list / channel_sync / channel_publish (public only,
admin-gated) + ruv://swarm/channels; client x_federation_channel_{create,grant,
accept,publish,read,list} and 'ruflo federation channel --action ...'.
Verified: gateway 12/12, CLI 11/11, and a live two-key E2E on wss://relay.ruv.io
— grantee reads the plaintext, an outsider key cannot decrypt, the ciphertext
does not contain the secret, and the gateway's own channel_sync returns
encrypted:true. Deployed as ruflo-x-gateway-00008-s6n.
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
|
||
|
|
dbf450a927 |
feat(federation): ruflo CLI + MCP integration for x.ruv.io, and Seraphina (swarm queen) (#3256)
* feat(federation): ruflo CLI + MCP integration for x.ruv.io, and Seraphina (swarm queen)
Integrate the open swarm federation into ruflo itself, and add Seraphina —
a primary-coordinator / swarm-queen guidance tool in the style of the ruOS
assistant terminal (invoked from a terminal or any MCP client).
MCP tools (src/mcp-tools/x-federation-tools.ts), registered in the barrel and
the in-process registry so `callMCPTool` and the MCP server both see them:
x_federation_sync / roster / claims / registry (open reads)
x_federation_publish / invite_mint / admit (gateway-identity writes,
require RUFLO_X_ADMIN_TOKEN; fail closed with no network call)
Each description follows ADR-112 ("Use when … wrong because …").
Seraphina (src/mcp-tools/seraphina-tools.ts): `seraphina_guidance { goal }`
gathers the live roster, claims board and recent messages from the x.ruv.io
gateway, compacts them (dedupe by from|type, cap 15 — a cheap tier drowns in
repeated PeerHellos), and asks the cognitum meta-llm gateway
(https://api.cognitum.one/v1/messages, model cognitum-auto by default with a
tier override) using a queen system prompt that treats message content as
data, respects one-owner-per-resource claims, and returns
{ guidance, proposals[], risks[] }. Proposals are advisory. JSON is extracted
by slicing the outermost object so fenced/prose-wrapped answers still parse.
Key from SERAPHINA_METALLM_KEY (GCP secret seraphina-metallm-api-key).
CLI: `ruflo federation sync|roster|claims|registry|invite|admit|publish`
(src/commands/federation.ts), thin over the same tools.
Skill: .agents/skills/open-federation/SKILL.md documents CLI, tools,
Seraphina, the claims rules, the canonical NIP-42 relay-tag gotcha, and
onboarding.
Tests: 11 (x-federation: RPC shaping, SSE parse, resource mapping, fail-closed
admin gating, isError surfacing; seraphina: registration, fail-closed key,
context gathering + queen prompt + x-api-key, tier override, fenced-JSON
extraction). Project typechecks at 0 errors.
Verified live: Seraphina against the real 3-node federation returned 3
structured proposals + 4 risks via cognitum-auto (routed to a cheap tier).
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
* fix(federation): ADR-125 env-var precedence — URL config takes a flag/arg, credentials registered env-only
The env-var-precedence audit correctly flagged the new process.env reads.
Config URLs now have a precedence path: `ruflo federation --gateway` feeds a
`gatewayUrl` tool arg (and Seraphina takes `metaLlmUrl`/`gatewayUrl`) that
wins over RUFLO_X_GATEWAY_URL / SERAPHINA_METALLM_URL, documented per ADR-125.
Credentials (RUFLO_X_ADMIN_TOKEN, SERAPHINA_METALLM_KEY) are registered as
env-only escape hatches with rationale: a secret must never be a CLI flag.
+1 test (arg precedence, arg not forwarded upstream). Audit passes locally.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
* feat(federation): `ruflo federation join --code` — self-service invite access with your own key
The user-facing path into the open swarm. Decentralized by design: the user
generates/holds THEIR OWN Nostr key (~/.ruflo/nostr.key, 0600), redeems an
invite code with a NIP-98-signed claim directly against the relay (no admin in
the loop), proves membership via NIP-42, and can then publish as themselves.
The gateway never signs for a user.
- src/mcp-tools/x-federation-join.ts: x_federation_join { code, relayHttp?,
relayWs?, keyFile? } — validates the code shape before any network call,
loads/creates the key, NIP-98 claim, NIP-42 verify, returns pubkey + role.
ADR-125: args take precedence over RUFLO_X_RELAY_HTTP / RUFLO_X_RELAY_WS /
RUFLO_NOSTR_KEY_FILE.
- CLI: `ruflo federation join --code v2.…`
- nostr-tools added as an optionalDependency (secp256k1/Schnorr is not in
node:crypto); the tool degrades with an install hint when absent. pnpm
lockfile regenerated in this PR (frozen-lockfile CI).
- 4 tests: 0600 key create/reuse, valid NIP-98 header (kind 27235, verifies,
bound to url+method+payload hash), malformed code rejected pre-network,
ADR-112 description. Suite: 16/16; tsc 0; env-var audit passes.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
* fix(federation): default relay -> wss://relay.ruv.io; join test tolerates missing nostr-tools
- x-federation-join defaults now point at the canonical relay.ruv.io host (Cloud Run
domain mapping for buzz-relay); the raw run.app host stays routable for old clients.
- Validate the invite-code shape before the optional-dependency check so a bad code
fails fast whether or not nostr-tools is installed.
- The root Test Suite runs the CLI tests via root npm ci, which never installs the
CLI's optionalDependencies: the crypto cases are it.skipIf(!nostr-tools) so the
file no longer trips the CI test ratchet.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
|
||
|
|
e111bfbd7f |
fix(memory,tests): restore memory search recall and green the CLI suite (69 → 0) (#3252)
* fix(memory,tests): restore `memory search` recall and green the CLI suite (69 → 0) The CLI suite had 69 failing tests across 50 files. Root-causing each rather than adjusting expectations turned up one real product regression, two real bugs, and a set of tests asserting things about the machine instead of the code. PRODUCT REGRESSION — `memory search` recalled nothing (re-break of #2558) `memory search` returned zero results for content that matched word for word, while store/list/retrieve worked. Bisected to the threshold default: * 2026-07-04 #2558 restored recall, designed around a 0.3 threshold. Its fusion scores a full-coverage keyword hit as 0.6*max(0,semantic) + 0.4*lexical, so with a non-positive cosine — routine for a one-word query — a PERFECT keyword match tops out at exactly 0.40. * 2026-07-26 #2790 set the CLI default to 0.7. Above 0.4, keyword recall is mathematically unreachable, so #2558 silently regressed. Instrumented the live path to confirm before changing anything: key=note/alpha terms=["connectivity"] cov=1 bm25=0.029 sem=-0.883 lex=1.000 score=0.400 thr=0.7 → dropped Default restored to 0.3 at both sites, with the ceiling documented so it is not raised past 0.4 again. Verified end to end: a shared keyword recalls all three entries, a unique keyword recalls only its own. This was broken on main too. REAL BUG — anchor containment rejected any project under a symlink containedPath() compared a realpath'd root against a NON-realpath'd candidate, so on macOS (/tmp → /private/tmp) every project looked like an escape. Now compares like with like. Both guarantees re-verified by probe: ../ traversal, deep traversal, absolute-outside and symlink-escape all still rejected; symlinked-root spelling now accepted. REAL BUG — a test that could not fail funnel.test.ts asserted `elapsed < 100ms` as a proxy for "no network call" — its own comment conceded a future fetch() would still pass. It also failed intermittently (118ms) under full-suite CPU contention. Replaced with an actual assertion on fetch/http.request/https.request, patched via the CJS copies (ESM namespaces are non-writable). Negative-controlled: the technique observes a real call and restores cleanly. TESTS THAT MEASURED THE MACHINE, NOT THE CODE * 51 wasm tests asserted `available === true` for optional packages that were simply not installed, gated only on `process.env.CI`. Now also gated on real availability. The 7 tests in those files that need no WASM were lifted into their own blocks so they keep running rather than being skipped along with the rest — no coverage traded away for a green tick. * 4 agenticow tests lacked the `skipIf(!havePkg)` guard their 4 siblings had; in degraded mode the verb is inert and returns before validation. * policy-runtime hashed the raw tmpdir path while the source hashes the CANONICAL one, so it looked for a trust anchor that is never written there — and its cleanup was deleting a nonexistent path, leaking real anchors into ~/.config on every run. * sona's provider allowlist predated the ruvector / wasm-embedder / @claude-flow/embeddings backends. * ruvector/index asserted vi.mock satisfies a *dynamic* import() of a bare specifier. It does not under vitest 4 — the specifier is rewritten to '/@id/@ruvector/core' and fails with ERR_MODULE_NOT_FOUND. Now pins the contract the try/catch actually provides: unloadable → false, never throws. TIMEOUTS — the "flakiness" was a fixed 5s budget Six memory/intelligence files failed with "Test timed out in 5000ms", never an assertion. They initialise a real ONNX embedder and SQLite; alone they are fast, but the full suite saturates every core (~440% CPU). Because it depended on scheduling, a different file failed each run. Raised testTimeout/hookTimeout to 30s. (Process-level isolation was tried first and ruled out — forks changed nothing, which is what identified the cause as time, not shared state.) Verified: 3 consecutive full runs at 0 failed / 3572 passed on the feature base, and 0 failed / 3537 passed on this branch's base. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67 * fix(tests): make ruvector availability assertion environment-independent CI caught what my first pass missed. The rewritten test asserted `isRuvectorAvailable()` resolves to `false`, but whether the dynamic `import('@ruvector/core')` resolves is environment-dependent: - under vitest's module runner a bare-specifier dynamic import is rewritten to '/@id/@ruvector/core' and fails (ERR_MODULE_NOT_FOUND) → false → my `toBe(false)` passed locally - a normal install where the package resolves → true → `toBe(false)` fails, which is exactly what the CI test-ratchet flagged The prior `toBe(true)` had the same defect in the other direction. Pin the invariant that holds in BOTH environments instead — the only thing the try/catch actually promises: it resolves to a boolean and never throws. No specific resolution outcome is asserted. Verified 33/33 locally; the sibling "should return boolean" test already covers the type, so this now documents the no-throw contract distinctly. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67 |
||
|
|
72df96c297 |
feat(agentbbs): cross-host federation — signed envelopes, pinned peers, union merge (#3251)
* feat(agentbbs): cross-host federation — signed envelopes, pinned peers, union merge
Phase 1 gave every host a local append-only room log and derived roomId
deterministically from the room label, so two hosts that register #sales
already compute the same roomId without talking. This adds the layer that
actually moves envelopes between them.
Design. A room log is an append-only set of immutable envelopes, so reconciling
two hosts is a set union — there is no conflicting write to arbitrate and
nothing for a consensus round to decide. Union is commutative, associative and
idempotent, so sync is order independent and safe to retry. That is a
grow-only set keyed on envelopeId, and it is cheaper and less failure-prone
than the Byzantine agreement the plugin README gestures at.
What union does not give you is authenticity: if any peer can inject, the merge
faithfully replicates forgeries. So each host now holds a persistent Ed25519
identity (Phase 1's key was ephemeral per process, which no peer can pin),
every published envelope is signed, and a receiver verifies against the key it
pinned at peer-add time rather than one carried in the envelope — otherwise an
attacker signs with their own key and claims any origin.
Transport is pull-based HTTP: peers poll GET /agentbbs/v1/rooms/:roomId/
envelopes?since=N. Pull needs no inbound connectivity, tolerates a node being
offline, and leaves ingest volume under the receiver's control; push would make
every node an unauthenticated write target. The server binds 127.0.0.1 unless
a bindHost is passed explicitly, and exposes no route that mutates state.
Bounds are enforced receive-side where a sender cannot negotiate them away:
envelopes per sync, bytes per envelope, bytes per response, peer count, and a
hop limit that is incremented on merge so a cycle terminates. hops is excluded
from the signed material, since it is mutated in transit by design.
Five tools: identity, peer_add, peers, serve, sync.
Tested: 35 new tests, most of them the trust boundary — forged payload,
forged origin, key substitution, unpinned signer, cross-room injection, replay,
oversize, hop exhaustion, traversal at the HTTP boundary, and private key never
served. Convergence and idempotence are asserted against two real nodes over a
real socket, not mocks. Verified end to end with three independent nodes:
all three converge on the same envelope set, a second round merges nothing,
and an unpinned node's injection is rejected.
The Phase 1 structural test pinned the surface at exactly 4 tools; it now pins
9 and names both groups, so adding a tool stays a deliberate contract change.
Full package suite: 68 failures against 70 on main, +35 passing — no
regressions.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
* ci(agentbbs): stop rule 2 firing on the federation wire namespace
ADR-164 rule 2 exists to stop `agentbbs` becoming a mandatory dependency: a
static `import ... from 'agentbbs'` is forbidden, and any other mention has to
sit behind loadAgentbbs / a dynamic import. Its catch-all is a substring test
for "agentbbs" anywhere in the file.
agentbbs-federation.ts trips that catch-all without importing anything. Its
only remaining mentions after comment-stripping are string literals that never
reach a module resolver: the '/agentbbs/v1/...' HTTP route prefix and the
'agentbbs:<kind>:' hash domain-separators.
Adds those two shapes to the existing benign-pattern strip list. The
static-import check is untouched and remains the real gate.
Verified this narrows the check rather than loosening it, by running the
workflow's own rule-2 body verbatim against fixtures:
- the real federation module -> passes (false positive gone)
- `import { x } from 'agentbbs'` -> still caught
- unguarded runtime reference -> still caught
- guarded `await import('agentbbs')` -> passes
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
* ci(agentbbs): feed rule 2 via a quoted heredoc so bash stops mangling it
The rule 2 fix in
|
||
|
|
07f1cf3a3f |
fix(agentbbs-tools): detect the real CLI-only agentbbs package instead of a nonexistent JS import (#3250)
* fix(agentbbs-tools): detect the real CLI-only agentbbs package instead of a nonexistent JS import
The published `agentbbs` package (0.2.1) is a CLI-only launcher — a `bin`
script with no importable JS entry point — that downloads/builds a Rust
binary and shells out to it. loadAgentbbs()'s `await import('agentbbs')`
could therefore never resolve, even with the CLI correctly installed and
on PATH, so all 4 federation_bbs_* tools reported degraded:true
unconditionally regardless of install state.
Replace the dynamic import with a subprocess presence probe
(agentbbsCliAvailable(), `agentbbs --version`), matching how the rest of
the codebase treats optional CLI tools. shell:true is required for the
probe to find npm's Windows .cmd shim (child_process does not consult
PATHEXT otherwise).
Making the probe actually succeed surfaced two further pre-existing bugs
in code that was previously always skipped:
- vitest.config.ts externalized @noble/ed25519 alongside genuinely-optional
deps (agentic-flow, agentdb, ...), but it is an always-installed hard
dependency; externalizing it broke dynamic-import resolution under Vite's
SSR transform.
- federation_bbs_human_join dynamically imported @noble/ed25519 a second
time (getSigningKey() already does), which is now deduplicated behind one
memoized loadEd25519() loader.
All 12 tests in agentbbs-tools.test.ts now pass, including the 4
happy-path tests that were previously always skipped.
* fix(agentbbs-tools): gate the probe shell to win32 instead of always on
`shell: true` was unconditional, and the probed binary comes from
`process.env.AGENTBBS_BIN`. Under a shell that argument is command-interpreted,
so `AGENTBBS_BIN='agentbbs --version; touch /tmp/PWNED'` executes the trailing
command. Verified locally: the file is created with `shell: true` and the spawn
fails with ENOENT once the shell is gated off.
This is the case browser-tools.ts already warns about in the comment beside the
same pattern -- "if user-controlled args are ever added, escape them before
spawn" -- and `shell: process.platform === 'win32'` is the convention already
used at browser-tools.ts:49, commands/init.ts:856 and :1194, and
init/helpers-generator.ts:1459. Unconditional `shell: true` appeared nowhere
else in the package.
The Windows behaviour the original was reaching for is unchanged: cmd.exe still
resolves the `agentbbs.cmd` shim via PATHEXT. POSIX simply stops interpreting
the override. Added windowsHide to match the sibling call sites.
Verified the fix still does what it set out to do: with the CLI resolvable the
suite is 12/12 and the four previously-always-skipped happy-path tests execute,
with the shell disabled on POSIX.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
|
||
|
|
d55b1bfeac |
fix(memory): stop reporting a gated native bridge as the active backend (#3228) + fast-uri CVE (#3245)
* 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 |
||
|
|
ea2df4726c |
fix(flywheel): encode candidatePolicy fractions as decimal strings (#3229) (#3235)
`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
|
||
|
|
6acc689150 |
dream(intelligence): #3220 gate EnhancedModelRouter's forwarded modelId on tier match (evaluated, ACCEPT-scoped) (#3221)
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 |
||
|
|
d413297dde |
fix(memory): stop seeding the bridge's ControllerRegistry with the sql.js dbPath
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 |
||
|
|
36740e4304 | fix(proxy): verify effective daemon on install | ||
|
|
b291ac4049 |
dream(intelligence): #3048 discounted Thompson sampling for model-router bandit (evaluated, ACCEPT-scoped) (#3049)
* 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> |
||
|
|
0e50fe7f57 |
dream(security): #3043 advisory scanner for untrusted settings.json hooks/allow-rules (evaluated, ACCEPT) (#3044)
* 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> |
||
|
|
02eae9c117 | fix(hooks): persist real session-end state (#3077) | ||
|
|
f7aab68602 |
fix: resolve Claude Code launches on Windows (#3076)
* fix: launch Claude Code from Windows npm installs * test: use Windows path semantics in resolver |
||
|
|
3a1b2257fb | fix(cli): honor MCP tool selection when listing (#3060) | ||
|
|
703e7741d7 |
fix(memory): #3051 memory_store tags lost via write-through cache (3.38.14) (#3079)
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
|
||
|
|
0f3c45101b |
fix: address #3045 #3064 #3065 + new ruflo-deepseek-harness plugin (3.38.13) (#3078)
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
|
||
|
|
e43f8b9750 |
Merge pull request #3073 from ruvnet/fix/322c-receipt-roundtrip
fix(flywheel): compute receipt statistics from encoded scale-12 values — honest receipts no longer fail their own verifier (ruflo#3072) |
||
|
|
a38647e1f9 |
fix(flywheel): compute receipt statistics from encoded scale-12 values (ruflo#3072)
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 |
||
|
|
7d0bb9c53b |
fix(flywheel): reject unknown fields in receipt and commit verification
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> |
||
|
|
693385eab0 | test(ci): keep safety guard root-compatible | ||
|
|
13cbd697db | fix(release): enforce CI and idle assignment safety | ||
|
|
b9a5077292 | test(cli): model atomic fs operations in MCP suite | ||
|
|
3f285bfb26 | fix(memory): disable unsafe native bridge on Windows (#3024) | ||
|
|
5c676fe242 | fix(doctor): report missing native sqlite bindings | ||
|
|
6b8f0d0b5a | fix(mcp): share state across HTTP transports | ||
|
|
2dbb790a0b | fix(mcp): restore HTTP protocol and CLI tools | ||
|
|
fa11ee8c24 | fix(cli): honor helper refresh opt-out before locking | ||
|
|
0c73bab4d0 | fix(cli): serialize helper refreshes across processes | ||
|
|
1e24bb8878 |
fix(agent): propagate explicit provider/model config into agent execution (#2962) (#3007)
* 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> |
||
|
|
57c0fcba98 |
fix(memory): stop misreporting bridge brute-force search as HNSW-accelerated (#3006)
* 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. |
||
|
|
c0c3ab071a |
fix(intelligence): stamp contentFingerprint in consolidate()'s graph write too (#3003)
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. |
||
|
|
35a6ad3697 |
fix(daemon): config.json resourceThresholds overrides were lost to stale daemon-state.json (#2935) (#3000)
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. |
||
|
|
9039e2bb33 |
fix(intelligence): same-ID content edits were cached/dropped instead of refreshed (#2920) (#2999)
* 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. |
||
|
|
7715eb5ea9 |
fix(security): loosen Stripe/OpenAI secret regex boundaries (#2931) (#2996)
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. |
||
|
|
2fd63cf3db |
fix(github-tools): pass --repo to gh in github_issue_track (#2963) (#2995)
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. |
||
|
|
d41af6e4d5 |
fix(hooks): await the ADR-130 reinforced-by graph edge write (#2961) (#2991)
`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. |
||
|
|
3d3f6e08cc |
fix(init): read the parser's actual camelCase flag keys (#2952) (#2989)
`--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. |
||
|
|
1e2178f246 |
fix(hooks): make hooks intelligence --train actually train (#2940) (#2988)
`--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. |
||
|
|
87f4f8ab67 |
fix(mcp): mcp start -t http exits after false-success table + doc drift from #2985 (#2987)
* fix(migrate,mcp): unresolvable remediation command + false-success HTTP server exit Two independently-reported, freshly-filed defects: - #2985: `ruflo migrate status`'s ADR-128 removed-agent remediation (shipped today via #2973/ADR-382 Part B) printed a command that cannot succeed twice over: `ruflo plugins install <plugin>` requires -n/--name (not a positional) AND targets the npm-package plugin system, while the gap detector's own registry check (installed_plugins.json) is for the Claude Code marketplace system entirely. Fixed to print the correct `/plugin install <plugin>@ruflo` slash-command flow, with a one-time "run /plugin marketplace add ruvnet/ruflo first" hint. Also corrects three adjacent doc-drift items the reporter found while verifying: README/USERGUIDE's `claude mcp add ruflo` examples (should be the canonical `claude-flow` key per #2206 — the exact stray-key duplicate #2612's healing logic exists to clean up), README's blanket "MCP server registered: No" claim for the plugin path (ruflo-core ships its own .mcp.json and does register one), and a stale comment in executor.ts conflating the CLI-track's mcp__claude-flow__* tool refs with the marketplace plugin binding (mcp__plugin_<plugin>_<server>__*). - #2984: `ruflo mcp start -t http` printed a full "Status: Running" success table — PID, health/RPC URLs — then exited 0 within seconds, never actually leaving the port bound. Root cause: bin/cli.js's "normal CLI mode" branch (which any explicit non-stdio --transport always takes) unconditionally exits once the dispatched command's action() promise resolves (#1552's "one-shot command" assumption). mcp.ts's `start` action returned immediately after printing the table, so the freshly-bound http.Server was torn down within milliseconds of the printed success claim. Fixed by blocking in the foreground after a successful start (SIGINT/SIGTERM-driven graceful shutdown), matching daemon.ts's established `--foreground` pattern — `daemonize` isn't currently wired to fork/detach anywhere in MCPServerManager, so every successful start via this path is a foreground server regardless of the -d flag. Tests: migrate-agent-detection.test.ts updated for the corrected command string; new mcp-http-foreground-2984.test.ts spawns the built CLI exactly like the bug report and asserts the port actually accepts connections after the success table prints (not just that it printed), then confirms SIGTERM still produces a clean exit. Both verified via git-stash A/B: fail against the pre-fix code, pass against the fix. Full suite otherwise unaffected — same 9 pre-existing sandbox-environment failures (ruvector/wasm bindings, embedder model downloads) reproduced identically before this change. Closes #2985. Closes #2984. Co-Authored-By: RuFlo <ruv@ruv.net> * revert: drop migrate-agent-detection changes, superseded by PR #2986 An external contributor (pacphi) independently opened #2986 fixing the same #2985 with the identical installCommand correction plus a real `migrate fix --agents` restore command — strictly more complete than this branch's minimal fix. Dropping the overlapping files here to avoid a duplicate/conflicting PR; reviewed #2986 and recommended it for merge. Remaining on this branch: the #2984 mcp start -t http fix and the doc-drift items #2986 explicitly scoped out (README/USERGUIDE mcp-add key, executor.ts comment). Co-Authored-By: RuFlo <ruv@ruv.net> |
||
|
|
dbd88254e3 |
fix(migrate): correct removed-agent install suggestion and add 'migrate fix --agents' restore (#2985) (#2986)
The suggestion printed by 'migrate status' (ADR-382 Part B) could not succeed: 'ruflo plugins install <plugin>' fails on syntax (--name is required) and targets the npm-package PluginManager, while the owning plugins are Claude Code marketplace plugins (not npm packages). The detection module's own header documents that split. - point installCommand at the marketplace flow the registry check actually verifies: /plugin install <plugin>@ruflo - add 'migrate fix --agents' (with --dry-run): restores missing agents from canonical plugin content — marketplace clone, then repo checkout, then GitHub raw pinned to the CLI's version tag with main fallback — rewriting the plugin-only mcp__plugin_ruflo-core_ruflo__ namespace to the canonical claude-flow server key (#2206) and recording provenance after the frontmatter. Never overwrites existing files. - status now hints at the fix command when gaps are found |
||
|
|
b8c37da8e9 |
fix(memory): unordered LIMIT truncation in search + honest sql.js persistence status (#2983)
Two independently-reported, verified defects in the memory bridge: - #2982/#2976 (duplicate reports): bridgeSearchEntries() and bridgeSearchHNSW() pre-ranked their corpus with `LIMIT 1000`/`LIMIT 10000` and no ORDER BY, so SQLite returned rows in arbitrary storage order (insertion order on a fresh table) before BM25/embedding scoring ever ran. On any corpus over the limit, newest entries were silently excluded from scoring regardless of relevance. bridgeListEntries() already ordered by updated_at DESC for the same reason; both search paths now match it. Verified via a stash A/B: identical repro returns zero results without the fix, recalls the target with it. - #2968: bridgeStoreEntry() swallowed every wal_checkpoint(PASSIVE) failure as "non-fatal" and unconditionally reported store success. On the sql.js fallback driver (engaged when better-sqlite3's native binding never got built — e.g. a skipped optionalDependency postinstall), that PRAGMA throws "Invalid PRAGMA command" every time, and writes may never reach disk. The specific failure signature is now distinguished from ordinary busy/non-WAL pragma failures and surfaced as `persistWarning`, which `memory store` prints instead of an unconditional green "Data stored successfully". Both fixes stay within the CLI's own bridge layer — no changes to the external agentdb/better-sqlite3 packages. Tests: 2 new regression files (3 tests), full CLI suite otherwise unaffected (10 pre-existing failures reproduced identically against a clean baseline with these changes stashed — all in ruvector/wasm/ embedder-model-download paths, unrelated to this change). Closes #2982, #2976. Part of #2968 (root-cause guarded; upstream agentdb sql.js persistence itself is unchanged). |
||
|
|
2c8f863583 |
fix(migrate): implement ADR-128's promised removed-agent detection (ADR-382 Part B) (#2973)
ADR-128 Phase 2 deleted 9 forked agent files from the init template (coder.md, researcher.md, reviewer.md, tester.md, memory-specialist.md, security-auditor.md, sparc-orchestrator.md, goal-planner.md, adr-architect.md), making each plugin canonical. Its Consequences section promised "`ruflo migrate` should detect removed agents and print install suggestions" — never implemented until now. Adds detectRemovedAgentGaps() (new sibling module migrate-agent-detection.ts, kept out of the already-783-line migrate.ts per the file-size budget) which, for each of the 9 agents, checks whether the basename is missing from the project's .claude/agents/ tree and whether the owning plugin is installed per Claude Code's global ~/.claude/plugins/installed_plugins.json registry (the only real on-disk signal for marketplace-plugin install state; ruflo's own PluginManager manifest tracks a separate npm-package-based system). Pure filesystem checks, no CLI version comparison, per ADR-382 Part B scope. `ruflo migrate status` now prints a second table naming any such gap, its owning plugin, and the exact `ruflo plugins install <plugin>` command to restore it (JSON output via ctx.flags.format === 'json' gains a removedAgentGaps field, matching this file's existing patterns). See v3/docs/adr/ADR-382-init-scaffold-content-drift-remediation.md (PR #2972) and https://github.com/ruvnet/ruflo/issues/2971. |
||
|
|
6d58837f4f |
fix(proxy): make the subscription plane reachable again after cloud routing (#2928)
* fix(proxy): disclose what enabling cloud routing actually decides, and let users pin a tier Closes the terminal half of cognitum-one/meta-proxy#43 M5b, and adds the `routing_mode` control ruflo never had. meta-proxy ADR-321 rev-2 (shipped v0.6.0, #43 M2) applies tier selection to ALL Cloud traffic. Before it, a deliberately configured Cloud plane forwarded the client's own model name -- and since Claude Code names a frontier model by default, every request was served at the top tier with no ceiling and no proxy daily cap. Difficulty-routing all Cloud traffic is the fix for that defect, not a regression, and it deliberately does not touch Passthrough or Local: those are the user's own subscription and their own backend. But ruflo never told anyone. `ruflo proxy config --cloud` said only that prompts go to api.cognitum.one "and forwarded to the selected provider" -- so a user could enable cloud routing still believing their model choice held. #43 M5a fixed the Developer Console selector; M5b ("equivalent disclosure to any terminal flow that explicitly activates Cloud") is this command, and it was still open. The disclosure now answers the three things activating Cloud actually decides, each of which has a different answer than the plane being left: - who processes the prompt (Cognitum, server-side) - who pays (the Cognitum account -- NOT the user's own Claude subscription, which is what Passthrough uses and is unaffected) - which model runs (the plane picks a tier per prompt rather than honoring the requested model) `--routing-mode <auto|low|mid|high>` is rev-2's escape hatch. The console got a control for it in meta-proxy#52; ruflo had none, so a ruflo user was stuck on auto with no way to disagree -- and the scorer reads prompt SHAPE, not task difficulty, so a short-but-hard prompt can under-escalate. Setting a tier must never activate Cloud (ADR-321 Revision 3 keeps the plane choice and this Cloud-only setting as separate controls), so `--routing-mode` alone writes only `routing_mode`, asks for no consent it does not need, and says the setting is inert until Cloud is on. `--routing-mode` with `--local-only` is refused rather than silently resolved, and an unconfirmed `--cloud` still writes nothing at all. Wire values confirmed against meta-proxy's `RoutingMode` enum (`src/config.rs`, same `#[serde(rename_all = "snake_case")]` treatment as `DataPlane`), and `#[serde(default)]` there means an existing config that omits the field keeps working and reads as auto. An unrecognized value already in the file reports as auto rather than being echoed back, matching the proxy's own degrade-to-safe-default behavior. ADR-304 records both, and drops its inline copy of the disclosure text -- that copy had already fallen behind the shipped string, and a reader could not tell which was real. #43 M3 (a Cloud tier ceiling and daily cap) stays open upstream as a product decision, so the disclosure makes no claim about caps either way. Test: 9 proxy test files, 68 tests, all passing (was 56). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(proxy): make the subscription plane reachable again after cloud routing `--local-only` was the only exit from the cloud plane, and it writes `default_data_plane = "local"` -- the user's own Ollama/vLLM/SGLang backend. `passthrough`, which is meta-proxy's own default AND the plane that uses the user's own Claude subscription, was unreachable from this command by design ("passthrough is never written", ADR-304's 07-16 addendum). So the undo we advertised was wrong. A user on passthrough who followed our own disclosure -- "Disable anytime: ruflo proxy config --local-only" -- landed on a THIRD state they never chose, pointed at a local backend that may not be installed. Two consequences, neither visible: 1. Their Claude subscription stops being used at all. That is the same harm cognitum-one/meta-proxy#51 closed on the login path ("silently switches a user away from Passthrough -- their own Claude subscription"), reached through a different door. 2. Automatic quota failover silently stops applying. meta-proxy gates it on the plane (`src/routing.rs`: `automatic_eligible = cfg.default_data_plane == DataPlane::Passthrough`) because Passthrough is the only plane that sees Anthropic's own rate-limit headers (ADR-320). A user parked on `local` is opted out of ADR-321 entirely and is never told. The no-flag report papered over the difference, printing one line -- "Cloud routing is OFF -- requests never leave this machine (or use your own Claude subscription on Passthrough)" -- for two planes that do not behave alike. `--local-only` keeps its meaning exactly; the flag name and ADR-304 both promise a local backend, and silently retargeting it would just be a second surprise. Instead: - `--passthrough` is added, writing `default_data_plane = "passthrough"` and revoking cloud-routing consent the same way `--local-only` does. - The disclosure names both exits instead of presenting one as THE undo. - `--cloud` reads the plane it is leaving before overwriting it and prints the command to restore it. That read is the only moment ruflo knows where the user was, and it beats persisting a "previous plane" that could go stale against a hand-edited TOML. - `--local-only` now says the Claude subscription is not used on that plane, and how to choose it instead. - The no-flag report describes each plane distinctly and calls out the ADR-321 failover consequence while on `local`. - Plane flags are mutually exclusive -- more than one is refused rather than resolved by precedence, since any precedence would discard something the user explicitly asked for. No default changes: a user who never runs this command is on passthrough, exactly as before. ADR-304 records the decision and supersedes its own "passthrough is never written" claim. Test: 9 proxy test files, 80 tests, all passing (was 68). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(proxy): correct disclosure's who-pays claim (post-merge reapply) Same fix as on #2927 — the disclosure asserted Passthrough "is the plane in effect until you turn this on", which is false whenever the user is already on --local-only. Reapplying after merging main (which already had this fix on the pre-passthrough disclosure text) since this branch's own commit still carried the original wording. Co-Authored-By: RuFlo <ruv@ruv.net> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: ruv <ruv@ruv.net> |
||
|
|
e1f7e4a232 |
fix(proxy): disclose what enabling cloud routing decides, and let users pin a tier (#2927)
* fix(proxy): disclose what enabling cloud routing actually decides, and let users pin a tier Closes the terminal half of cognitum-one/meta-proxy#43 M5b, and adds the `routing_mode` control ruflo never had. meta-proxy ADR-321 rev-2 (shipped v0.6.0, #43 M2) applies tier selection to ALL Cloud traffic. Before it, a deliberately configured Cloud plane forwarded the client's own model name -- and since Claude Code names a frontier model by default, every request was served at the top tier with no ceiling and no proxy daily cap. Difficulty-routing all Cloud traffic is the fix for that defect, not a regression, and it deliberately does not touch Passthrough or Local: those are the user's own subscription and their own backend. But ruflo never told anyone. `ruflo proxy config --cloud` said only that prompts go to api.cognitum.one "and forwarded to the selected provider" -- so a user could enable cloud routing still believing their model choice held. #43 M5a fixed the Developer Console selector; M5b ("equivalent disclosure to any terminal flow that explicitly activates Cloud") is this command, and it was still open. The disclosure now answers the three things activating Cloud actually decides, each of which has a different answer than the plane being left: - who processes the prompt (Cognitum, server-side) - who pays (the Cognitum account -- NOT the user's own Claude subscription, which is what Passthrough uses and is unaffected) - which model runs (the plane picks a tier per prompt rather than honoring the requested model) `--routing-mode <auto|low|mid|high>` is rev-2's escape hatch. The console got a control for it in meta-proxy#52; ruflo had none, so a ruflo user was stuck on auto with no way to disagree -- and the scorer reads prompt SHAPE, not task difficulty, so a short-but-hard prompt can under-escalate. Setting a tier must never activate Cloud (ADR-321 Revision 3 keeps the plane choice and this Cloud-only setting as separate controls), so `--routing-mode` alone writes only `routing_mode`, asks for no consent it does not need, and says the setting is inert until Cloud is on. `--routing-mode` with `--local-only` is refused rather than silently resolved, and an unconfirmed `--cloud` still writes nothing at all. Wire values confirmed against meta-proxy's `RoutingMode` enum (`src/config.rs`, same `#[serde(rename_all = "snake_case")]` treatment as `DataPlane`), and `#[serde(default)]` there means an existing config that omits the field keeps working and reads as auto. An unrecognized value already in the file reports as auto rather than being echoed back, matching the proxy's own degrade-to-safe-default behavior. ADR-304 records both, and drops its inline copy of the disclosure text -- that copy had already fallen behind the shipped string, and a reader could not tell which was real. #43 M3 (a Cloud tier ceiling and daily cap) stays open upstream as a product decision, so the disclosure makes no claim about caps either way. Test: 9 proxy test files, 68 tests, all passing (was 56). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(proxy): correct disclosure's who-pays claim for --local-only users The disclosure text asserted "[Passthrough] is the plane in effect until you turn this on" — false whenever default_data_plane is already "local" (i.e. exactly a --local-only user considering re-enabling cloud routing). Reword to state Passthrough is the proxy's default and what the user's Claude subscription is used by, without claiming it's necessarily the plane currently in effect. Co-Authored-By: RuFlo <ruv@ruv.net> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: ruv <ruv@ruv.net> |
||
|
|
d3e647a5dd |
fix(proxy): bump pinned meta-proxy release 0.4.0 -> 0.7.3 (#2925)
* fix(proxy): bump pinned meta-proxy release 0.4.0 -> 0.7.3 `DEFAULT_PROXY_RELEASE` had not moved in six meta-proxy releases, so `ruflo proxy install` shipped a binary predating every fix since v0.4.0: - require explicit Cognitum Cloud routing consent (#60) first in v0.7.0 - handle --help before binding (#74/#76) first in v0.7.2 - --help/--version win from any argv position (#85) first in v0.7.3 - reload live daemon config on login/logout (#28) first in v0.4.1 - pin the Linux glibc floor to 2.28 (#66) first in v0.7.1 The consent one matters most: the install disclosure promises cloud routing is "never enabled by install alone", but the commit that separated authentication from routing consent landed in v0.7.0 — after the pinned build. The --version regression is directly observable on 0.4.0: the binary starts the daemon, writes the token file and binds 127.0.0.1:11435, where meta-proxy's own source documents --version as side-effect-free ("no server, no I/O"). Any probe silently leaves a listener running. Also stop hardcoding the version in proxy.ts's example text. That string was written as a literal rather than interpolating the constant, so the advertised version could — and did — drift from the installed one. Both tests now read the pin from the module for the same reason; asserting a literal means every bump needs hand-edited test expectations. Verified on Windows x64: `ruflo proxy install --release 0.7.3 --yes` installs with signature + checksum verification intact, and `--version` then prints and exits with no process left running and no port bound. Test: 9 proxy test files, 56 tests, all passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(proxy): surface pin drift to already-installed users Bumping DEFAULT_PROXY_RELEASE only changed what a NEW install receives. Someone who had already run `proxy install` on 0.4.0 got no signal that anything moved: `proxy status` reported installation as a boolean, and `proxy update` required an explicit --release, so reaching a newer binary meant already knowing its version number. The fixes that motivated the bump therefore reached only the users who did not yet have the problem. ProxyStatus now carries `version`, read from the install manifest install.ts already writes. `proxy status` prints it, the console guidance names the gap and the command that closes it, and `proxy update` defaults to the pin the same way `install` does. Updating stays explicit and user-initiated. The version is read from the manifest, never by executing the binary: releases before v0.7.2 start the server and bind a port on ANY invocation, so a `--version` probe against an old build leaves a listener running -- precisely the state this check exists to detect. A missing or malformed manifest reports "unknown", never "up to date". Also corrects two stale claims this change makes load-bearing: - proxy/lifecycle.ts's header asserted as fact that the binary "takes no CLI flags -- no --version/--help, any invocation just starts the server" (confirmed 2026-07-16). v0.7.2 handles --help before binding and v0.7.3 makes --help/--version win from any argv position. Left as-is, the next reader concludes no version handshake is possible. - ADR-307 still described the pin as v0.4.0 -- a fourth copy of the literal this PR is de-duplicating, and the one a reviewer reads to decide what the pin should be. It now names no version at all and points at the constant, plus records the drift-surfacing behavior. Test: 9 proxy test files, 64 tests, all passing (was 56). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a39a1dccd0 |
fix(proxy): fall back to tar when Expand-Archive fails, and cover extraction (#2926)
`ruflo proxy install` aborted on Windows after the release had already been
downloaded and its Ed25519 signature + sha256 verified:
[ERROR] Install failed
Expand-Archive failed (exit 1): Expand-Archive : The 'Expand-Archive'
command was found in the module 'Microsoft.PowerShell.Archive', but the
module could not be loaded.
`Microsoft.PowerShell.Archive` is a script module reached through PowerShell's
autoloading, so it can fail for environment reasons unrelated to the archive.
The same command succeeded on the same machine minutes later, so this is
intermittent rather than a hard platform break — which makes it worse, not
better: a verified archive was discarded and the install left with nothing.
Windows is the only platform exposed to this. releaseArchiveExtension() sends
`*-windows-msvc` to .zip and every other target to .tar.gz, and the .tar.gz
branch already shells out to tar.
Expand-Archive stays the PRIMARY zip path — bsdtar's zip support is still not
something to lean on by default, per the existing comment. It now falls back to
`tar -xf` (bsdtar ships on Windows 10+) instead of giving up, and only a failure
of BOTH extractors is fatal, reporting each cause. Extraction is split into two
helpers so each path is independently exercisable.
Adds __tests__/proxy-extract-archive.test.ts (10 tests). Extraction was the only
platform-divergent step in the install pipeline and had no direct coverage:
nothing in __tests__ referenced extractArchive, ExtractionError, or
Expand-Archive. The executor is mocked, so routing is asserted on every platform
without needing a real powershell.exe or bsdtar.
`extractArchive` is exported for those tests.
Verified the tests pin the fallback rather than merely the new export: with the
fallback disabled but the export kept, exactly the 5 fallback-specific tests
fail and the other 5 still pass.
Test: 10 proxy test files, 66 tests, all passing. Changed files typecheck clean.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
1d365d4a65 |
fix: silent memory-write loss + daemon lock cwd bypass (#2887, #2878, #2877) (#2966)
Three independent data-integrity/correctness bugs in the memory and daemon subsystems: #2887 — agentdb_hierarchical-store reported success:true for writes that were never persisted. Root cause: agentdb dropped its HierarchicalMemory export at 3.0.0-alpha.17, so every call was silently landing on an in-process-only TieredMemoryStore fallback. The fallback now writes through to a `tiered_memory` SQLite table when given a db handle, and the bridge refuses to report success for a write that isn't durable — it names the fallback controller and reason instead. agentdb_health now reports the real persisted row count so a claimed write vs. an empty table is visible. #2878 — concurrent sql.js writers lost updates while reporting success (measured 10/12 lost in a 12-way concurrent repro), because storeEntry, getEntry, deleteEntry, applyTemporalDecay, and ensureSchemaColumns did unlocked whole-image read-modify-write against memory.db. All five now run under the existing withMemoryDbLock primitive (previously only used by purgeNamespace). Made the lock reentrant via AsyncLocalStorage, since every mutator calls ensureSchemaColumns internally and a plain O_EXCL lock isn't reentrant. Also fixed a path-normalization bug where different callers spelled the same db path differently and so serialized against nothing. #2877 — the daemon lock/PID file was keyed to raw process.cwd(), so `daemon start` from a project subdirectory span its own lock/PID pair instead of contending with one from the project root, bypassing the #2407/#2484 dedup. Added resolveDaemonProjectRoot(), which walks up from cwd to the nearest directory carrying a real Ruflo marker (not just any package.json, so independently-initialized nested sub-projects in a monorepo keep their own daemon), stopping at a .git boundary so the walk can't escape into an unrelated ancestor project. Wired into every daemon command that previously used process.cwd() directly. Tests: 123 passing across affected suites (controller-registry.test.ts, tiered-memory.test.ts, hierarchical-store-durability-2887.test.ts, memory-concurrent-write-loss-2878.test.ts, daemon-autostart.test.ts). Both packages typecheck clean. Pre-existing failures in unrelated suites (ruvector wasm bindings, agenticow, memory-search-recall-2558) reproduce identically with these changes reverted — confirmed not regressions. Fixes #2887, #2878, #2877 |