Skip to content

fix(statusline): resolve the real CLI bins so delegation stops falling to stale npx - #28

Merged
pacphi merged 1 commit into
mainfrom
fix/statusline-cli-bin-resolution
Jul 17, 2026
Merged

pacphi merged 1 commit into
mainfrom
fix/statusline-cli-bin-resolution

Conversation

@pacphi

@pacphi pacphi commented Jul 17, 2026

Copy link
Copy Markdown
Owner

The symptom

A machine running ruflo 3.32.2 — whose getSecurityStatus is genuinely fixed, and whose kit CVE overlay had therefore correctly retired itself — still rendered the fabricated ⚠ 1 CVE and a perpetually stuck 🛡 scanning…. Nothing was scanning; there was no process to wait on.

Root cause

Upstream's resolveCliBinCandidates probes filenames no shipped package ships:

  • ruflo's bin map is {"ruflo": "bin/ruflo.js"} — there is no ruflo/bin/cli.js, ever
  • @claude-flow/cli does ship bin/cli.js, but as ruflo's nested dependency, never the top-level global install the candidates probe

So 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 as 1 CVE and IN_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-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 (via a new rufloRealCliBins footer helper reusing rufloFindRufloRoot'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)

  • Before: fresh render (cache deleted) → 🛡 scanning… + CVE insight — proving the fabrication came from the delegation path, not stale cache
  • After injection: fresh render → 🛡 ✓, no CVE, footer segments (Agentic QE / SONA / RL) intact; delegation hits the real 3.32.2 bin
  • node --check syntax 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 sync heal 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):

  • injected even when the security overlay is retired — the exact state that bit us
  • prepends real bins ahead of upstream candidates at run time — executes the patched file; machine-independent assertions (membership + order)
  • inert on a template without resolveCliBinCandidates — the typeof guard
  • idempotency test extended to count ruflo-bin markers

The 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.

…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
pacphi merged commit 6692116 into main Jul 17, 2026
11 checks passed
@pacphi
pacphi deleted the fix/statusline-cli-bin-resolution branch July 17, 2026 14:50
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant