fix(cli,plugins): resolve executed optional modules from the tool's own install (replaces #3954; security review in PR body)
Co-Authored-By: RuFlo <ruv@ruv.net>
Leftovers of #3817/#3822 and #3818/#3823.
Numbers from CLI JSON (#3822). JSON.parse('1e999') is Infinity; a count can
also be negative, fractional or absurd. Bounded where read with data/safe.ts
countOf / ratioOf and a new measureOf (finite, within +-1e12):
- secure.ts scanReader: every severity count and the total are at least
the listed findings, so a summary never reads CLEAN beside a listed
finding. A count or total the tool printed but no reader can trust stays
UNKNOWN (SecFindings.unreadable / isTotalUnknown), with the listed
findings only as its lower bound: "critical ?" / "high ≥2 ?",
"? findings (total unreadable)", COUNTS UNREADABLE. securityBand() is the
one rule for the band (views/bar.ts) and the menu badge (menu-style.ts):
it warns on high/critical ("N+" when anything is unknown) and on any
unknown count or total. Threat confidence via ratioOf, composition score via measureOf.
The verdict-exit judgement (#3934, SCAN_FOUND/countsWith) is unchanged.
- Same class swept in perf.ts, mh-lab.ts (labLines cells, gepa chars,
genome ids, receipts), neural.ts, memory-lines.ts,
data/evolve.ts, data/research.ts and data/cli.ts.
Mission resume (#3823). setPaused takes who resumed. The mission-resume
palette entry built inside Claude's console_run (state.control.viaModel)
resumes as 'model': hand-out counts are kept and an advisor stop-the-line
is not re-opened, so an exhausted task gets no new billed hand-out. The
person's resume (page buttons, palette, a typed /ruflo run) still gives
every task its hand-outs back.
Co-Authored-By: Ruflo & AQE
Claude-Session: https://claude.ai/code/session_01CGoza3gyxBjpaiMPhGHcHs
pinToUserHelpers() only recognised the #1943 probes, the exact git-root
statusline and the git-root ESM import without argv.splice, so
`init --upgrade --settings` on the home directory left older shipped forms
that always run the opened project's helper:
- pre-#1943 `sh -c 'exec node "${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/<x>" <sub>'`
and the Windows `cmd /c node %CLAUDE_PROJECT_DIR%/.claude/helpers/<x> <sub>`;
- ADR-059 git-root node -e forms (CJS and ESM, with argv.splice);
- relative `node .claude/helpers/<x> [sub]` hooks and statusline.
It now re-pins any command whose target is a known ruflo helper under
.claude/helpers reached through CLAUDE_PROJECT_DIR, %CLAUDE_PROJECT_DIR%,
`git rev-parse --show-toplevel` or a relative path, keeping the subcommand.
Commands with redirects/chaining, other scripts, or already pinned to HOME
are left alone.
executeUpgrade() created a missing settings.json with
generateSettings(DEFAULT_INIT_OPTIONS), whose targetDir is the module-load
cwd; it now passes the upgrade's targetDir.
Tests: one case per legacy form, a user-authored/already-pinned case, an
upgrade-merge case, and an executeUpgrade missing-settings case.
Settings written to ~/.claude/settings.json apply to every project the user
opens. The #1943 probe ran `$CLAUDE_PROJECT_DIR/.claude/helpers/<script>`
whenever that file existed, so in user-level settings the opened project
decided which helper code ran.
helper-commands.ts now owns the hook/statusline command forms:
- project-level settings keep the #1943 project-first probe, unchanged;
- user-level settings (init target is the home directory) get commands
pinned to $HOME/.claude/helpers with no project lookup;
- `init --upgrade --settings` on the home directory re-pins the probe and
git-root forms ruflo generated earlier and leaves user-authored commands.
The #1943/#1948 fix invariants now point at helper-commands.ts.
Test: settings-user-level-helpers.test.ts (8 cases).
- smoke-plugin-registry-signature.mjs: follow the verifier rename to
registrySignatureFailure and require the fail-closed call site
`if (failure) return this.fallbackToBuiltIn(`, keeping full #1922 coverage.
- verifyEd25519Signature verifies in RFC 8032 strict mode, and
isPlaceholderSigningKey rejects every small-order or off-curve key, not
only the all-zero one.
- An unverified registry claims no official plugins and no verified
authors; MCP transfer_plugin-* results carry the registry status.
- Tests no longer assume the shipped trust anchor stays a placeholder.
- Document the strict-mode asymmetry for unlisted packages in --help.
The live plugin registry CID is unsigned, so discovery always fell back to
the built-in plugin list, whose entries all claim trustLevel "official".
`plugins install` passed that claim on as registry trust, which runs npm
lifecycle scripts and skips the permission gate (#3557) for any listed
plugin, with nothing actually verified.
- Discovery reports `verified` + `unverifiedReason`; only a registry whose
signature verifies against the pinned key keeps its trust labels. The
built-in fallback and a registry fetched with requireVerification off are
reported with every entry as 'unverified'.
- A placeholder pinned key (missing, malformed, all-zero) is never used to
verify: the all-zero key is small-order and accepts a forged signature.
- CLAUDE_FLOW_STRICT_PLUGINS=true (ADR-145): no fallback to the built-in
list, and `plugins install` refuses unless --allow-unverified is passed.
- `plugins install` passes registry trust, permissions and checksum only
for a verified registry entry; `plugins list`/`info` show registry status.
- PluginIntegrityVerifier (@claude-flow/security) skips placeholder anchors,
such as the all-zero one in trust-anchors.json, and verifies in RFC 8032
strict mode, which rejects small-order keys.
A project-local install lives at <project>/node_modules/@claude-flow/cli, so the
8-level walk-up from import.meta.url used by the testgen/metaharness locators and
doctor crossed node_modules and reached <project>/plugins/ruflo-*, i.e. code the
opened repository supplies. ownInstallAncestors()/ownPluginDirs() now stop at the
first node_modules directory. Source-checkout dev layouts are unaffected.
The @claude-flow/embeddings hunk is dropped from this change: that package cannot
ship from main until its alpha branch is reconciled, so it needs its own change.
Co-Authored-By: RuFlo <ruv@ruv.net>
The CLI, two MCP tools and two plugin scripts imported or ran code found
relative to the cwd, which is the opened project:
- doctor (metaharness check), `metaharness` command and metaharness MCP
tools: plugins/ruflo-metaharness/scripts under the cwd;
- testgen MCP tools: plugins/ruflo-testgen/scripts under the cwd, reached
on every published install because that plugin is not bundled;
- init: <cwd>/node_modules/@claude-flow/codex;
- embeddings: <cwd>/node_modules, <cwd>/../node_modules and a fixed
/workspaces/claude-flow path for agentic-flow;
- ruflo-ruos ledger: the CLI's mcp-client resolved from the cwd;
- cost-tracker compact.mjs: @claude-flow/integration resolved from the cwd.
Each now resolves from the tool's own install (module location /
createRequire(import.meta.url)), plus the user's marketplace checkout for
the plugin scripts. The project remains the data/scoring target.
Test: optional-module-resolution-trust.test.ts (10 cases, all fail on main).
On a project whose policy state is large, every `ruflo mcp exec` waits on the
policy state lock and some calls fail with policy-state-lock-timeout
(#3164, #3892). Mission create made 15 sequential task_create calls; one
transient failure aborted the whole mission, cleanup cancels were
fire-and-forget (orphan pending tasks while the page said they were
cancelled), the CLI mission record was left planned, and the reason was never
shown. The outcome of a run that reports for itself (mission create, ADR
writes) never reached console_state.lastResult. resultOf also read the
`Parameters:` object the CLI prints before any `Result:` line as the answer.
- data/mcp-run.ts: read only after the CLI's own `Result:` line (string-aware
bracket matching); the failure reason from data/failure.ts. Transient only
on exactly policy-state-lock-timeout, the caller's own structured time
limit, or a non-zero exit with stdout and stderr both empty. Any answer,
any other error line (a denial mentioning a timeout, command not found)
and any host rejection are final.
- mission-create.ts: retries after 1, 2, 4 s. task_create has no request id:
after each wait, and once more after the last failed try (a lost answer),
the store is read for exactly this step's task (description and both
tags carry the new mission id; a 5 min skew window); one is used, several
stop the chain, an unreadable store stops a retry. Strict time: no call,
wait or read starts after its deadline (3 min, cleanup 2 min more), each is
cut to the time left, and a call the host never answers is let go then.
One create at a time per console; unique request ids. On abort: cancel
every open task of the mission in the store (not only acknowledged ones),
cancel the mission record (again at the revision a conflict names; also
when mission_plan is refused), then READ BACK the store and the mission
observation and report what they show, and what was left for lack of time.
- ActionSpec.run may answer a RunReport; the runner keeps it as the console
outcome when the run ends, unless the outcome slot was replaced meanwhile
(another action reported; then it goes to the Events feed). The
verdict-exit judgement in runner.ts is unchanged. Mission create and ADR
init/propose/status writes answer theirs.
- tests: real `ruflo mcp exec` transcripts (ruflo 3.56.1) as fixtures.
Co-Authored-By: Ruflo & AQE
Claude-Session: https://claude.ai/code/session_01CGoza3gyxBjpaiMPhGHcHs
vitest applies queued doMock/doUnmock entries for one path in resolve order,
so an afterEach doUnmock could win over the next test's doMock and the
stale-lock retry test saw the real agentdb (CI: 0 fake instances).
Rebased onto main, where ControllerRegistry and AgentDBBackend build AgentDB
inside withAgentdbLockRecovery(); useHostSqliteDriver() now runs inside that
closure right after each `new AgentDB`, so the instance a stale-lock retry
creates also opens its handle with getHostSqliteDriver(). Tests drive the
retry path for both sites.
@claude-flow/memory 3.0.3 was published by 3.56.3 without the driver, so
this ships as 3.0.4 and the CLI pins 3.0.4. The #3693 memory-owner test
now reports a skip when no native addon is available instead of passing.
The root CI job resolves the published @claude-flow/memory 3.0.2, which does
not route AgentDB through its own driver; there the owner must stay agentdb.
Follow-up to the AgentDB driver change. shared-sqlite.ts (#3693/#3883)
resolved better-sqlite3 from agentdb's location so CLI handles share
AgentDB's native copy. With AgentDB now opened through
@claude-flow/memory's better-sqlite3, that put graph-edge-writer, the
memory-initializer repair paths, backup restore, distillation, tuning and
the attestation log back on agentdb's nested 11.x: `hooks post-task` still
loaded both addons (and aborted on Node 24), and mixing copies on one file
can delete the -wal/-shm sidecars under AgentDB's live handle.
- @claude-flow/memory exports getHostSqliteDriver(), the one source of the
constructor; useHostSqliteDriver() opens AgentDB with it and reports
whether it replaced the loader.
- shared-sqlite resolves better-sqlite3 from @claude-flow/memory when the
installed memory carries agentdb-native-driver.js, else from agentdb as
before, so either memory version keeps one copy per process.
- @claude-flow/memory 3.0.3, CLI pin 3.0.3 (leaf-publish gate).
Tests: identity between AgentDB's handle constructor and
loadBetterSqlite3(); #3693 owner model for the memory holder and the
pre-3.0.3 fallback; both AgentDB construction sites; routing against a
real nested copy; a drift guard on agentdb's initializeDatabase contract.
On Node 24.19+, `memory search`, `memory_store` and the AgentDB MCP tools
abort intermittently with `Assertion failed: (env) != nullptr` in
node::RemoveEnvironmentCleanupHook, from Statement::~Statement() in
agentdb/node_modules/better-sqlite3 (exit 134 / "Connection closed").
agentdb 3.0.0-alpha.x pins better-sqlite3 ^11.8.1. 11.x has no Node 24
prebuild, so npm compiles it against the local Node headers, and 24.19+
headers carry a partial ObjectWrap cleanup-hook backport
(nodejs/node#65446) that aborts when a Statement is collected outside an
Environment. Our own better-sqlite3 12.x ships a Node 24 prebuild built
before that header change. The `better-sqlite3` override in the published
package.json files does not reach agentdb: npm ignores overrides in a
dependency, and `npm i -g ruflo` still installs the nested 11.10.0.
ControllerRegistry and AgentDBBackend now replace the AgentDB instance's
initializeDatabase() so its handle is opened with this package's
better-sqlite3; AgentDB's own loader stays as the fallback and forceWasm
is untouched.
CI: "Test Suite (ubuntu-latest)" failed on 1a5c8b83 -- root-caused, fixed.
ci-test-ratchet's summary showed an opaque `Error: STACK_TRACE_ERROR` for
"scales near-linearly with node count..." Reproduced the exact CI
invocation locally (CI=1 node node_modules/vitest/vitest.mjs run ... from
repo root, the same command scripts/ci-test-ratchet.mjs spawns, after
`npm install` at root) and got the real error: the legacy O(n^2)
algorithm's warmup+3 trials at n=1600, for both algorithms at both sizes,
exceeded vitest's default 5000ms per-test timeout on this invocation path
-- the same failure class PR #3754 (2026-10-05) hit and fixed the same way.
Fix: explicit 30000ms timeout on the test, trial count 3->2 to trim real
wall-clock cost further. Re-verified against the exact CI invocation:
4/4 tests pass, ~3s total (comfortably inside the new budget). Measured
growth ratios in this run are consistent with the range already disclosed
in the gist/issue/PR -- no change to the reported evidence, only to the
test's own timeout margin.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01JL2GEEWf9ezZo1X1wQ2Ls6
fallbackLouvain()'s local-moving loop rebuilt a communityTotal map via a
full scan of every node, once per node, once per pass (O(n^2) per pass),
instead of maintaining it incrementally like networkx's Stot/python-louvain's
status.degrees (O(1) per accepted move). loadRuVector() never resolves real
native acceleration against any published ruvector/@ruvector/wasm version,
so this fallback is the only community-detection path that ever runs,
reachable via the shipped `claude-flow analyze modules` command.
New test file proves byte-identical correctness against a frozen copy of
the pre-fix algorithm plus a complexity-growth regression guard; a
standalone tsx probe shows per-N speedup growing 2.1x->37.5x as N grows
8x, the signature of a complexity-class fix. Full @claude-flow/cli suite
and tsc --noEmit byte-identical to baseline outside the new test file.
Evaluated: ACCEPT. Independently adversarially reviewed: CONFIRMED, no
blocking caveats. Darwin skipped (binary correctness-preserving fix, no
tunable parameter).
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01JL2GEEWf9ezZo1X1wQ2Ls6