Repository navigation
release: 3.46.0 integration train (29 PRs + memory/neural/cli-core leaf publishes) - #3443
Merged
Merged
Conversation
…ait (evaluated, ACCEPT) UnifiedSwarmCoordinator.waitForTaskCompletion()/waitForQueuedTask() busy-polled state.tasks via setInterval(...,100) despite the coordinator already extending EventEmitter and already firing task.completed/task.failed at every terminal task-status transition. Rewritten to be event-driven with a synchronous pre-check for an already-terminal task and a setTimeout fallback for the timeout case; setMaxListeners raised to avoid spurious warnings at realistic concurrency (adversarial-critic finding, fixed same session). 4 new discriminating tests (baseline fails 3/4, candidate passes 4/4 via stash isolation); real wall-clock benchmark 100.359ms -> 0.206ms mean (~487x, N=20); 234/234 full swarm-package suite, tsc clean. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01VWUxwsNQVofSGXi8us9dEc
Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01VWUxwsNQVofSGXi8us9dEc
acquireLock() records {pid, acquiredAt} in .claude-flow/policy/state.lock
but only ever reclaimed a lock by mtime age (LOCK_STALE_MS = 30 s). Every
MCP tool call, reads included, passes through authorizeMcpTool() ->
withPolicyTransaction() -> acquireLock(). When a server is killed inside a
transaction (SIGKILL, OOM, or a default-action SIGTERM: the bin/cli.js
stdio loop installs no handler), its finally block never releases the
lock, so each call on a fresh server waited LOCK_WAIT_MS and failed with
policy-state-lock-timeout until the orphan aged past 30 s: five 5 s
failures after every such crash.
Reclaim the lock at once when its owner is provably gone:
- The lock now also records host, platform and, on Linux, PID namespace
and kernel boot id. The pid is trusted only when every field matches
the reader's, because a sandboxed CLI (bwrap --unshare-pid), a
container, another kernel boot or another machine sees ESRCH for a live
owner. host alone is not an identity: two machines sharing a directory
can report the same hostname, and every Linux init PID namespace has
the same inode (PROC_PID_INIT_INO).
- Only ESRCH counts as dead (EPERM and any other error mean alive), and
the pid must be a number, an integer, and in 1..2^31-1 (process.kill
coerces a numeric string).
- The lock is read only if it is a small regular file (lstat, then
O_NOFOLLOW|O_NONBLOCK, fstat, at most 1 KiB), so a symlinked FIFO or
device cannot block the event loop.
- After the probe the lock is re-read and must be byte for byte the one
that was read, so a lock that changed hands during the probe is not
deleted. Comparing dev/ino/ctime instead is unsound on Linux: ext4
reuses a freed inode number at once, and before kernel 6.13 two files
created in one clock tick share a ctime. Replaying the handover on a
non-multigrain ext3 mount, the stat triple deleted the live new owner's
lock in 192 of 200 runs; the byte comparison in 0 of 200.
Every other lock, including one written by an older version, keeps the
30 s mtime rule. ADR-324's lock paragraph is updated to match.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTedGmPKpEdArVBMMn3JaP
`mcp start` on the stdio transport force-"restarted" whatever server was recorded in the shared PID file — a single slot per os.tmpdir() (per user on macOS/Windows; the machine-wide /tmp on Linux unless TMPDIR is set). It sent SIGKILL to that PID, because a stdio server "can't be health- checked". The victim was simply the last server started through the command parser: another terminal's stdio server, or a live http/websocket server that other clients were using. For the same reason, a terminal stdio server made a later `mcp start -t http` refuse with "MCP Server already running". A stdio server is owned by the client that spawned it, over its own pipes, and any number can run side by side. Only a port-bound transport is single-instance. ADR-071 made this singleton safe against self-detection and PID reuse; a stdio server has no business claiming or killing the shared slot at all. So: - commands/mcp.ts: a stdio start kills a recorded server only on an explicit --force; http/websocket keep their guard unchanged. - MCPServerManager: a stdio server no longer refuses to start over the PID file, writes it, removes it on stop(), or health-checks it. While it serves stdio, getStatus() and checkHealth() report the process itself rather than whatever PID is recorded. A stale record is still cleaned up by the pre-start status read, as before. The documented piped-stdin `mcp start` (how MCP clients launch it) is answered by bin/cli.js's inline fast path, which never touches the PID file. A piped start whose argv does not begin with `mcp` (a leading global flag) does reach the parser, and is covered too, because the fix keys on the transport rather than on the TTY. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTedGmPKpEdArVBMMn3JaP
…ecord durable identity (#3364) Review round 2 (@ruvnet on #3365). The first round kept a one-way "has served stdio" flag so a second stop() could not delete another server's record. That flag outlived the server it described: after start -> stop, an independent status query still reported the manager running and checkHealth() still reported it healthy. On upstream e558f0c the same replay reported {"running":true,...} and {"healthy":false,"error":"No PID file found"} — the status half is the pre-existing #2934 fallback, the health half was ours. The flag is now the manager's live lifecycle ('idle' | 'running' | 'stopped'): - while it is serving stdio the manager IS the server, so it reports itself and never reads or clears the record it deliberately never wrote (the 30s health monitor calls checkHealth()); - once stopped it reports running: false and healthy: false rather than whatever PID the shared file happens to hold; - 'idle' — a manager that never started, which is what `mcp status`, `mcp health` and `mcp stop` build — keeps today's behaviour exactly, including the #2934 stdio fallback. stop() no longer needs a sticky flag: a stopped manager reports not running, so a second stop() returns at its own guard. The PID record now carries durable instance identity, the same model #3363 reached for the policy lock: host, platform, Linux PID namespace and kernel boot id, and the owner's process start time (/proc/<pid>/stat field 22 on Linux, `ps -o lstart=` elsewhere). A recorded PID is believed only while all of it still matches, so a PID the OS has since handed to another process, or one from another boot, host or namespace, is no longer treated as a live server — it can no longer make an http start refuse, or be SIGKILLed by `mcp start --force`. A server retracts its record only while the file still holds, byte for byte, what it wrote. Format compatibility: line 1 is still the bare PID, so an older ruflo's parseInt and `head -1` both read it unchanged; line 2 is the identity. A bare-integer record — an older ruflo's, or `start-mcp.sh --daemon`'s — parses with no identity and behaves exactly as today. v3/scripts/start-mcp.sh now reads the first line. Not covered: PID reuse and boot detection need the identity line, so a record written by an older ruflo keeps today's exposure; Windows has neither /proc nor ps, so its records carry no start time and PID reuse stays unqualified there, as isProcessRunning()'s process-name check already is. Tests: 13 added (20 total in the file), covering start -> stop -> independent status/health denial, concurrent ownership, PID reuse and non-owned processes, and the Windows-shaped inputs. All 13 fail on 85c6888 except the two labelled as pinning existing behaviour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTedGmPKpEdArVBMMn3JaP
…led instead of npm-installing at tool-call time (#3366) On a stock ruflo 3.42.4 install every metaharness_* MCP tool reached for the npm registry at tool-call time, although the packages it needs are already on disk. Two causes, in the same six helper files, which is why they ship together: the helper PINS rejected what ruflo installs, and three code paths never looked for an installed copy at all. Fixing only the pins leaves darwin and the memory tools on npx; fixing only the lookups leaves them resolving a pin that nothing on disk satisfies. 1. Pins (metaharness, @metaharness/darwin) The metaharness_* tools resolve MetaHarness through the plugin helpers' own tilde pins, not through package.json. Those pins were never bumped: _harness.mjs stayed at ~0.3.0 after the CLI declared metaharness ^0.4.1 (b8ec03f), and _darwin.mjs stayed at ~0.8.0 while the CLI moved @metaharness/darwin to ~0.9.0 (#2958) and ~0.10.2 (#3262). On a stock install (metaharness 0.4.2, darwin 0.10.2) findLocalPackageDir() therefore rejected the bundled metaharness, and every score / genome / mcp_scan / threat_model / oia_audit / learn call ran `npm install --prefix ~/.ruflo/metaharness-cache-0.3.0 metaharness@~0.3.0` at tool-call time — a download of the older 0.3.2 (180 s install budget inside a 120 s MCP subprocess budget), degraded offline. The darwin tools fetched darwin 0.8.x through npx while gepa imported the bundled 0.10.2. - _harness.mjs: METAHARNESS_PIN_VERSION ~0.3.0 -> ~0.4.1 (= CLI ^0.4.1) - _darwin.mjs: DARWIN_PIN_VERSION ~0.8.0 -> ~0.10.2 (= CLI ~0.10.2) - scripts/check-metaharness-pins.mjs: the watcher compared only the CLI's declared ranges (and MH_DARWIN_PIN), so it reported "all current" while the pins that decide what runs were two minors behind. It now reads each helper's *_PIN_VERSION: the pin must be a `~X.Y.Z` range (the only form _invoke.satisfiesTildeRange accepts), and must cover the declared range, or admit npm latest when the CLI declares nothing (redblue). An unreadable constant is fatal. `satisfies` now shares a `bounds()` helper with the new `covers()`; the results for the existing rows are unchanged. - metaharness-pin-drift.yml also triggers on the three helper files. - The two current-pin comments in TypeScript (distill-oracle.ts MH_DARWIN_PIN, metaharness-tools.ts ADR-153 header) named the old ~0.8.0 plugin pin; they now name ~0.10.2. Comments only, no behaviour. 2. No npm/npx at tool-call time (darwin, redblue, memory) - _darwin.mjs ran `npx -y -p @metaharness/darwin@<pin> metaharness-darwin` on every evolve / bench / security_bench call: npm resolved the range each time, it ignored the optionalDependency the CLI ships, and it could run a different darwin than the gepa import in the same plugin. It now resolves like _harness.mjs — installed copy satisfying the pin, then the one-time versioned darwin-cache-<pin> that gepa already shares, then `process.execPath <abs bin>` with shell:false. - _redblue.mjs always npm-installed into redblue-cache-<pin> on first use, never looking at the installed copy (ruflo 3.42.4 carries 0.1.4). It now checks the installed copy first. The chosen CLI path is realpath'd, because redblue only dispatches when import.meta.url === file://argv[1]; a pnpm-symlinked package dir (or a cache base under /tmp on macOS) otherwise makes it exit 0 having done nothing. Its spawn moves to process.execPath with shell:false: the argv carries MCP-supplied --config/--out/--in, which under the previous `shell: win32` went through cmd.exe unquoted. - audit-list / audit-trend / oia-audit / similarity ran `npx @claude-flow/cli@latest memory ...` — an npm-registry resolution per call (the check _harness.mjs already removed for its own npx path), and possibly a different CLI version than the server that spawned the tool (#3306; cold-cache npx cost #3145/#3154). Without registry access oia_audit reported persisted:false and audit_list silently showed 0 records. They now call _invoke.runRufloCli(): process.execPath + bin/cli.js of the @claude-flow/cli that ships the plugin (<cli>/plugins/ruflo-metaharness, or v3/@claude-flow/cli in a checkout). A candidate needs dist/src/index.js next to bin/cli.js (mcp-launch.cjs's unbuilt-checkout guard) plus a package.json named @claude-flow/cli (a new check, so an unrelated parent directory can never qualify). CLI_CORE=1 still opts into `npx @claude-flow/cli-core@alpha`, and a layout with no usable local CLI still falls back to `npx @claude-flow/cli@latest`. - drift-from-history.mjs: drop its unused CLI_PKG constant. The two new lookups end in a spawn of a package bin with MCP-supplied argv, so they pass findLocalPackageDir(..., { fromCwd: false }) and search only the ruflo install that owns the plugin — never the tool's cwd or its ancestors. The pre-existing metaharness lookup keeps its cwd walk unchanged. Tests (both hermetic, published layout, stub packages, npm/npx PATH trap): - test-pin-alignment.mjs (+ smoke 17z74a): 3 passed / 9 failed on main, 12 / 0 here. - test-no-tool-time-npx.mjs (+ smoke 17r2): 7 passed / 19 failed on main, 26 / 0 here. It covers both darwin success branches (installed copy and the versioned cache), the memory round trip through the shipping CLI, the kept contracts (CLI_CORE, npx fallback, a bin/cli.js that is not @claude-flow/cli, darwin absent -> degraded exit 0), and the hardening above: a darwin/redblue in the tool's own cwd node_modules is never executed, a cache dir with package.json but no bin gets one reinstall instead of degrading forever, and neither helper spawns the resolved bin through a shell. ADR-150 is unchanged: no static @metaharness import is added, no new dependency, and every unresolvable path still returns the same {degraded:true, reason} with exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTedGmPKpEdArVBMMn3JaP
…ess (#3376) `embeddings init` skipped the ONNX download whenever the optional `@claude-flow/embeddings` package was absent — which is every plain `npm i -g ruflo`, since that package is not declared in the CLI's package.json at all. It then slept 500ms faking progress, printed a dimmed "(Skipped — …)" note on the spinner's tail, and fell through to the success path: "Embedding subsystem initialized", a settings table asserting the model and 384 dimensions, a `.claude-flow/embeddings.json` recording a `modelPath` that contains no model, and exit 0. This is the command `memory/embedding-policy.ts` points users at in its `[no-stub]` error, so the documented repair for broken embeddings reported success and changed nothing. - A download that was requested and did not happen now ends in `spinner.fail` + `output.printWarning` + `{ success: false, exitCode: 1 }`, per the CLI command contract in docs/MCP_CLIENT_GUIDE.md. `--no-download` still exits 0: no download was asked for. - `embeddings.json` gains additive optional `modelDownloaded` and `modelSkipReason` keys. Every pre-existing key keeps its name and meaning, so older readers are unaffected. - Both `setTimeout(r, 500)` fake-progress delays are gone. The second, in `embeddings models --download`, also reported `success: true` for a skip; it now fails the same way. Scope is items 1-3 of the issue. The missing `status`/`download` subcommands (item 4) are deliberately left alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTedGmPKpEdArVBMMn3JaP
…3360) bridgeGetEntry() consults a per-process TieredCache before the database, and each process only invalidated that cache on its own writes. Several MCP servers routinely share one store (Claude Code, Codex and Grok each run `mcp start` against the same .swarm/), so once server B had read a key, an update or delete made by server A never reached B: B kept returning the old value (found: true) until its cache entry expired (5-minute TTL by default, not extended by reads). Before a cached read, check SQLite's PRAGMA data_version (prepared once per handle), which changes only when a different connection has committed to the file, and clear the cache when it moved. Own writes don't trip it, so hits stay cheap (~1 us added). Native better-sqlite3 handles only: agentdb's sql.js fallback works on a private image that never sees other processes' commits, so there is nothing to detect and it is skipped (isWasm / no inTransaction) rather than probed. A handle that can't prepare the check is not probed again. A check that cannot validate the store on one read clears the cache for that read, leaves the baseline alone and is retried — both when it throws (e.g. SQLITE_BUSY) and when it answers without a usable number, since neither is evidence the store is unchanged. The check is fail-open toward the caller: it can never break the read it guards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTedGmPKpEdArVBMMn3JaP
…chet failure (#3208) The ratchet runs vitest with `--reporter=json --outputFile=…`, so nothing vitest prints reaches the job log and the only record of an unexpected failure is the file name. Recovering the assertion means downloading the `test-results-ubuntu-latest` artifact — and a rerun replaces it, so after a rerun attempt 1's assertion is gone for good. The failure messages are already in the report this process parsed. Printing them costs nothing, needs no new artifact, and survives a rerun, because job logs are per-attempt. `formatUnexpectedFailures()` is exported and pure so it unit-tests alongside `evaluateTestReport()`. Every field it reads is optional by design: a file-level abort ("No test suite found in file") carries `message` and no assertions, and a report from another reporter version may carry neither. In both cases the filename still prints, exactly as before, and the PASS path is untouched. Verified against a real failing run rather than a fixture: replaying run 35521170139's `vitest.json` through this script turns + v3/@claude-flow/cli/__tests__/memory-search-scores-3327.test.ts into that same line plus ✗ memory_search score contract (#3327 Finding B) preserves standard fallback on import failure AssertionError: expected [ { key: 'a', …(5) } ] to deeply equal [ { key: 'a', …(4) } ] which is the line that would have identified the failure in #3208 without a second run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTedGmPKpEdArVBMMn3JaP
Bumps [agentic-flow](https://github.com/ruvnet/agentic-flow) from 2.1.2 to 2.1.3. - [Release notes](https://github.com/ruvnet/agentic-flow/releases) - [Changelog](https://github.com/ruvnet/agentic-flow/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/ruvnet/agentic-flow/commits/v2.1.3) --- updated-dependencies: - dependency-name: agentic-flow dependency-version: 2.1.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
# Conflicts: # docs/dream-cycle/LEDGER.md
…age vitest alias Vite alias keys prefix-match, so the bare '@claude-flow/security' alias rewrote the safe-git subpath import to '.../src/index.ts/safe-git' (ENOTDIR) and failed about ten cli test files once #3382 added the import. Co-Authored-By: RuFlo <ruv@ruv.net>
Pin the three standalone-published leaves exactly (memory alpha.26 in both dependencies and optionalDependencies, neural alpha.10, cli-core alpha.6) and hand-edit the lockfiles minimally: pnpm importer specifiers plus one new memory registry snapshot, and the root package-lock cli-core/neural entries. Co-Authored-By: RuFlo <ruv@ruv.net>
Co-Authored-By: RuFlo <ruv@ruv.net>
…re-sign manifests Regenerated on a clean, fully built tree, so all 117 entries verify on each OS (no dist entries carried forward unattested). Regen was run per OS with the manifest os field set explicitly, since regen.mjs stamps the runner's OS. Co-Authored-By: RuFlo <ruv@ruv.net>
This was referenced Sep 26, 2026
Closed
ruvnet
added a commit
that referenced
this pull request
Sep 27, 2026
…-run-gap notes Records the 2026-09-26 batch merge clearing #3330/#3350/#3352/#3357/#3385/#3395 (GitHub-verified via pull_request_read), #3378's landing via batch PR #3443 + npm publish #3452, and a new confirmed no-run gap on 2026-09-23/25/26. Appends tonight's row (#3475/#3476, ACCEPT-scoped). Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_017NSCBLZggg81jUaa8RvZV7
ruvnet
added a commit
that referenced
this pull request
Oct 9, 2026
…-run-gap notes Records the 2026-09-26 batch merge clearing #3330/#3350/#3352/#3357/#3385/#3395 (GitHub-verified via pull_request_read), #3378's landing via batch PR #3443 + npm publish #3452, and a new confirmed no-run gap on 2026-09-23/25/26. Appends tonight's row (#3475/#3476, ACCEPT-scoped). Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_017NSCBLZggg81jUaa8RvZV7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Integration train for 3.46.0: 29 reviewed PRs plus the #3359 fix, validated together, with the three unbundled leaves published standalone first.
MERGE WITH A MERGE COMMIT, NOT SQUASH. The included PR heads are ancestors of this branch; a squash would leave all of them open.
Included (merged into this branch, each read in full by a security reviewer)
Standalone leaf publishes (not bundled, so they would otherwise reach nobody)
Published and registry-verified (integrity matches the local pack) before this PR:
@claude-flow/memory3.0.0-alpha.26: dream(memory): #3351 fix AgentDBAdapter.store() same-key upsert orphaning (evaluated, ACCEPT-scoped) #3352 + fix(memory): preserve distinct namespace/key tuples during upsert #3359 together. dream(memory): #3351 fix AgentDBAdapter.store() same-key upsert orphaning (evaluated, ACCEPT-scoped) #3352 alone is the data-loss version.@claude-flow/neural3.0.0-alpha.10: dream(performance): #3329 wire MoE gate's load-balance loss into the actual weight update (evaluated, ACCEPT-scoped) #3330 (MoE load-balance loss, default-on).@claude-flow/cli-core3.7.0-alpha.6: dream(security): #3384 add PATH/search-path family to validateEnv() denylist (evaluated, ACCEPT-scoped) #3385 (validateEnv PATH-family denylist).CLI pins all three exactly;
v3/pnpm-lock.yamlandpackage-lock.jsonhand-edited minimally (proved:pnpm@8 --frozen-lockfilepasses on the new lock, fails on the old).Fixed during validation
vitest.config.tsaliased@claude-flow/securityby prefix, so #3382's new@claude-flow/security/safe-gitimport resolved to.../src/index.ts/safe-git(ENOTDIR) and failed ~10 cli test files. #3382 was red for this reason, not a flake. Subpath alias added ahead of the bare alias.Validation
init-memory-package-resolver-2545x2,policy-runtimex1) fail identically on cleanmainin the same environment.Release-note items
embeddings init/models --downloadnow exit 1 when@claude-flow/embeddingsis missing.sonaTracksGlobalchanges from boolean to null.--request-timeout-ms; fix(witness): add strict hash-drift verification for CI #3424 adds--strict; fix(doctor): report agent-browser runtime compatibility #3441 adds a doctor check.globalFishermoves ~100x slower per call after the EMA direction fix.Known gaps
core.fsmonitor;core.hooksPath,filter.*,diff.external,core.sshCommand,GIT_CONFIG_*and ~15 rawgitcallers remain. dream(security): #3384 add PATH/search-path family to validateEnv() denylist (evaluated, ACCEPT-scoped) #3385's denylist omitsGIT_CONFIG_*andHOME.@claude-flow/mcp@3.0.0-alpha.9still ships the shell-stringgithub-safe.js(Coordinated disclosure: github-safe.js helper interpolates argv into a shell string passed to execSync (AS-S001) #3411). Source is already fixed;mcpis a bundled package, so a standalone publish needs an explicit decision.Deliberately NOT included
#3333 (would deny every Codex worker MCP call:
maxConcurrency:1with no concurrency supplied), #3268 (WebSocket fails open with no tokens), #3339 (breakinghive-mind_init), #3393, #3369 and #3387 (need signing / lockfile work), #3425 and #3428 (arbitrary-pathdbPath; ancestor-directory CLI execution), #3432 #3433 #3435 #3437 #3440 #3442 #3341 #3373 (change requested), #3438, #3295 #3296 #3298 #3270 #3307 #3309 #3310.🤖 Generated with RuFlo