Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions src/commands/status.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { scanRvf } from '../lib/rvf.mjs';
import { registry, syncBlocks } from '../lib/blocks.mjs';
import { loadKitConfig } from '../lib/config.mjs';
import { driftReport, selfDrift } from '../lib/versions.mjs';
import { upstreamCveCounterFabricated, fixStatusline } from '../lib/statusline.mjs';
import { drift as ruvnetBrainDrift } from '../lib/ruvnet-brain.mjs';
import { readJson } from '../lib/settings.mjs';
import { have } from '../lib/exec.mjs';
Expand Down Expand Up @@ -261,10 +262,35 @@ export async function collect({ pkgRoot, cwd = process.cwd() }) {
// statusline footer (project scope)
const sl = paths.projectStatusline(cwd);
if (fs.existsSync(sl)) {
const hasFooter = fs.readFileSync(sl, 'utf8').includes('ruflo-seg:BEGIN');
rows.push(row('statusline', hasFooter ? 'ok' : 'warn',
hasFooter ? 'activation footer present' : 'statusline present but footer missing',
hasFooter ? null : 'sync re-injects the footer'));
const slSrc = fs.readFileSync(sl, 'utf8');
const hasFooter = slSrc.includes('ruflo-seg:BEGIN');
// Drift is "would a sync CHANGE this file?", which fixStatusline's dry run answers
// exactly. A marker-presence test alone cannot see CONTENT drift: after a kit upgrade
// revises the footer or the security overlay, the marker is still there, this row
// reports 'ok', and — because sync builds its plan from rows carrying a `fix` — the
// re-injection never runs and the stale block survives indefinitely. Observed live:
// an updated overlay silently failed to land for exactly this reason.
let wouldChange = !hasFooter;
try { wouldChange = fixStatusline(cwd, { dryRun: true }).applied; } catch { /* keep marker fallback */ }
rows.push(row('statusline', wouldChange ? 'warn' : 'ok',
wouldChange
? (hasFooter ? 'injected blocks are out of date' : 'statusline present but footer missing')
: 'activation footer present and current',
wouldChange ? 'sync re-injects the footer' : null));
// The CVE-counter overlay is tracked SEPARATELY from the footer: a footer-only
// check reports 'ok' while the statusline still renders ruflo's fabricated
// "⚠ 3 CVEs" (hardcoded totalCves, cvesFixed from a file count). Only warn while
// the upstream defect is actually present — once ruflo fixes getSecurityStatus
// the overlay is intentionally absent, and this row must go quiet on its own
// rather than nag for a patch that is no longer wanted.
if (upstreamCveCounterFabricated()) {
const patched = slSrc.includes('ruflo-sec:BEGIN');
rows.push(row('statusline/cve', patched ? 'ok' : 'warn',
patched
? 'CVE counter overlaid with real scan results'
: 'statusline shows ruflo\'s fabricated CVE count (upstream defect)',
patched ? null : 'sync injects the security overlay'));
}
} else {
rows.push(row('statusline', 'info', 'no project statusline here (created by setup)'));
}
Expand Down
65 changes: 62 additions & 3 deletions src/lib/statusline.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFileSync } from 'node:child_process';
import { projectStatusline, projectSettings } from './paths.mjs';
import { projectStatusline, projectSettings, rufloCliDist } from './paths.mjs';
import { installedVersion } from './versions.mjs';
import { readJson, writeJsonWithBackup } from './settings.mjs';

