fix(statusline): resolve the real CLI bins so delegation stops falling to stale npx - #28
Merged
Merged
Conversation
…g to stale npx
A machine running ruflo 3.32.2 — whose getSecurityStatus is fixed, and
whose kit CVE overlay had therefore correctly retired itself — still
rendered the fabricated "⚠ 1 CVE" and a perpetually stuck "🛡 scanning…".
Upstream's resolveCliBinCandidates probes filenames no shipped package
ships: ruflo's bin map is {"ruflo": "bin/ruflo.js"} (no cli.js), and
@claude-flow/cli — which does ship bin/cli.js — is ruflo's nested
dependency, never a top-level global. Every candidate misses, always, so
the statusline silently falls through to `npx --prefer-offline
@claude-flow/cli`: whatever stale version the npx cache holds. Here that
was 3.28.0, whose hardcoded {totalCves: 3, cvesFixed: 2} renders as
"1 CVE" and IN_PROGRESS ("scanning…") forever — there is no scan running.
The retirement gate misfire is the instructive part: the gate probed the
globally installed CLI (genuinely fixed) while the render path executed a
stale npx copy the gate never saw.
Fix: a ruflo-bin wrapper block (same function-declaration-hoisting
mechanism as the security overlay) prepends bins verified to exist on
disk — bin/ruflo.js under the global/local ruflo root, plus the nested
@claude-flow/cli/bin/cli.js — and keeps upstream's candidates as the
tail. Deliberately NO retirement gate: the wrapper only prepends
existing paths and falls back to upstream's list on any error, so on a
fixed upstream it converges to the same delegation instead of fighting
it. A gate would be one more proxy-probe that can misfire.
Verified live: with the wrapper injected, a fresh render delegates to the
real 3.32.2 bin and shows "🛡 ✓" with no CVE; the two new tests fail
without the fix (missing wrapper; candidates stay ['/orig']).
pacphi
added a commit
that referenced
this pull request
Jul 17, 2026
) npx envs (<npm-cache>/_npx/<hash>/) are snapshots keyed by requested spec: once `npx @claude-flow/cli` caches a version, --prefer-offline serves that copy forever — upgrading the global install never touches it. The statusline/hook npx fallbacks execute these verbatim, which is how a machine running a fixed ruflo 3.32.2 kept rendering the fabricated CVE counter from a cached 3.28.0 (#28): six such envs held ~6.4 GB of retired code spanning ruflo 3.10-3.28 and agentic-qe 3.11.5. New `npx` status row + sync heal automates what #28 remediated by hand. The prune rule is conservative by construction — a miss fails safe as "not pruned", never a wrong prune: - every package the env is keyed to must be kit-managed (ruflo, @claude-flow/cli, agentic-qe); an env we can't fully judge is exempt - each needs an installed global baseline (@claude-flow/cli resolves from its NESTED location under ruflo — the same layout fact behind the #28 bin fix; there is never a top-level global copy) - only a cached copy STRICTLY older than its baseline counts; equal or newer stays, since a current cache is what a pre-install machine's npx fallback runs Runs on the `npx` row or after `versions` upgrades — an upgrade is precisely what turns a previously-current cache stale. The cache dir is resolved from npm_config_cache or platform defaults without spawning npm; a custom userconfig cache path is missed, which only means an empty scan. Verified end-to-end with a synthetic stale env planted in the real cache: status detects (warn row), sync --dry-run plans it, the heal prunes exactly that env (current and foreign envs untouched), and status converges to ok.
pacphi
added a commit
that referenced
this pull request
Sep 27, 2026
…live (#246) * docs(plan): Branch 4 upstream watch live code-level plan * feat(upstream): split lastCheckedAt from conformance-verified dates (schema 6) The registry now records two dates. lastCheckedAt is the last state re-read (issue states, released versions) and is the tests' clock; lastVerifiedAt is the last date every constraint's retest was re-run. The staleAfterDays rule keys on lastCheckedAt, a per-constraint retest moves only that constraint's nextRetestAt, and a re-check past a retest date reports stale evidence while the registry stays valid. Schema 5 -> 6: lastCheckedAt is required, a date, never in the future and never before lastVerifiedAt. Data migrated in place (lastCheckedAt 2026-09-27; lastVerifiedAt left as recorded). The watch report, its plain-text header, the idle ledger line and the hook healing plan's upstream block carry lastCheckedAt. No test pins a literal plan digest. docs/UPSTREAM-WATCH.md now separates re-checking from re-verifying. * feat(upstream-watch): read fixing pull requests and tag containment createFetcher() gains two read-only calls. fixingChanges(id) asks the GitHub GraphQL API what closed a thread and keeps only pull requests merged into the repository's default branch (falling back to the ClosedEvent's closing commit); an unmerged closing reference such as ruflo#3373, an off-branch merge or a hand close yields no change. contains(repo, refs, sha) compares each tag spelling with the commit: behind or identical is contained, ahead or diverged is not, a tag missing for every spelling is unknown, and any other failure (rate limit, auth, 5xx, an unexpected status) throws so it surfaces as "Could not check" rather than "not contained". Fixtures recorded read-only on 2026-09-27: ruflo#3167/#3194/#3415 are closed by PRs #3434/#3421/#3423, each contained in v3.46.0. * feat(upstream-watch): confirm releases from the merged fixing change Without a recorded minVersion, a release now counts only when its tag contains the merged pull request (or closing commit) that fixed the thread. collect() asks the fetcher for the fixing changes and walks the releases after the fix, oldest first and at most five, stopping at the first tag that contains the change or has no tag to check. releaseState() returns released (confirmed, with the change and tag), not released (every checked release lacks the change), or unconfirmed (no fixing change, or no tag). The new report group "Released, fix not confirmed" holds the unconfirmed ones; they get no dispatch and no ledger line. The released ledger line drops candidate= and carries pr=<n> or commit=<sha7>. A confirmation failure is "Could not check". Release gates may carry tagPattern (containing {version}); the four openai/codex gates use rust-v{version}. The loader now also rejects an unknown key in a release gate, matching the schema's additionalProperties: false. Tests changed on purpose: the "candidate" test is replaced by the confirmation tests; the merged-PR test (ruflo#2986) asserts release-unconfirmed without a confirmation and released-actionable with one; fixtureFetcher() gains fixingChanges/contains stubs. * feat(upstream-watch): count AgentDB fixes released only when Ruflo bundles them ak gets AgentDB through Ruflo, so an agentdb publish alone must not read as released. A release gate may now carry bundledBy, the carrier chain from the package ak installs down to the parent of the gated package; the three ruvnet/agentdb gates use ["ruflo", "@claude-flow/cli"]. fetcher.bundled(chain, name) takes npm latest of the first carrier (the newest Ruflo, hence the newest inside the support window), walks each manifest's dependencies then optionalDependencies, and resolves every range to its highest published match by semver (maxVersion accepts npm's bare string for one match and an array otherwise). A missing dependency or a range with no match is reported, not thrown; any other npm failure is "Could not check". collect() resolves each chain once. releaseState() then counts the fix released only when the carrier's resolved version is at or after the fixed one; the released version is the carrier's (what ak installs) and fixedVersion the package's. Live today: ruflo 3.46.1 -> @claude-flow/cli 3.46.1 -> agentdb 3.0.0-alpha.20; agentdb#26/#27/#28 remain open and waiting. * feat(upstream-watch): record the ledger issue #243 watchPolicy.ledger now requires issue (integer >= 1); the registry records 243, the pinned and locked "Upstream watch" issue. The routine prompt opens that issue directly instead of searching by title. * feat(upstream-watch): let a reviewed thread leave the reply queue A new history event, reviewed, records that the maintainer read every comment up to that day and none needs a reply. It clears earlier comments from "Needs our reply" (and the reply ledger lines) the way a status change does, never later ones, and does not re-date reopened or retire-proposed lines. ruvnet/ruflo#3153 records it for sparkling's four comments. * feat(upstream-watch): register every upstream thread user-facing docs cite The citation guard now also scans README.md and every top-level docs/*.md guide (userFacingDocs); CLI help already lives in src/. ADRs, audits, plans and research sit in subfolders and are never scanned; three top-level history files are exempt by name (USER_DOC_EXEMPT). ruvnet/ruflo#1234 is a placeholder in an example command and joins SYNTHETIC. Registers the 26 threads the docs cite: 17 open as watching (mapped to the doc caveat they back) and 9 closed as retired. Adds the claude-code and opencode dependency policies those threads need. New entries are appended as text in the file's own style; no existing line moved. * chore(upstream-watch): migrate the #213 and #240 upstream remainder into the registry #213 now tracks ruvnet/ruflo#3196, #3446 and #3450, records our 2026-09-27 reply, and names the route/peek API request still to be filed. ruflo#3196's adjustment no longer waits for a unified path: the two stores are deliberate (ruflo#2786), so ak waits for a tested preservation or migration outcome. The threads #213 cites are registered: #2786 and #3195 retired, #3143 and #2889 watched as unmapped. #240's adjustment names agentic-qe#574 as the remaining upstream work and records that #719 was released in 3.14.4 on 2026-09-27; its kit file is src/lib/aqe-readiness.mjs. * chore(upstream-watch): map, retire or query the three stale threads openai/codex#16045 is mapped to the connected host check, which disables each MCP server by name because -c mcp_servers={} is still a no-op (reproduced on codex-cli 0.157.1); the code comment now cites it. openai/codex#16921 is retired: the same request is watched through #17827, which carries the same adjustment; the two duplicate entries now point at #17827 only. ruvnet/ruflo#952's adjustment records that --tools / CLAUDE_FLOW_MCP_TOOLS (3.46.1) narrows only advertised schemas while execution stays registered, so the permissions.deny gating stays; the mcp.mjs comment says the same. * docs(upstream-watch): record Branch 4 decisions and the live watch The audit record gains a Branch 4 decisions section (B4-G1, B4-G2, B4-Q1, B4-Q2, B4-Q3) in the decision format, with implementing commits, guarded by a test. ADR-0041 section 7 now states schema 6's two dates, release confirmation from the merged fixing change, AgentDB gated through Ruflo, the wider citation guard and the ledger issue number. UPSTREAM-WATCH.md says the routine is created after this reaches main and that #240 and #213 are registry entries. The glossary adds Confirmed release and Reviewed thread. * fix(upstream-watch): keep an AgentDB released line stable across Ruflo releases The released line for a bundled fix put the newest Ruflo in version=, so every Ruflo release produced a new line and the routine posted a repeat, breaking the rule that an exact recorded line is never acted on twice. version= is now the fixed agentdb version; the carrier version stays in the report (carrierVersion and the basis). * fix(upstream-watch): name the first release not ruled out as unconfirmed When the first release after a fix was shown not to contain it and the next had no tag, the unconfirmed result still named the first release. It now names the first release whose check was unknown (or the first never checked), and the report says "not ruled out" to match. * fix(upstream-watch): report a failed bundle resolution as could not check An AgentDB entry without a recorded minVersion (all three live ones) stayed "Released, fix not confirmed" when resolving what Ruflo bundles failed, contrary to the collect comment. A failed resolution now gives released: null, so the entry is "Could not check" and keeps its thread facts, as the minVersion case already did. * refactor(upstream-watch): define the package and owner/repo patterns once The package-name pattern lived in both the registry validator and the fetcher, and the fetcher repeated the ledger owner/repo check inline. Both are now exported from src/lib/hook-audit/upstream-watch.mjs, spelled as the schema spells them, and a test holds the schema to the same source. The older future-dates test uses the withDocument helper. * fix(upstream-watch): start the release walk when the fixing change merged An issue closed after the release that already shipped its fix left that release out: the walk started at the close time, so the entry read 'no release since the fix' or named a later version. The fixing-change query now reads mergedAt, and both the walk and the classification start at the first fixing pull request's merge. A closing commit keeps the close time, which is when it landed on the default branch. The recorded GraphQL answers were re-recorded read-only with the new query. * fix(upstream-watch): look past the first five releases through the newest The confirmation window never moved: when the first five releases after a fix did not contain it, the entry stayed fixed but unreleased even after a later release shipped it, and parallel release lines can fill those slots. After five releases without the fix the walk now checks the newest release (latest, not a later-published backport); when it has the fix, the releases in between are walked oldest first, so the released version is the oldest containing one and a newer release never changes the ledger line. * fix(upstream-watch): keep the thread when its release confirmation fails A failed fixingChanges or contains call set the entry's error, which discarded the thread already read: the entry fell to 'Could not check' only and check lost its closed, reply and acknowledged lines, which are limited to --since and so were lost for good on a day the routine posted other events. The failure is now recorded on the release (released: null, the error in its basis) and the thread's facts are kept. check --json now lists fetchErrors, and plain check writes each one to stderr as 'Could not check <id>: <error>' so stdout stays ledger lines. * fix(upstream-watch): name #213 and #240 our tracking issues, not issues to migrate Both are already registry entries (relation: tracking), yet the report grouped them under 'Tracking issues to migrate' and the guide and skill still said tracking issues migrate here. The group is now 'Our tracking issues', and the guide and both skill copies say what a tracking entry is. * fix(upstream-watch): keep the partial fix agentic-qe#719 from dispatching #719 was mapped with an adjustment, so its release (3.14.4) produced a released line and a dispatch to remove the temporary busy rule while agentic-qe#574 is still open and #240's own adjustment says it is not yet verified that 3.14.4 clears FsyncFailed under a live lock. #719 is now context only (unmapped, with a note), and #574 drives the dispatch. * docs(upstream-watch): state that a reviewed line covers its whole UTC day History lines carry dates only, so a reviewed line treats every comment of that day as read, including one posted after the review. Say so, and when to record a review so that no comment is missed. * docs(upstream-watch): record that the watch runs on POSIX only On Windows npm is a .cmd file that Node's execFile refuses to start without a shell, so every npm-gated release would read 'Could not check'. A shell is no fix: cmd.exe treats the caret in version ranges passed to npm view as an escape. The routine runs on Linux; the guide and the script header now say macOS and Linux only. * docs(audit): record Branch 4's implementation status and open items The audit record had Branch 4's decisions but no implementation status or open items, and the program plan's Branch 4 checkboxes were all unchecked. Add a Branch 4 status subsection in the voice of Branch 1's, open-items bullets for what is left (routine, #617 rehearsal, #240, unposted drafts, five untriaged stale threads, three released Ruflo items), tick the code item in the plan and annotate the rest. Commit ids in the decisions section are refreshed after the rebase onto main. * docs(upstream-watch): keep the previous checked-at when a thread could not be checked
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.
The symptom
A machine running ruflo 3.32.2 — whose
getSecurityStatusis genuinely fixed, and whose kit CVE overlay had therefore correctly retired itself — still rendered the fabricated⚠ 1 CVEand a perpetually stuck🛡 scanning…. Nothing was scanning; there was no process to wait on.Root cause
Upstream's
resolveCliBinCandidatesprobes filenames no shipped package ships:{"ruflo": "bin/ruflo.js"}— there is noruflo/bin/cli.js, ever@claude-flow/clidoes shipbin/cli.js, but as ruflo's nested dependency, never the top-level global install the candidates probeSo the candidate list resolves to empty, always, and the statusline silently falls through to
npx --prefer-offline @claude-flow/cli— whatever stale version the npx cache holds. Here: 3.28.0, whose hardcoded{totalCves: 3, cvesFixed: 2}renders as1 CVEandIN_PROGRESS("scanning…") forever.The instructive part is the retirement-gate misfire:
upstreamCveCounterFabricated()probed the globally installed CLI (genuinely fixed → overlay retired) while the render path executed a stale npx copy the gate never saw. Gate correct, user still lied to.The fix
A
ruflo-binwrapper block (same function-declaration-hoisting mechanism as the security overlay) prepends bins verified to exist on disk —bin/ruflo.jsunder the global/local ruflo root plus the nested@claude-flow/cli/bin/cli.js(via a newrufloRealCliBinsfooter helper reusingrufloFindRufloRoot's nvm/mise/Windows/custom-prefix logic) — and keeps upstream's own candidates as the tail.Deliberately no retirement gate: the wrapper only prepends existing paths, dedupes, and falls back to upstream's list on any error, so on a fixed upstream it converges to the same delegation instead of fighting it. A gate would be one more proxy-probe that can misfire — the lesson above.
Verification (live)
🛡 scanning…+ CVE insight — proving the fabrication came from the delegation path, not stale cache🛡 ✓, no CVE, footer segments (Agentic QE / SONA / RL) intact; delegation hits the real 3.32.2 binnode --checksyntax gate + rollback path unchanged; injection idempotent (marker counts stay 1)Also remediated on the affected machine (not part of this diff): pruned six stale npx cache envs (ruflo 3.10/3.21/3.25, cli 3.28 ×2, agentic-qe 3.11.5) — 7.2G → 820M, zero copies of the fabricating code remain. Cache-pruning alone would have been temporary; this fix takes npx off the hot path entirely. An automated
ak syncheal for stale npx envs is a possible follow-up.Tests
Extended
tests/kit/statusline.test.mjs(hermetic; the host template now models the broken resolver and prints its candidates so tests observe run-time behavior, not just block presence):resolveCliBinCandidates— the typeof guardruflo-binmarkersThe two detector tests fail without the fix; the inert-guard passes on both (safety net, not detector). Gates: 85 mjs + 43 cjs tests pass, eslint 0, typecheck 0.