Expand All @@ -18,6 +18,61 @@ const FOOTER_TEMPLATE = path.join(

const eol = (s) => (s.includes('\r\n') ? '\r\n' : '\n');

// Security overlay wrapper. Wraps getStatuslineData() rather than patching
// applyLocalOverlays(), because applyLocalOverlays is NOT on every path: the
// fresh-cache early return (`if (cache.fresh && cache.promoFresh) return
// overlayMemoPromo(cache.data)`) bypasses it, so for the 60s TTL a patched
// applyLocalOverlays is simply never called and the fabricated count renders
// anyway (verified empirically — the overlay had no effect until this wrapper).
// Wrapping the single entry point covers all four return paths (CLI delegation,
// fresh cache, stale-while-revalidate, local fallback) with one injection.
//
// Relies on function-declaration hoisting: `function getStatuslineData()` is
// initialized before any top-level code runs, so this block — injected near the
// top of the file — can reassign the binding, and the later declaration does not
// re-execute and clobber it. The typeof guard keeps it inert on any template that
// lacks the function (e.g. the minimal statusline-v3.cjs).
const SEC_WRAP = [
'/* ruflo-sec:BEGIN */',
'try {',
' if (typeof getStatuslineData === "function") {',
' var _rufloOrigGetStatuslineData = getStatuslineData;',
' getStatuslineData = function(){',
' var d = _rufloOrigGetStatuslineData.apply(this, arguments);',
' try {',
' if (d) {',
' d.security = rufloLocalSecurity(process.cwd(), d.security);',
' d.promo = rufloHonestInsight(d.promo, d.security);',
' }',
' } catch(e){}',
' return d;',
' };',
' }',
'} catch(e){}',
'/* ruflo-sec:END */',
].join('\n');
const SEC_WRAP_STRIP = /\/\* ruflo-sec:BEGIN \*\/[\s\S]*?\/\* ruflo-sec:END \*\/\n?/g;

/** Upstream defect: ruvnet/ruflo#2694.
* True while ruflo's getSecurityStatus() still FABRICATES the CVE count — i.e. the
* installed CLI still has `const totalCves = 3` (a hardcoded constant naming ruflo's
* own v3 roadmap items, not the rendered project's risk) with cvesFixed derived from
* scans.length (a FILE count, not findings). Read-only probe of the installed CLI.
*
* This is the stopgap's self-retirement gate, mirroring improvement-eval's --cli-check
* (#2222): detect the defect in shipped code rather than pinning a version number, so
* the kit stops patching the moment upstream fixes it — no release-tracking required.
* Unreadable/absent/changed => false (fail safe: never patch what we cannot verify is
* broken; the worst case is ruflo's own unmodified behavior). */
export function upstreamCveCounterFabricated() {
try {
const f = path.join(rufloCliDist(), 'funnel', 'local-signals.js');
if (!fs.existsSync(f)) return false;
const src = fs.readFileSync(f, 'utf8');
return /const totalCves = 3\b/.test(src) && /scans\.length/.test(src);
} catch { return false; }
}

export function fixStatusline(root = process.cwd(), { dryRun = false } = {}) {
const file = projectStatusline(root);
if (!fs.existsSync(file)) return { file, applied: false, reason: 'no statusline.cjs (created by ruflo init)' };
Expand All @@ -35,9 +90,13 @@ export function fixStatusline(root = process.cwd(), { dryRun = false } = {}) {
const footer = fs.readFileSync(FOOTER_TEMPLATE, 'utf8').replace(/\r\n/g, '\n').trim();
s = s.replace(/\/\* ruflo-seg:BEGIN \*\/[\s\S]*?\/\* ruflo-seg:END \*\/\n?/, '');
s = s.replace(/ \+ rufloActivationSegments\(process\.cwd\(\)\)/g, '');
// (d) security overlay: stripped unconditionally BEFORE the gate is consulted, so the
// stopgap retires itself on the first sync after upstream fixes getSecurityStatus.
s = s.replace(SEC_WRAP_STRIP, '');
const securityOverlay = upstreamCveCounterFabricated();
const lines = s.split('\n');
const at = lines[0]?.startsWith('#!') ? 1 : 0;
lines.splice(at, 0, footer);
lines.splice(at, 0, securityOverlay ? footer + '\n' + SEC_WRAP : footer);
s = lines.join('\n');
s = s.replace(/console\.log\(generateStatusline\(\)\)/, 'console.log(generateStatusline() + rufloActivationSegments(process.cwd()))');

Expand Down Expand Up @@ -71,5 +130,5 @@ export function fixStatusline(root = process.cwd(), { dryRun = false } = {}) {
repointed = true;
}

return { file, applied: out !== raw, repointed, version: ver };
return { file, applied: out !== raw, repointed, version: ver, securityOverlay };
}
Loading
Loading