mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-11-09 22:13:39 +09:00
2f192d4eb92f661cb74cb030b021d64dc70f2168
221
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2f192d4eb9 | Merge branch 'pr-3367' into integ/train-3.45.1 | ||
|
|
227af0b969 | Merge branch 'pr-3424' into integ/train-3.45.1 | ||
|
|
4c2c62df27 |
dream(intelligence, evaluated): wire CLAUDE_FLOW_PRIOR_DECAY env override for ModelRouter's dormant decay primitive (#3349) (#3350)
* dream(intelligence): #3349 wire CLAUDE_FLOW_PRIOR_DECAY env override for ModelRouter's dormant decay primitive ModelRouter's discounted-Thompson-sampling priorDecay (built/tested/ benchmarked in #3049) shipped permanently inert: DEFAULT_CONFIG.priorDecay was hardcoded to 1 (disabled) with no env/config knob, unlike its sibling maxUncertainty (envMaxUncertainty()). Adds envPriorDecay() mirroring that exact pattern; default behavior is unchanged when the env var is unset. Evaluation: stash-isolated discriminating test (1/3 new tests fails against reverted source, 16/16 pass restored); full @claude-flow/cli suite and tsc --noEmit both byte-identical baseline vs candidate outside the 3 new tests; benchmark re-run reproduces the 2026-08-17 receipt exactly. Real non-stationary recovery win in the 'low' bucket; a small but statistically real stationary-accuracy cost in the 'med' bucket, accepted only under the pre-existing ±1pp tolerance band from the original receipt — disclosed in the issue/gist rather than oversold. Independent adversarial critic verdict: CONFIRMED-WITH-CAVEATS. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_011tXcJWNao4vh57f4uFXutg * docs(dream-cycle): append 2026-09-17 ledger row + backfill 09-13..09-16 gap/run verification Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_011tXcJWNao4vh57f4uFXutg * fix(audit): register CLAUDE_FLOW_PRIOR_DECAY as a documented env-var escape hatch scripts/audit-env-var-precedence.mjs (ADR-125/ADR-130) flagged the new CLAUDE_FLOW_PRIOR_DECAY read in model-router.ts as undocumented CLI-flag precedence. It's the same operator-knob shape as the adjacent, already- registered CLAUDE_FLOW_MAX_UNCERTAINTY (envMaxUncertainty() is the pattern envPriorDecay() mirrors) — no CLI invocation owns the router's persisted lifetime, so there's no flag to wire precedence against. Registered alongside it with the same justification. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_011tXcJWNao4vh57f4uFXutg --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
0702af8158 | fix(witness): fail marker-preserving hash drift in strict mode | ||
|
|
82dac70fd7 |
feat(router): ADR-389–391 — refresh router helper, opt-in MiniLM embedder, benchmark gate (no default change) (#3410)
* docs(adr): ADR-389..391 — router: refresh the helper, use the real embedder, change defaults only on a measured win ADR-389: add router.js to CRITICAL_HELPERS + re-sign, so installed copies get the word-boundary fix (#3401); today they are frozen at first write. ADR-390: hooks_route embeds patterns and tasks with the local MiniLM chain (generateLocalEmbedding), hash embedder as the reported fallback. ADR-391: frozen, labelled 150-200 prompt corpus; A/B/C/D (current, MiniLM, typesafe-hash, typesafe-onnx); ADR-150-style AND-gate for promotion. Co-Authored-By: RuFlo <ruv@ruv.net> * feat(init): refresh router.js as a critical helper on upgrade (ADR-389, #3401) Installed copies of .claude/helpers/router.js never received the #2257 word-boundary fix: helper-refresh only re-copies CRITICAL_HELPERS and router.js was not on the list, so an April substring router kept routing "sync and review latest issues" to tester. - helper-refresh.ts: add router.js to CRITICAL_HELPERS; generator fallback also emits it (generateAgentRouter). - sign-helpers.mjs / verify-helpers.mjs / executor.ts executeUpgrade / smoke-helper-signing-security.mjs: keep their copies of the list in sync. - New test: stale April router.js is replaced by the package copy via the signed refresh path (throwaway key), after which the prompt no longer routes to tester. helpers.manifest.json is NOT re-signed here. Until it is, verify-helpers fails ("manifest has no entry for router.js") and the production refresh is fail-closed blocked for all helpers. Co-Authored-By: RuFlo <ruv@ruv.net> * bench(router): ADR-391 labelled evaluation corpus (197 prompts, frozen) Blind-labelled corpus for comparing routing candidates: 11 labels incl. none, 64 adversarial trap cases, deterministic stratified dev/test split (i%5 in {0,2} -> dev), dependency-free validator that recomputes the split and prints the sha256. Frozen at sha256 b0c1923b2813b61907304dff56c53d199b709797bfb9600a7c1f496cf0c0bf04. Co-Authored-By: RuFlo <ruv@ruv.net> * feat(hooks): ADR-390 — semantic router can use the real MiniLM embedder hooks_route compared tasks with pattern keywords using a character hash (generateSimpleEmbedding), which measures spelling, not meaning. This adds a router-embedder abstraction (src/ruvector/router-embedder.ts): - embedForRouter(texts, kind) -> { vectors, embedder: 'minilm'|'hash', reason? } - MiniLM path uses generateLocalEmbedding ONLY (never the bridge-first generateEmbedding, #2312). backend !== 'onnx', a throw, or a non-384-d vector degrades EVERY text in the call to the hash, with a reason. - Selection: CLAUDE_FLOW_ROUTER_EMBEDDER=minilm|hash; DEFAULT_ROUTER_EMBEDDER stays 'hash' until ADR-391's benchmark decides. getSemanticRouter builds both the native VectorDb and pure-JS indexes from one embedder, records which one actually built the index, embeds the query with that same embedder, rebuilds when the requested embedder changes, and shares one in-flight build between concurrent callers. If the query fails on MiniLM after a MiniLM index was built, the index is rebuilt with the hash so the two spaces never mix. hooks_route results gain `embedder` (+ `embedderReason` when degraded); existing fields are unchanged. routeTaskForBench(task, { embedder }) is an internal, bench-only entry that runs the same local path as hooks_route (skipping the AgentDB pre-route and the opt-in typesafe wrapper) for ADR-391. Co-Authored-By: RuFlo <ruv@ruv.net> * chore(audit): register CLAUDE_FLOW_ROUTER_EMBEDDER as a known env escape hatch ADR-390's router embedder selector is process-lifetime MCP router state, like CLAUDE_FLOW_DISABLE_NATIVE_ROUTER; the ADR-391 bench passes the embedder explicitly rather than through a CLI flag. Co-Authored-By: RuFlo <ruv@ruv.net> * bench(router): ADR-391 first run — no candidate promoted; fix typesafe-router test (e) Benchmark (run-bench.mjs) on the frozen 197-prompt corpus (sha256 b0c1923b…), test split n=113: A current (hash) 25.7% acc, p95 1.06 ms B MiniLM (ADR-390) 36.3% acc, p95 6.21 ms (+10.6 pts, +485% p95) C typesafe hash 26.5% acc, p95 1.20 ms D typesafe onnx 29.2% acc, p95 8.35 ms (raw pick 44.3%, gated to 5.6%) No candidate passes the ADR-391 AND-gate (B fails the relative latency bound), so DEFAULT_ROUTER_EMBEDDER stays 'hash' and typesafe stays opt-in. Receipt committed under benchmarks/router/results/. ADR-391 gains a Results section with the findings (structural ceiling: no pattern returns researcher/reviewer/none; latency criterion mis-specified for a ~1 ms base; typesafe gate too tight) as owner decisions, not applied. ADR-389 -> Implemented; ADR-390 -> Implemented as opt-in; ADR-391 -> Implemented. typesafe-router test (e) was red on main since #3402 made the keyword fallback word-bounded: it still asserted the old "latest"->tester fallback. It now asserts the typesafe override and that a legacy fallback is kept. Co-Authored-By: RuFlo <ruv@ruv.net> |
||
|
|
72e17533ca |
fix(memory): release graph-edge-writer's native WAL handle so sql.js memory_store isn't refused (#3397) (#3405)
* fix(memory): release graph-edge-writer's native WAL handle so sql.js memory_store isn't refused (#3397) graph-edge-writer cached its better-sqlite3 WAL handle in a module singleton for the whole MCP server lifetime; nothing but the test-only _resetBridgeDb ever closed it. Its -wal/-shm sidecars therefore stayed on disk forever, and the #2735 sidecar guard (correctly) refused every later sql.js whole-image write. On Windows, where the native AgentDB bridge is off by default (#3024), that turned one hooks_post-task into a permanent memory_store outage for the server. - getBridgeDb() arms an unref'd idle timer (1s, CLAUDE_FLOW_GRAPH_EDGE_IDLE_MS) that checkpoints (TRUNCATE) and closes the handle; re-armed on every use. - New releaseBridgeDb(dbPath?) is called by memory-initializer right before each #2735 guard, so a store immediately after an edge write works and the edge is checkpointed into the image sql.js rewrites. - process 'exit' hook releases the handle so a clean shutdown does not leave stale sidecars for the next process. No signal handlers (would change Node's default termination). - The guard itself is unchanged: an open WAL connection makes a whole-image write unsafe even in-process until its WAL is checkpointed. Fixes #3397 Co-Authored-By: RuFlo <ruv@ruv.net> * chore(audit): register CLAUDE_FLOW_GRAPH_EDGE_IDLE_MS as an env-only tuning knob (ADR-125) Co-Authored-By: RuFlo <ruv@ruv.net> |
||
|
|
0a2df2bfd2 |
fix(metaharness): use the @metaharness/* and ruflo CLI already installed 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
(
|
||
|
|
6f0ed71128 |
fix(release): spawn npm.cmd with shell:true in prepare-root-publish too (#3348)
Same EINVAL bug as #3346 (stage-internal-runtime-bundles.mjs's runBuild), one file over: prepare-root-publish.mjs's own npm.cmd spawnSync for building v3/@claude-flow/swarm and cli lacked shell:true, blocking the root claude-flow package's Windows publish the same way. Found immediately after #3346 while publishing 3.42.3 end to end. Single-string shell:true form, matching #3346's fix, to avoid Node's DEP0190 warning. Still constant literals, no injection surface. |
||
|
|
fcee45bc1d |
fix(release): spawn npm.cmd with shell:true on Windows (#3346)
* fix(release): spawn npm.cmd with shell:true on Windows
spawnSync('npm.cmd', ['run','build'], {stdio:'inherit'}) threw EINVAL on
Windows, blocking every @claude-flow/cli publish on this platform since
CreateProcess can't launch a .cmd directly and Node has refused to shell
out implicitly since CVE-2024-27980.
Found live while publishing 3.42.3: prepublishOnly -> prepare-publish.mjs
-> stageInternalRuntimeBundles() rebuilds the four internal runtime
packages (security/codex/mcp/plugin-agent-federation) and hit this on
the very first Windows publish attempt after upgrading past the Node
version where the implicit shell fallback still worked.
command/args here are constant literals ('npm.cmd', ['run','build']),
never hook-derived or user-controlled, so shell:true carries no
injection risk.
Co-Authored-By: RuFlo <ruv@ruv.net>
* fix(release): avoid DEP0190 warning in the npm.cmd shell:true fix
Single-string command form instead of args array — same shell:true
safety (still constant literals, nothing interpolated), no warning.
Co-Authored-By: RuFlo <ruv@ruv.net>
|
||
|
|
90889f4772 |
feat(sona): default the learning mode from RUFLO_INTELLIGENCE_MODE (#3334)
* feat(sona): default the learning mode from RUFLO_INTELLIGENCE_MODE The SONA learning mode was only settable per call (`config.mode`), defaulting to `balanced` otherwise. A deployment that wants every session to learn in a specific profile — a managed desktop fleet running `research` for +55% quality, or `edge` on constrained hosts — had no way to set that default short of threading `mode` through every call site or patching a generated helper. This reads `RUFLO_INTELLIGENCE_MODE` as the fleet-wide default in both the SONA config merge (`@claude-flow/integration` sona-adapter) and the memory learning bridge (`@claude-flow/memory`). Precedence is unchanged where a caller is explicit: `config.mode` (or `sonaMode`) still wins; the env only fills the former default slot; and an unset or unrecognised value returns undefined so the code falls through to `balanced` — a typo can never silently select a profile nobody asked for. Validated against the mode enum in each package. Co-Authored-By: claude-flow <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01MUrJdDHhQ91hwpUBQhV4fC * fix(sona): resolve sonaMode per-instance in LearningBridge, not at module load learning-bridge.ts captured sonaModeFromEnv() into a module-scope DEFAULT_CONFIG constant, evaluated once on first import — so any RUFLO_INTELLIGENCE_MODE set later in the same process (including test setup) was permanently missed. sona-adapter.ts's equivalent (mergeConfig()) already re-reads the env var on every call; this brings LearningBridge's constructor in line with that behavior, preserving the same precedence (explicit config.sonaMode > env > default). Adds regression tests for both packages (sona-adapter.ts had none previously) and registers RUFLO_INTELLIGENCE_MODE in KNOWN_ESCAPE_HATCHES per the audit script's existing requirement for operator-knob env vars outside its SCAN_ROOTS. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01SjxUvZnyBdwtx4Y1jKAqt6 |
||
|
|
e036e35dd7 |
dream(memory): #3231 wire embedding near-dup detection into MemoryConsolidator.dedup() (evaluated, ACCEPT) (#3232)
* dream(memory): wire embedding near-duplicate detection into MemoryConsolidator.dedup()
MemoryConsolidator.dedup() only ever deduplicated entries via byte-exact
SHA-256 content hashing, even though every entry already carries a
computed .embedding and the adapter's HNSWIndex (already held in scope
for removal bookkeeping) is incrementally kept in sync. Paraphrases and
reformattings of the same memory were never caught.
Adds a second pass: for hash-pass survivors with an embedding, query the
already-populated HNSWIndex for cosine-similarity neighbors above a
configurable similarityThreshold (default 0.95, matching the unwired
domain-layer consolidator's own constant), and merge using the same
keeper-selection strategies (now factored into shared selectKeeper/
mergeGroup helpers). Guarded to cosine-metric indexes only; disabled via
similarityThreshold >= 1.
Evaluation: 471/472 passing in @claude-flow/memory (1 pre-existing,
unrelated, environmental failure); baseline (git-stash-isolated source)
fails exactly the 2 new discriminating tests and passes the rest,
confirming the fix is additive and non-regressive. tsc --noEmit clean.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01H4kfasS4XkYaSGYXFLSSEG
* dream(memory): fix-forward — converge near-dup pass to a fixed point in one dedup() call
Independent adversarial critique (STEP 10) reproduced a real, bounded gap
in the prior commit: a near-duplicate cluster larger than
NEAR_DUP_SEARCH_K (8) split into multiple leftover sub-group survivors
within a single dedup() call, because each round's `consumed` bookkeeping
permanently excluded a group's keeper from further matching even though
it was still fully present in the index. 15 pairwise-identical-embedding
entries collapsed to 2 survivors (merged: 13) instead of 1 (merged: 14).
It self-healed across repeated runAll() calls (background timer /
nightlyLearner), so this was never a permanent-data-loss bug, but a
single manual dedup() call under-converged relative to its documented
"collapse duplicates" contract.
Fix: loop pass 2 to a fixed point (re-scan until a full round produces
zero merges) instead of a single scan. Always terminates — entries
strictly decrease each round that merges anything. `groups` now counts
merge operations across all rounds, which can exceed the number of
underlying duplicate clusters when one needed more than one round;
documented in the method's doc comment.
Added a discriminating regression test reproducing the critic's exact
15-entry scenario; confirmed it fails against the pre-fix single-round
code (merged: 13) and passes against this fix (merged: 14, single
survivor). Full package suite: 472/473 passing (1 pre-existing,
unrelated, environmental failure, unchanged from before this commit).
tsc --noEmit clean.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01H4kfasS4XkYaSGYXFLSSEG
* dream(memory): 2026-09-08 research gist — embedding near-dup consolidation
Full SOTA report: 5-role parallel research fan-out, ledger check with
GitHub-verified fates for the last 14 rows (3 merged since 09-03, one PR
now crossing the 14-day stale threshold for the first time this cycle),
competitor comparison (CrewAI/Mem0/Zep/LangMem/Qdrant/SemDedup), plugins
and automation scan findings, adversarial critique with a real bug found
and fixed, and witness stamp.
No gist-creation MCP tool available in this session (GitHub MCP tools
cover issues/PRs/repos, not gists) — committing to docs/dream-cycle/
instead, matching every dream-cycle night since 2026-08-14.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01H4kfasS4XkYaSGYXFLSSEG
* dream(memory): #3231 update LEDGER.md for 2026-09-08
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01H4kfasS4XkYaSGYXFLSSEG
* dream(memory): #3231 empirical receipts for dedup() near-dup pass (response to review)
Response to PR #3232's INCONCLUSIVE review, which asked for a threshold
contract rather than a smoke fixture: a contamination-checked embedding
corpus with pinned model, precision/recall including hard negatives and
cross-namespace cases, baseline vs. candidate latency/memory at realistic
cardinality, and deterministic replay + unchanged exact-dedup behavior.
Real pinned-model corpus: attempted directly against both ONNX backends
this monorepo ships. Both fail for reasons unrelated to this candidate,
reproduced on an unmodified checkout before writing anything:
- @huggingface/transformers@4.2.0: transformers.node.mjs does
`import { Tensor } from "onnxruntime-common"` against a CJS module — a
real ESM/CJS interop break under this sandbox's Node version.
- @xenova/transformers@2.17.0: pulls in sharp@0.32.6, whose native
binding (sharp-linux-x64.node) isn't present for this platform — the
same failure independently reproduced moments later by this package's
own nightlyLearner test, which falls back to mock embeddings.
Neither is fixable within this PR's scope without touching unrelated,
pre-existing native/module-resolution infrastructure. Documented in
detail at the top of the new test file rather than silently worked
around or omitted.
What's delivered instead, in
v3/@claude-flow/memory/src/consolidator-embedding-benchmark.test.ts:
- A deterministic synthetic corpus where pairwise cosine similarity is
constructed EXACTLY via vectorAtSimilarity() (Gram-Schmidt against a
random orthogonal vector), not measured after the fact — a rigorous
instrument for threshold mechanics specifically, disclosed as not
validating real-world semantic accuracy.
- Precision/recall/false-merge-rate across a 5-point threshold sweep,
every point at a deliberate margin from every corpus similarity value
after an exact-boundary collision (sweep value == corpus value) proved
float32-jitter-sensitive while writing this — documented as a finding,
not silently patched around.
- Namespace isolation: proved cross-namespace near-dup merging matches
cross-namespace hash-exact merging exactly (both pre-existing,
unscoped-by-namespace behavior — this candidate doesn't change it).
- Determinism: 3 independent runs, compared by deterministic entry KEY
(not raw id, which is randomly generated per store() and was a second
bug this file's own first draft had to fix).
- Latency/memory at N=5000 (matching this repo's own HNSW-benchmark
convention): initially measured 9.8s for the candidate pass, which
traced to an unset `ef` search parameter defaulting to efConstruction
(200) — every dedup() search traversed a 200-candidate list for a
duplicate-detection task that only needs very-close neighbors. Fixed
by passing an explicit NEAR_DUP_SEARCH_EF=32 in consolidator.ts,
re-measured at 3.7s (~2.7x), precision/recall unchanged (still exact).
Real numbers logged in the test output either way, not asserted away.
Full package suite: 478/479 passing (472 pre-existing + 7 new; same 1
pre-existing unrelated environmental failure as every prior commit on
this branch). tsc --noEmit clean.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01H4kfasS4XkYaSGYXFLSSEG
* dream(memory): #3231 add new benchmark file to CI test-ratchet baseline
CI on a2e99cf failed with "1 unexpected failing file(s): +
v3/@claude-flow/memory/src/consolidator-embedding-benchmark.test.ts".
Root-caused via the vitest JSON report artifact (test-results-ubuntu-latest,
downloaded and parsed directly, not guessed): a suite-level collection
failure, "Cannot find package '@claude-flow/security' imported from
'.../v3/@claude-flow/memory/src/agentdb-retrieval-guard.ts'" — the same
unbuilt-sibling-package gap already accepted in scripts/ci-test-baseline.txt
for 9 other @claude-flow/memory test files, including consolidator.test.ts
itself (line 95). Every file in this package fails identically in this CI
checkout state; my new file just wasn't in the baseline yet because it's
new. Not a regression this PR introduced — confirmed by the failure being
a pre-import-time module-resolution error, not a test assertion in the
new file's own logic (which passes locally in a checkout with @claude-flow/
memory's dependencies actually built: see the 6/6 local run in the prior
commit's message).
Fix: add the new file to the baseline, alongside its 9 memory-package
siblings already there, matching this repo's own established convention
for this exact gap rather than working around it some other way.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01H4kfasS4XkYaSGYXFLSSEG
* dream(memory): #3231 baseline 2 more federation/gateway plugin test files
CI on
|
||
|
|
1bd734e272 |
dream(security): #3102 bind ADR-377 caller-identity verification into live authorizeMcpTool chokepoint (evaluated, ACCEPT-with-caveats) (#3103)
* dream(security): #TBD bind ADR-377 caller-identity verification into live authorizeMcpTool chokepoint (evaluated, ACCEPT-with-caveats)
authorizeMcpTool() trusted the plain, unsigned CLAUDE_FLOW_PRINCIPAL_ID env
var as caller identity with zero verification (OWASP ASI07-class gap).
ADR-377 Phase 3 already implemented Ed25519 issueInvocationToken/
verifyInvocationToken but never wired it into a live dispatch path.
Binds the two: DualModeOrchestrator mints a per-worker signed
InvocationToken at spawn (worker-lifetime TTL, wildcard tool scope --
disclosed reduction from the primitive's original per-call design);
authorizeMcpTool verifies it before trusting identity, failing closed on
missing/forged/expired tokens. Off by default (CLAUDE_FLOW_MCP_CALLER_AUTH).
6 new deterministic Vitest scenarios plus an independent adversarial
critique (fresh session, verdict CONFIRMED-SAFE-WITH-CAVEATS) are recorded
in docs/dream-cycle/dream-gist-2026-08-26.md. Darwin skipped (scope
mismatch -- binary crypto gate, no tunable parameter space).
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015bfVASxkNXKpZbMSLhVU1L
* fix(ci): register ADR-377 Phase 3 caller-identity env vars as known escape hatches
CLAUDE_FLOW_MCP_CALLER_PUBKEY and CLAUDE_FLOW_MCP_INVOCATION_TOKEN
(DualModeOrchestrator's per-worker Ed25519 credential pair, read by
resolveMcpCallerIdentity()) weren't registered in
audit-env-var-precedence.mjs's KNOWN_ESCAPE_HATCHES, so the audit flagged
CLAUDE_FLOW_MCP_CALLER_PUBKEY as a real violation (exit 1). Same no-CLI-flag
reasoning as the existing CLAUDE_FLOW_PRINCIPAL_ID entry: these are
credentials minted into a spawned worker's own environment, not a value
any caller should be able to select via a flag.
Verified: audit-env-var-precedence.mjs now exits 0. policy-runtime.test.ts
still 18/18.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01RYP825phHMjPRmmjkcAWpq
* fix(ci): regenerate root package-lock.json for codex's new @claude-flow/security dep
The CI Test Suite job runs plain `npm ci` at repo root (no v3 pnpm
install step), so it resolves workspace deps strictly from
package-lock.json — not from v3/pnpm-lock.yaml, which this PR had
already updated. codex/package.json's new @claude-flow/security
dependency was missing from package-lock.json's codex entry, so CI's
npm ci never linked it and every dual-mode test importing
orchestrator.ts failed with "Cannot find package '@claude-flow/security'".
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01RYP825phHMjPRmmjkcAWpq
* fix(ci): build @claude-flow/security before Test Suite runs it
Complements
|
||
|
|
8008bc3562 |
fix(federation): verified event identity must win over publisher-controlled content (#3322)
* fix(federation): verified event identity must win over publisher-controlled content fetchRecent/fetchManyOn/fetchChannel spread the parsed message content after the signature-verified id/pubkey/created_at, so a publisher could put those keys in their own content and overwrite their verified identity downstream (reduceClaims trusts .pubkey for claim release/handoff authorization). Reorder so verified fields always win. Also in x-federation-channels.ts: reqEvents() never called verifyEvent() at all, so channel-read/grant-accept events were trusted unsigned; add the check. Same content-override reorder in x_federation_channel_read. fix(hooks): escape argv for the Windows shell:true npm-shim path ruflo-hook.cjs's invokeHook() passes hook-derived values (command text, file paths) through cmd.exe via shell:true to resolve npm's .cmd/.ps1 shims. Node does no escaping in that mode, so a value containing a cmd.exe metacharacter could be reinterpreted as a separate command or redirection instead of reaching the CLI as data. Add escapeCmdArg() (qntm.org/cmd algorithm, same one cross-spawn uses) and apply it to every argv element on that path. No native Windows/cmd.exe available to execute this end-to-end in this environment — verified at the string-transform level only (8 unit tests). Real Windows execution remains a required follow-up gate. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01RYP825phHMjPRmmjkcAWpq * fix(ci): register new hook test env var and test file with CI guards Follow-up to the previous commit's escape-cmd-arg.test.cjs: - Register RUFLO_HOOK_UNIT_TEST in audit-env-var-precedence.mjs's KNOWN_ESCAPE_HATCHES, matching the existing RUFLO_HOOK_CLI_OVERRIDE/ RUFLO_HOOK_DEBUG_STDOUT entries for the same file. - Register plugins/ruflo-core/scripts/escape-cmd-arg.test.cjs in ci-test-baseline.txt, matching the sibling mcp-launch.test.cjs entry — both are node:test-based .cjs files that vitest's own JSON reporter marks "failed" (it doesn't recognize the node:test API) even though every test inside genuinely passes under `node --test`. Verified locally: node scripts/audit-env-var-precedence.mjs now passes (exit 0). A scoped `vitest run` + `ci-test-ratchet.mjs --report` against just these two files confirms escape-cmd-arg.test.cjs is no longer an unexpected failure (the one remaining flagged file is a stale local worktree artifact under .claude/worktrees/, not part of a clean CI checkout). Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01RYP825phHMjPRmmjkcAWpq |
||
|
|
8485302f10 |
feat(x-gateway): 0.6.1 — bound Seraphina by budget instead of an admin token (#3275)
* feat(x-gateway): 0.6.1 — Seraphina bounded by budget, not by an admin token Squashes what is actually running on x.ruv.io so git and production agree. Seraphina was gated alongside the write tools, but it is not like them: it reads the roster, the claims board and recent messages, asks a model, and returns advice. It writes nothing and carries no authority. The gate answered the wrong question — the exposure is model spend, and spend is bounded with a budget, not a password. The practical cost was worse than a wrong abstraction. A browser cannot hold a bearer secret, so a published UI was either locked out or tempted to ship RUFLO_ADMIN_TOKEN to the client — the same token that mints invites and publishes as the gateway identity. The safe-looking option was the catastrophic one. Seraphina now answers with no token, bounded by a shared daily cap and a per-client hourly cap, and anonymous callers cannot select the high or ultra tiers. An admin token lifts both. Every write tool keeps its gate; verified live that claims_issue, federation_invite_mint, federation_publish, federation_admit and channel_publish all still refuse an anonymous caller. Not included, deliberately: a tool to relay member-signed events through the gateway. It was built, tested against the live relay, and removed. buzz-relay refuses any EVENT whose pubkey differs from the NIP-42 authenticated connection, so a gateway cannot publish on another identity's behalf — and it does not need to. Members already publish as themselves through the wss://x.ruv.io proxy, verified end to end. Shipping a tool that always fails would be worse than none. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67 * feat(x-gateway): serve onboarding guidance, and deliberately not key generation The registry said "generate a Nostr keypair" and stopped, and it never named the two identities in play. That gap was not theoretical: a capable agent inspected the surface, found only gateway-signed publishing, and concluded ordinary members could not publish from a dashboard at all. They can. It just was not written anywhere they could reach. federation_onboarding and ruv://federation/onboarding now answer it, open, no token. The guide leads with the distinction that causes the confusion — your key signs for you, the gateway's key signs for the service, and the gated tools are gated precisely because they speak as the service. It carries the five steps, the four things never to do, and the three traps this service has actually taught us: sign the NIP-42 relay tag with the canonical URL even when proxied, the relay binds publishing to the authenticated connection, and the channel tag is `c` not `h`. It does NOT generate keys, and that is the point rather than an omission. A service that mints your keypair and returns the secret has seen your secret, and becomes custodian of every identity it "helped" — the same custody mistake as putting an admin token in a browser, inverted. The guide ships the code so the caller runs it locally and the key never crosses the wire. One test note worth keeping: the first version of the safety assertion regex- matched for "send your secret" and failed on the guide's own "Never send your secret key" line. It is now structural — no field may be named for secret material — because a string search cannot tell an instruction from its negation. Verified live on 0.7.0 (ruflo-x-gateway-00015-qj7) with an anonymous call. 20/20. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67 * fix(ci): register the gateway's credential and budget knobs, and skip test/ like tests/ The ADR-125 precedence audit failed the gateway PR, and it was right to look — but for two reasons that are both about the audit, not the code. RUFLO_ADMIN_TOKEN was not registered as an escape hatch even though its sibling RUFLO_X_ADMIN_TOKEN is, with the same reasoning: a secret must never be a CLI flag, where it lands in shell history and process listings. The gateway is a long-running service with no typed command surface at all, so there is no invocation to attach a flag to. Same for the two Seraphina budget knobs added in #3275. The other half was a naming gap. SKIP_DIRS already excludes `tests` and `__tests__` because the audit is about production precedence, not test setup — but the flagged lines were in `plugins/ruflo-x-gateway/test/`, singular, which was not in the set. Two `process.env.X = 'test-admin-token'` assignments inside a test fixture were reported as undeclared production reads. Verified: the audit now exits clean on this branch. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67 |
||
|
|
dbf450a927 |
feat(federation): ruflo CLI + MCP integration for x.ruv.io, and Seraphina (swarm queen) (#3256)
* feat(federation): ruflo CLI + MCP integration for x.ruv.io, and Seraphina (swarm queen)
Integrate the open swarm federation into ruflo itself, and add Seraphina —
a primary-coordinator / swarm-queen guidance tool in the style of the ruOS
assistant terminal (invoked from a terminal or any MCP client).
MCP tools (src/mcp-tools/x-federation-tools.ts), registered in the barrel and
the in-process registry so `callMCPTool` and the MCP server both see them:
x_federation_sync / roster / claims / registry (open reads)
x_federation_publish / invite_mint / admit (gateway-identity writes,
require RUFLO_X_ADMIN_TOKEN; fail closed with no network call)
Each description follows ADR-112 ("Use when … wrong because …").
Seraphina (src/mcp-tools/seraphina-tools.ts): `seraphina_guidance { goal }`
gathers the live roster, claims board and recent messages from the x.ruv.io
gateway, compacts them (dedupe by from|type, cap 15 — a cheap tier drowns in
repeated PeerHellos), and asks the cognitum meta-llm gateway
(https://api.cognitum.one/v1/messages, model cognitum-auto by default with a
tier override) using a queen system prompt that treats message content as
data, respects one-owner-per-resource claims, and returns
{ guidance, proposals[], risks[] }. Proposals are advisory. JSON is extracted
by slicing the outermost object so fenced/prose-wrapped answers still parse.
Key from SERAPHINA_METALLM_KEY (GCP secret seraphina-metallm-api-key).
CLI: `ruflo federation sync|roster|claims|registry|invite|admit|publish`
(src/commands/federation.ts), thin over the same tools.
Skill: .agents/skills/open-federation/SKILL.md documents CLI, tools,
Seraphina, the claims rules, the canonical NIP-42 relay-tag gotcha, and
onboarding.
Tests: 11 (x-federation: RPC shaping, SSE parse, resource mapping, fail-closed
admin gating, isError surfacing; seraphina: registration, fail-closed key,
context gathering + queen prompt + x-api-key, tier override, fenced-JSON
extraction). Project typechecks at 0 errors.
Verified live: Seraphina against the real 3-node federation returned 3
structured proposals + 4 risks via cognitum-auto (routed to a cheap tier).
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
* fix(federation): ADR-125 env-var precedence — URL config takes a flag/arg, credentials registered env-only
The env-var-precedence audit correctly flagged the new process.env reads.
Config URLs now have a precedence path: `ruflo federation --gateway` feeds a
`gatewayUrl` tool arg (and Seraphina takes `metaLlmUrl`/`gatewayUrl`) that
wins over RUFLO_X_GATEWAY_URL / SERAPHINA_METALLM_URL, documented per ADR-125.
Credentials (RUFLO_X_ADMIN_TOKEN, SERAPHINA_METALLM_KEY) are registered as
env-only escape hatches with rationale: a secret must never be a CLI flag.
+1 test (arg precedence, arg not forwarded upstream). Audit passes locally.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
* feat(federation): `ruflo federation join --code` — self-service invite access with your own key
The user-facing path into the open swarm. Decentralized by design: the user
generates/holds THEIR OWN Nostr key (~/.ruflo/nostr.key, 0600), redeems an
invite code with a NIP-98-signed claim directly against the relay (no admin in
the loop), proves membership via NIP-42, and can then publish as themselves.
The gateway never signs for a user.
- src/mcp-tools/x-federation-join.ts: x_federation_join { code, relayHttp?,
relayWs?, keyFile? } — validates the code shape before any network call,
loads/creates the key, NIP-98 claim, NIP-42 verify, returns pubkey + role.
ADR-125: args take precedence over RUFLO_X_RELAY_HTTP / RUFLO_X_RELAY_WS /
RUFLO_NOSTR_KEY_FILE.
- CLI: `ruflo federation join --code v2.…`
- nostr-tools added as an optionalDependency (secp256k1/Schnorr is not in
node:crypto); the tool degrades with an install hint when absent. pnpm
lockfile regenerated in this PR (frozen-lockfile CI).
- 4 tests: 0600 key create/reuse, valid NIP-98 header (kind 27235, verifies,
bound to url+method+payload hash), malformed code rejected pre-network,
ADR-112 description. Suite: 16/16; tsc 0; env-var audit passes.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
* fix(federation): default relay -> wss://relay.ruv.io; join test tolerates missing nostr-tools
- x-federation-join defaults now point at the canonical relay.ruv.io host (Cloud Run
domain mapping for buzz-relay); the raw run.app host stays routable for old clients.
- Validate the invite-code shape before the optional-dependency check so a bad code
fails fast whether or not nostr-tools is installed.
- The root Test Suite runs the CLI tests via root npm ci, which never installs the
CLI's optionalDependencies: the crypto cases are it.skipIf(!nostr-tools) so the
file no longer trips the CI test ratchet.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
|
||
|
|
6396ff636c |
feat(x-gateway): open Nostr swarm federation gateway — MCP + ruv:// + ws proxy (deployed at x.ruv.io) (#3255)
* feat(x-gateway): MCP + ruv:// gateway for open Nostr swarm federation
New plugin `ruflo-x-gateway` — the service behind x.ruv.io. An MCP server
(Streamable HTTP at /mcp) that exposes ruflo swarm FEDERATION and CLAIMS over
an open, membership-gated, SIGNED Nostr relay (buzz-relay).
Why Nostr: every coordination message is a signed Nostr event, so authorship is
cryptographically verifiable and anyone the relay admits can participate — open
but secure. The relay gates membership + NIP-42 auth; no pre-pinning needed.
Surface:
- Tools: federation_identity / federation_join / federation_publish /
federation_sync, claims_issue / claims_release / claims_status.
- Resources: ruv://federation/registry, ruv://swarm/roster, ruv://claims/board.
- src/nostr-federation.mjs: NIP-42 authenticated connect, signed publish, and
verified fetch of #t=ruflo-swarm coordination events.
- src/server.mjs: node http server routing /, /health, /mcp (stateless
Streamable HTTP), plus the ruv:// resources.
- Dockerfile for Cloud Run; persistent Nostr identity at /data (0600).
Smoke-tested locally: server starts, /health + / respond, POST /mcp tools/list
returns the tool set over SSE. Federation tools require relay membership (by
design) — deployment wires membership + DNS (x.ruv.io) as follow-up.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
* feat(x-gateway): stable identity via RUFLO_NOSTR_KEY_HEX (GCP secret) + read-only-FS fallback
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
* feat(x-gateway): v0.2.0 — ws proxy, admin-gated writes, invite/admit tools, tests
Security model made explicit: /mcp is public, so every tool that writes
using the GATEWAY's own identity (join, publish, claims_issue/release,
invite_mint, admit) now requires `adminToken`, checked constant-time and
fail-closed (no configured token => all writes denied). Reads and ruv://
resources stay open. Users publish with THEIR OWN keys via invite->claim.
- src/ws-proxy.mjs: transparent WebSocket proxy so wss://x.ruv.io (and
/relay) fronts the Nostr relay; 500-conn cap, 502/503 on failure.
- src/relay-admin.mjs: mintInvite (NIP-98 POST /api/invites) and
admitMember (NIP-43 kind 9030) — the gateway holds relay admin role, so
the owner key never leaves GCP.
- src/security.mjs: per-IP token-bucket rate limit (60/min), 256KB body
cap enforced before buffering, security headers, timingSafeEqual admin.
- src/claims.mjs: owner-per-resource reducer extracted for testing.
- src/server.mjs: createGateway() factory (testable), stateless MCP.
- test/gateway.test.mjs: 8 tests — claims rules, gating, rate limit, body
cap, NIP-42 against a mock relay (verifies the signed challenge), routes.
Verified live before this commit: gateway pubkey admitted + promoted to
relay admin; invite minted and a fresh key self-joined via claim and
passed NIP-42 auth; deployed gateway publishes/reads over the relay.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
* fix(x-gateway): surface canonicalRelay — relay verifies NIP-42 relay tag strictly
Empirical: auth through the wss://x.ruv.io proxy is ACCEPTED when the client
signs relay=<canonical relay URL> and REJECTED (verification failed) when it
signs relay=wss://x.ruv.io. Expose canonicalRelay + authNote at GET / and in
ruv://federation/registry so clients sign the right tag.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_013u4pmL9ZUAXb6usVQgNo67
* ci: retrigger test-suite (ratchet flake; main green at
|
||
|
|
13cbd697db | fix(release): enforce CI and idle assignment safety | ||
|
|
21f7c0adb8 | fix(release): align bundled runtime metadata | ||
|
|
2ec82b0cd1 | chore(release): prepare stable 3.38.10 train | ||
|
|
1e24bb8878 |
fix(agent): propagate explicit provider/model config into agent execution (#2962) (#3007)
* fix(agent): propagate explicit provider/model config into agent execution (#2962) `providers configure` and `agent spawn --provider/--model` persisted user intent but the execution path (callAnthropicMessages, executeAgentTask, determineAgentModel) only ever consulted env vars and a 5-alias model list, silently discarding both. A local Ollama/OpenRouter setup with explicit config would either fail closed or fall back to whatever provider the env vars happened to select. - determineAgentModel(): treat any non-alias config.model string as an explicit selection via the existing modelId fast-path, instead of falling through to task-based routing / agent-type defaults. - agent_spawn: forward config.provider to the stored (and returned) agent record when it's an unambiguous explicit choice ('ollama'/'openrouter'; 'anthropic' is excluded since the CLI silently defaults to it when --provider isn't passed). - callAnthropicMessages(): accept an optional provider param and consult the persisted `agents.providers` config for baseUrl/apiKey/model when env vars are absent. A self-hosted Ollama baseUrl no longer requires the undocumented OLLAMA_API_KEY='local' sentinel. - executeAgentTask(): forward agent.provider into the first dispatch call. Precedence: explicit per-agent flag > env vars > persisted config > key-presence inference (unchanged, last resort). Co-Authored-By: RuFlo <ruv@ruv.net> * fix(ci): update #2042 smoke to match the widened OpenRouter branch shape The #2042 regression smoke statically matched the literal token sequence `useOpenRouter && openrouterKey`. #2962 widened that condition to `if (useOpenRouter) { const apiKey = openrouterKey || persistedOpenRouter?.apiKey; ... }` so a persisted `agents.providers` config can supply the key when no env var is set — the smoke's ordering guarantee (OpenRouter branch reachable before the Anthropic-key early-return) still holds, only the literal pattern needed updating. Co-Authored-By: RuFlo <ruv@ruv.net> |
||
|
|
83e536396f |
fix(scaffold): add dead-reference guard + deterministic CLI remap (ADR-382 Part C) (#2974)
Adds scripts/smoke-init-scaffold-references.mjs (ADR-382 Part C, #2971): four static assertions over v3/@claude-flow/cli/.claude/** and the plugin/marketplace surface, deriving the live MCP tool set and canonical CLI form from source rather than hand-maintaining them. 1. dead `npx claude-flow` (bare) invocation 2. dead `mcp__claude-flow__<tool>` references not in the live registry 3. plugin .mcp.json launches with no local-bin-first resolver (Part A regression guard) 4. plugins/* directories missing from .claude-plugin/marketplace.json Ships warn-only (no --strict) with a documented --strict flag to flip once backlogs clear. Wired into .github/workflows/v3-ci.yml as a new job, gated on plugins/*/.mcp.json, .claude-plugin/marketplace.json, and the script itself (v3/@claude-flow/cli/.claude/** was already a trigger path). Deterministic remap applied: 701 occurrences of bare `npx claude-flow` across 142 files -> `npx @claude-flow/cli@latest` (mechanical, 1:1, verified by rerunning the guard: check 1 701 -> 0, check 2 unchanged at 410). The 386 legacy `npx claude-flow@alpha` / `@v3alpha` occurrences elsewhere in the same tree were left untouched (still-maintained dist-tags). NOT remapped in this PR (tracked follow-up, per ADR-382 Part C's own guidance not to guess): 410 dead `mcp__claude-flow__<tool>` occurrences across 98 files, 54 distinct dead tool names (memory_usage: 53 files, task_orchestrate: 41, sparc_mode: 34, swarm_monitor: 16, plus 50 more at lower counts). Each requires per-call-site judgment about the surrounding example's intent (store vs retrieve vs list, 1-line vs restructured 2-line orchestration calls) that a bulk regex pass cannot make safely. Checks 3-4 are expected-red until ADR-382 Part A merges (plugin/ruflo-core .mcp.json resolver + the 3 missing marketplace entries: ruflo-agntcy, ruflo-bbs-federation, ruflo-business-pods). |
||
|
|
f35c545fbe |
feat(metaharness): pull in @metaharness/turn-credit + fix stale router/darwin pins (#2958)
* feat(metaharness): pull in @metaharness/turn-credit + fix stale router/darwin pins (post metaharness#176) metaharness#176 shipped @metaharness/turn-credit (ADR-248, recursive turn-level credit assignment) and bumped darwin to 0.9.0 (ADR-249 signal seams) + router to 0.4.0 (calibration module). This brings ruflo's dependency contract back in sync and makes the new package available: - Add @metaharness/turn-credit ~0.1.0 as a new optionalDependency, following the same "must be installable, not peer-only" pattern darwin/flywheel/radio already use (ADR-150) — dependency-free, 64.9K unpacked, zero lifecycle scripts, same profile as the other three. - Bump @metaharness/darwin ~0.8.3 -> ~0.9.0 and the paired MH_DARWIN_PIN constant in distill-oracle.ts (was already out of range: tilde only absorbs patches, and 0.9.0 is a minor bump). - Bump @metaharness/router peer range ^0.3.2 -> ^0.4.0 (still deliberately peer-only + triple-gated behind CLAUDE_FLOW_ROUTER_NEURAL=1, per neural-router.ts — that design choice is unchanged, only the stale range is fixed) and update the matching manual-install hint in neural.ts. - scripts/check-metaharness-pins.mjs + scripts/metaharness-clean-install-test.mjs: add turn-credit to the watched/contract-checked package list. - .github/workflows/no-cli-optdep-bloat-2561.yml: CLI_MAX 10 -> 13. The prior bump (PR #2956) left zero slack (budget == count exactly), which a code review flagged as a latent trap — the very next unrelated optional dep would trip this guard. This bump leaves real headroom (11 declared today, budget 13) instead of repeating that mistake. - Also fixes a live ReferenceError in distill-oracle.test.ts (MH_DARWIN_PIN used but never imported) — a gap from the prior release's test fix that somehow didn't surface in that PR's CI; caught here while touching the same file. Co-Authored-By: RuFlo <ruv@ruv.net> * fix: regenerate v3/pnpm-lock.yaml — was out of sync with package.json edits The previous commit edited v3/@claude-flow/cli/package.json directly (darwin/router/turn-credit pin changes) without regenerating the pnpm workspace lockfile, so every CI job running `pnpm install --frozen-lockfile` failed immediately with ERR_PNPM_OUTDATED_LOCKFILE — cascading into every downstream smoke/test job that depends on that install step. Regenerated with pnpm@8.15.9 (matching CI's pinned version) in an isolated worktree to avoid the lockfile-version drift a newer local pnpm would introduce. Co-Authored-By: RuFlo <ruv@ruv.net> |
||
|
|
0b3cfb77d6 |
MetaHarness hardening: repair the dependency contract + strict sequential promotion evidence (#2956)
* feat(metaharness): repair dependency contract + strict sequential promotion evidence Item 1 — dependency & compatibility repair (the contract was silently broken): - @metaharness/darwin ^0.8.3, @metaharness/flywheel ^0.1.10, and @metaharness/radio ^0.1.0 are now explicit optionalDependencies of @claude-flow/cli (optional PEER deps are never auto-installed, so a clean ruflo install shipped with zero MetaHarness packages on disk) - check-metaharness-pins.mjs now searches dependencies, optionalDependencies, AND peerDependencies; UNDECLARED and PEER-ONLY (for installable pins) are fatal drift instead of silently passing; radio added to the watch list; --require-installed asserts the real published symbol contracts (RefineMutator, withSequentialEvidence, RadioBus, ...) - new scripts/metaharness-clean-install-test.mjs: installs the declared ranges into a pristine temp dir and asserts every advertised export; wired as a MANDATORY clean-install job in metaharness-ci.yml - doctor: new "MetaHarness declared packages" check FAILS (not warns) when a declared optional dep does not resolve at runtime; --component metaharness now runs upstream + declared-deps + integration checks - MH_DARWIN_PIN bumped 0.8.0 → 0.8.3 in lock-step with the declared range Item 2 — strict promotion evidence at the transaction authority: - receipts now carry task-level pairedOutcomes (taskId + per-task baseline/ candidate scores) behind heldOutDeltas; verifyFlywheelReceipt refuses rows that cannot reproduce their aggregate; evaluateFlywheelCandidate populates them; pre-existing receipts still verify byte-identically - new flywheel-sequential-evidence.ts: anytime-valid e-process over discordant paired outcomes (testing-by-betting) composed with per-candidate alpha allocation (alpha_k = alpha_total * 6/(pi^2 k^2)), so the family-wise false-promotion probability across an ADAPTIVE candidate stream is bounded by alpha_total = 5% - promoteFlywheelCandidate requires paired evidence by DEFAULT — aggregate- only receipts are refused, never silently downgraded (the upstream withSequentialEvidence fallback hole); explicit --allow-aggregate-evidence / allowAggregateEvidence escape hatch for pre-upgrade receipts; alpha spend is persisted per receiptId in the transaction state (looking spends alpha; retries reuse their index) - acceptance test: 1,000 null-improvement streams (20 adaptive candidates x 40 worst-case discordant pairs) — measured family-wise false promotion 0.6%, within the <=5% budget Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01FKWLVWewYaWmcUg2q8Y9wy * chore: gitignore node-compile-cache build artifact Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01FKWLVWewYaWmcUg2q8Y9wy * fix(ci): satisfy #2561 guard, dep-overlap audit, and smoke contract for metaharness optional deps Three CI failures from the dependency-contract repair, three fixes: - tilde-pin @metaharness/{darwin,flywheel,radio} (~0.8.3 / ~0.1.10 / ~0.1.0) per the ADR-150 anti-caret rule enforced by smoke step 17z73 — upstream is unstable, tilde absorbs patches but never minors - remove the three from peerDependencies/peerDependenciesMeta: an entry in BOTH optionalDependencies and peerDependencies crashes npm 11.x arborist on dedupe (#1147/#2018 dep-overlap audit); optionalDependencies alone is the declaration that actually installs - update the #2561 cold-startup guard per its own escape clause: budget 8 → 10 and drop @metaharness/darwin from the forbidden list — that entry dated from the pre-0.8 heavy-tree era; darwin@0.8.3 is 1.8M unpacked with ZERO dependencies and no lifecycle scripts (all three packages combined: 2.3M, 753ms cold install into an empty dir, measured 2026-08-10; the clean-install CI job re-verifies on every pin change) - smoke.sh 17h now accepts the componentMap ARRAY form ('metaharness': [checkMetaharness, ...]) introduced with the declared-packages doctor check Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01FKWLVWewYaWmcUg2q8Y9wy * fix(ci): remove apostrophe that broke the single-quoted guard script The #2561 guard runs as `node -e '...'` inside bash; an apostrophe in a comment ("guard's") terminated the quoted string and bash tried to execute the next // comment line (exit 126, '//: Is a directory'). Verified by executing the extracted run block end-to-end: all three checks OK, exit 0. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01FKWLVWewYaWmcUg2q8Y9wy * feat(flywheel): ADR-381 — alpha-stream governance, reset epochs, accept/v2+seq Completes the sequential-evidence governance PR #2956 opened: ADR-381 (new) records the decisions: - the ADR-322 promotion ledger IS the alpha stream (per project root) — receipt lineageIds default to fresh UUIDs and cannot scope the control - evidence epochs: resetSequentialEvidence({confirm, reason}) archives the spend into an append-only sequentialResets audit trail, EXPIRES every outstanding evaluated receipt (fresh-data enforcement — old-epoch evidence cannot be replayed against a reopened budget), and increments evidenceEpoch; surfaced as `flywheel evidence-reset --reason … --confirm` (CLI) and the metaharness_flywheel MCP op, both behind the same policy gate as promotion - accept/v2+seq: the ADR-176 generations loop now decides each bundle with a third conjunct — the e-process over the bundle's own embedded per-task holdout at alpha_k for its position in the attempts stream — with the full verdict recorded in the bundle and independently replayed by verifyReceiptBundle (v1 bundles keep v1 semantics; versions pin per bundle); flywheelStatus surfaces next test index / threshold / minimum pairs / remaining budget so exhaustion reads as plateau, not mystery - two-layer pre-flight: evaluateFlywheelCandidate annotates (never blocks) when the promotion holdout cannot clear the next threshold on a perfect sweep; promoteFlywheelCandidate refuses size-inviable receipts BEFORE allocating an alpha index — sample size is ancillary, so the refusal looks at no evidence and spends no budget Tests: reset semantics (archive/expire/epoch/fresh-promote), alpha-free size refusal vs alpha-spending e-process refusal, v2 promote/reject/replay including tamper + index-shopping detection, v1 compat, helper math anchors (min pairs = 9 at test 1), budget monotonicity. 82 tests green across the eight affected suites; tsc build clean. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01FKWLVWewYaWmcUg2q8Y9wy * fix(flywheel): close 3 concurrency/epoch gaps in ADR-381 sequential evidence Code review of this PR found three real correctness bugs that can silently violate the <=5% family-wise false-promotion guarantee that is the PR's own core deliverable: - flywheel-transaction.ts: resetSequentialEvidence's "fresh data only" guarantee only expired receipts already registered at reset time; a receipt whose evidence predates a reset but is registered afterward was silently admitted into the new, cheaper epoch (index shopping). Now tracks evidenceEpochStartedAt and promoteFlywheelCandidate refuses any receipt whose payload.issuedAt predates it, before allocating an alpha index. - harness-flywheel-generations.ts: the daemon generations loop computed its sequential-evidence testIndex via an unlocked loadAttempts(root).length+1 read before appending. Two overlapping runFlywheelGeneration calls on the same root could be assigned the same test index and spend the same alpha_k twice. The read-index -> build-bundle -> append critical section now runs under the same O_EXCL lock pattern flywheel-transaction.ts already uses. - harness-flywheel.ts: evaluateFlywheelCandidate's sequentialPreflight (backing the `promotable` flag) was computed from an unlocked snapshot taken before the async retrieval/scoring work, so it could go stale by the time promoteFlywheelCandidate allocates the real index under its own lock. Moved to the latest possible read (after receipt registration) to minimize the window; documented as advisory, since promoteFlywheelCandidate remains the sole authority. Added a regression test for the epoch-boundary refusal and a concurrency test proving two racing generations get distinct sequential test indices. Co-Authored-By: RuFlo <ruv@ruv.net> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
fabcc9261a |
fix(hooks): Codex hooks.json schema + PreToolUse verdict compat (#2857)
Codex's plugin hook-manifest loader accepts only `description` and
`hooks` at the top level and hard-rejects the rest of the manifest on
anything else. plugins/ruflo-core/hooks/hooks.json and
plugins/ruflo-cost-tracker/hooks/hooks.json carried `_note` /
`_platform_note` documentation fields, so a fresh Codex install of
ruflo-core@ruflo (still true as of the currently-published 3.32.38 /
ruflo-core 0.2.5) fails to load the plugin at all with:
failed to parse plugin hooks config .../hooks.json:
unknown field `_note`, expected `description` or `hooks`
Fold the doc content into `description` (content preserved, no hook
commands changed) for both marketplace-listed manifests, and add a
strict top-level-key check to
scripts/audit-plugin-hooks-cross-platform.mjs so this class of
regression fails CI going forward. `.claude-plugin/hooks/hooks.json`
is POSIX-only and not marketplace-distributed (not what Codex fetches
for ruflo-core@ruflo); its doc fields are folded too but its
audit-script flags (`_platform`, `_legacy_unaudited_shim`) are kept.
Second, deeper bug once the manifest loads at all: `modify-bash`/
`modify-file` PreToolUse hooks always echo Cursor's
`{"permission":"allow"}` verdict. Codex's own strict output schema
(additionalProperties: false) rejects that shape outright and reports
"hook returned invalid pre-tool-use JSON output" on every single tool
call — verified directly against the real parser
(codex-rs/hooks/src/engine/output_parser.rs,
codex-rs/hooks/src/events/pre_tool_use.rs) and its generated JSON
schema. Empty stdout is the one input Codex's parser treats as
no-opinion/implicit-allow with no error. `isCodexPluginHost()` (added
for #2816, ruflo-core 0.2.5) already detects Codex via its
PLUGIN_ROOT/PLUGIN_DATA env vars and correctly suppresses the verdict
in that case — harden it with a `turn_id`-based fallback (a documented
Codex-only field always present in Codex's PreToolUse input JSON, per
codex-rs/hooks/schema/generated/pre-tool-use.command.input.schema.json)
so detection doesn't depend solely on those env vars being set.
Bump ruflo-core 0.2.5 -> 0.2.6 and ruflo-cost-tracker 0.26.2 -> 0.26.3
so Codex's per-version plugin cache invalidates and re-fetches the
fixed manifest instead of serving a stale cached copy indefinitely.
Fixes #2855, #2856.
|
||
|
|
401e02d511 |
fix: complete reports and consistent initialization for v3.32.37 (#2851)
* fix(metaharness): preserve readiness verdict payloads * test(metaharness): cover blocked genome verdicts * fix(adr): parse bullet metadata and relationships (#2659) * fix(adr): align adr-create with AgentDB schema (#2651) * fix(adr): make index updates idempotent (#2660) * fix(memory): bound session-end graph consolidation (#2628) * fix(memory): align active row visibility (#2652) * fix(memory): honor database path during init * fix(hooks): keep all shim fallback tags aligned * fix(codex): omit unbacked full-template skills * fix(init): generate complete native dual projects * test(memory): isolate path and legacy-row regressions * chore(release): prepare v3.32.37 |
||
|
|
67ff9898c0 |
fix: resolve current runtime and verification defects for v3.32.36 (#2850)
* fix(cli): make runtime status and learning signals truthful * fix(metaharness): accept padded scan severities * fix(codex): make core hooks and status skill native * fix(security): make witness verification hermetic * fix(metrics): expose grounded learning outcomes * fix(hooks): deduplicate project and plugin events * fix(verification): preserve security audit payloads * fix(runtime): stabilize dual memory skills and MCP schemas * fix(cli): harden helper signing and reflexion health * test(release): align helper and container invariants * chore(release): prepare v3.32.36 * test(funnel): align gates with cold-start seed pool * fix(ci): document MCP environment precedence |
||
|
|
9cf769ccce |
feat: ship adaptive swarm and resolve top runtime issues (#2848)
* feat: ship adaptive swarm and issue fixes * fix: register intentional runtime escape hatches |
||
|
|
ddd27576cb |
fix(release): ship Capability Brain as a self-contained 3.32.30 train (#2829)
* fix(release): bundle policy and Codex runtimes * fix(release): verify self-contained three-package archives |
||
|
|
b08246b04a |
chore(release): bump to 3.32.27 (#2823)
* chore(release): bump to 3.32.27 * chore(release): publish policy security dependency * chore(release): sign 3.32.27 helper manifest * ci: harden policy release gates |
||
|
|
0e3d412756 |
feat(flywheel): implement ADR-322 promotion loop (#2817)
Adds verified evaluation receipts, atomic compare-and-swap promotion, bounded Darwin/local proposer integration, CLI/MCP surfaces, ADR specifications, and the 3.32.26 release bump. |
||
|
|
db76d67235 |
fix(metaharness): pin @metaharness/darwin + pin-drift guard (ruflo analog of upstream #142/#149) (#2813)
* fix(metaharness): pin @metaharness/darwin + add pin-drift guard (ruflo analog of #142/#149) Upstream agent-harness-generator #142 (darwin caret-locked to 0.2.x, 3 majors behind) and #149 (META_PROXY_VERSION pinned with no watcher) are both silent pin-drift failures. Ruflo had the same latent gap on its own metaharness deps: - distill-oracle.ts invoked `npx --yes @metaharness/darwin` with NO version pin, so the Tier-1 mechanical oracle floated to whatever npm `latest` was — a breaking darwin release could change eval behavior mid-run. Pinned via a new `MH_DARWIN_PIN = '0.8.0'` constant used by all three npx call sites. - Declared `@metaharness/darwin: ^0.8.0` in optionalDependencies (subprocess- invoked, kept optional per ADR-150/321) so the pin has a single source of truth and installs cache-warm. - Fixed a stale `@metaharness/darwin@~0.3.1` doc comment (darwin is 0.8.0). New guard (the #149 analog ruflo lacked): - scripts/check-metaharness-pins.mjs — diffs each declared range (metaharness, @metaharness/router, @metaharness/darwin) against npm `latest`, plus a lock-step check that MH_DARWIN_PIN satisfies the declared darwin range. Exit 1 on drift; network flakes surface as a warning, never a false positive. - .github/workflows/metaharness-pin-drift.yml — runs the guard weekly + on PRs touching the pins; opens/updates a tracking issue when a pin falls behind, and hard-fails PRs that introduce drift. All pins are current (guard exits 0); router API-surface compat 9/9; CLI builds clean. Lockfile reconciled for the new optionalDep (frozen-lockfile verified). Refs: ruvnet/agent-harness-generator#142, #149; ADR-150, ADR-321. Co-Authored-By: RuFlo <ruv@ruv.net> * chore(release): bump to 3.32.25 (metaharness pin-drift guard) Co-Authored-By: RuFlo <ruv@ruv.net> |
||
|
|
27410d402b |
feat(security): ADR-320 — MCP Composition Inspector v2 + ChannelGuard v2 (#2791)
Follow-up from dream-cycle issue #2783 / PR #2784. Implements the v2 that the already-shipped v1 (commits 381b7ebcc/581cd2bf3) explicitly deferred: SimHash-based cross-tool fragment detection for MCP composition, and a ChannelGuard reusing the real InputValidator instead of a reinvented catalog. 29 new tests, 134/134 existing hooks tests passing, 0 regressions, clippy clean. |
||
|
|
469a901eda |
fix(bridge): wire bridgeRecordFeedback to real intelligence.recordTrajectory (#2786 fix-3)
The prior code called `learningSystem.recordFeedback`/`.record` and
`reasoningBank.recordOutcome`/`.record` — none of those methods exist
on the LocalSonaCoordinator / LocalReasoningBank instances the bridge
actually wires into the registry (see initializeIntelligence in
memory/intelligence.ts). Two silent `catch { /* API mismatch — skip */ }`
blocks swallowed every call. Feedback recording was 100% a no-op for the
CLI's default in-process intelligence path.
Real fix: call `intelligence.recordTrajectory(steps, verdict)` — the
same public API `hooks_post-command` already uses. It initializes
lazily, embeds the step, drives SONA + pattern distillation.
Also fixed the ReasoningBank pattern-store branch to call the ONE
method that actually exists on LocalReasoningBank (`.store(pattern)`)
with the correct StoredPattern shape.
E2E verified in a fresh scratch cwd (v3.32.10 CLI):
- `memory init`
- 3x `hooks post-task --task ... --store-results true`
- `hooks intelligence stats` — Neural Persistence reports 6 trajectories
on disk + 3 pattern entries. Before this fix: 0.
No mocks, no silent catches on the happy path.
Closes: #2786 fix-3 (the last flagged item from the 2026-07-26 tracker
sweep). Was tagged "architectural" because the sweep agent assumed the
target was the AgentDB LearningSystem/ReasoningBank; turned out the
registry wires the local intelligence classes instead, and the intelligence
module already exposes the correct public API. One-file surgical fix.
Co-Authored-By: RuFlo <ruv@ruv.net>
|
||
|
|
9810d8d9c1 |
fix(statusline): real model name from stdin + worktree version resolution
Fixes #2733, #2742. #2733 — hooks.ts's getUserInfo() hardcoded `const modelName = 'Opus 4.6 (1M context)'`, ignoring the actual active model Claude Code passes on stdin entirely. Cosmetically masked in the default render path (the generated .claude/helpers/statusline.cjs already parses stdin correctly and overrides this), but real for direct/manual `hooks statusline` CLI use or any stdin-parse failure in the wrapper. Fixed by mirroring statusline.cjs's own getModelFromStdin() approach inside hooks.ts, so the CLI subcommand is correct standalone. #2742 — getPkgVersion() in the generated statusline.cjs only probed CWD-relative paths (CWD/node_modules/..., CWD/v3/@claude-flow/cli/...). A linked git worktree has no node_modules of its own (worktrees don't get their own `npm install`), so every probe missed and the version silently fell back to the baked-in default from whenever the helper was last generated. Fixed with a pure-fs worktree-root resolver: a linked worktree's `.git` is a plain FILE containing `gitdir: <main>/.git/ worktrees/<name>`; walk up from CWD, parse the pointer, strip the trailing segment to recover the main repo root, and probe its node_modules/v3 paths too. No `git rev-parse` spawn (statusline renders are latency-sensitive). Caught and fixed a real bug in this same resolver during testing: git writes the gitdir pointer with forward slashes even on native Windows, so a path.sep-based (backslash) marker search silently never matched — normalize to forward slashes before searching. `v3/@claude-flow/cli/.claude/helpers/statusline.cjs` is the source of truth per the #2679 redesign (generateStatuslineScript() reads it and substitutes two tokens); regenerated the propagated root-level copy via scripts/regen-statusline-artifact.mjs, which also needed a small cross-platform fix (dynamic import() of a raw Windows path isn't a valid ESM specifier — wrap with pathToFileURL()). Also corrected a stale/misleading comment in message-transport.ts claiming a fresh install "shows the in-code fallback pool until the first refresh lands" — there has never been an in-code fallback pool since ADR-311 ("zero local promo content"); a fresh install's promo row is genuinely empty until the first background refresh lands. Found while investigating a "promo doesn't show on new installs" report; the underlying fail-closed design and SessionStart-triggered refresh mechanism were confirmed working as intended (this machine's own ~/.ruflo state shows a healthy, recently-rotated promo history) — only the comment was wrong, not the behavior. Left alone (separate, unreferenced, ~500-line legacy implementation predating the #2195 delegation rewrite, no source references in its own package): v3/@claude-flow/mcp/.claude/helpers/statusline.cjs. Verified: 21/21 new + existing statusline tests pass, 122/122 funnel tests pass, 10/10 hooks tests pass, clean tsc --noEmit. Manually confirmed #2733 (real stdin model name renders; malformed/empty stdin falls back to "Claude Code", never the old hardcoded string) and #2742 (a real `git worktree add` scenario resolves the main repo's version instead of falling back) end-to-end. |
||
|
|
0a110aee9f |
fix(audit): register #2721's test-only hook env vars as escape hatches
RUFLO_HOOK_CLI_OVERRIDE and RUFLO_HOOK_DEBUG_STDOUT (both added to plugins/ruflo-core/scripts/ruflo-hook.cjs to let test-hooks.mjs point at a local CLI build and observe its output) tripped the env-var-precedence audit's "CLI flag must win" requirement. Same category as the existing RUFLO_HOOK_SKIP_NPX entry: hook scripts have no CLI-flag surface to attach to (invoked by hooks.json, never a user-typed command), and both are test-only — production never sets them. |
||
|
|
b68ad4ccba |
fix(plugins): make ruflo-core/ruflo-cost-tracker hooks Windows-native (#2721)
Both plugins' hooks.json wrapped every command in `/bin/bash -c '...'`,
which fails outright on native Windows (no such path) -- Codex/Claude
Code report "PreToolUse hook (failed) -- exit code 1" on every tool
call. The `_platform: posix` / "ruflo init overrides this on Windows"
claim in both files was never actually true: Claude Code merges
plugin-declared hooks additively with any init-generated
.claude/settings.json, it doesn't replace them, and there's no `ruflo
init` step at all in the reported Codex marketplace install flow.
Fix: every hook command is now a `node -e` bootstrap that resolves
plugins/*/scripts/ruflo-hook.cjs from process.env.CLAUDE_PLUGIN_ROOT
inside Node -- no shell env-var expansion (${VAR} vs %VAR%), so the
exact same command string runs unchanged on Windows/macOS/Linux.
ruflo-core's ruflo-hook.cjs (previously a full port of ruflo-hook.sh
that existed on disk but was never referenced by hooks.json) gained:
- JSON parsing of the hook event from stdin (replaces jq) for
post-command/post-edit, deriving the same CLI flags the bash
version computed
- the PreToolUse permission-allow stdout echo Cursor's stricter
contract requires (previously only the bash wrapper's trailing
printf did this)
- precompact-manual/precompact-auto guidance text (previously plain
bash echoes, no CLI call)
- a real Windows shell-quoting fix: shell:true with an args array
does NOT quote array elements, so "echo hi" silently truncated to
"echo" and a heredoc's `<<` errored as unexpected -- skip the
shell entirely for `node` invocations (never a .cmd shim, so
CreateProcess gets the argv array byte-for-byte)
cost-tracker's existing ruflo-hook.cjs (already correct, just
orphaned) needed no logic changes, only wiring.
Also:
- corrected the false "_platform_note" claims about ruflo init
overriding plugin hooks
- hardened scripts/audit-plugin-hooks-cross-platform.mjs: a
POSIX-exempt hooks.json now must actually reference its sibling
.cjs shim, not just have one sitting on disk unreferenced (which
is exactly the shape cost-tracker shipped in undetected)
- added windows-latest to the plugin-hooks-smoke CI matrix (it was
ubuntu/macos-only because the old bash-based hooks.json couldn't
run on Windows at all) and rewrote test-hooks.mjs to drive hooks.json's
literal command strings via `shell: true` -- exactly how Claude
Code/Codex invoke them -- instead of wrapping everything in an
explicit `bash -c` that could never have caught this bug
- flagged (not fixed) a separate, currently-published, actively
maintained plugin package (.claude-plugin/ + plugin/, the older
"claude-flow" plugin, not listed in the ruflo marketplace) with
the same underlying bug via jq/xargs pipes instead of bash --
explicitly marked _legacy_unaudited_shim so the hardened audit
doesn't silently regress on out-of-scope work
Verified locally on native Windows (this fix's actual target
platform): all 17 ruflo-core hook cases pass, all 3 cost-tracker
cases pass, the existing 12-case smoke-ruflo-hook-cjs.mjs passes
unchanged, both hook-command audits pass clean.
Fixes #2721
|
||
|
|
a4a7d99c22 | test(statusline): document hook-only identity setting | ||
|
|
1fb874005c | test(plugins): preserve standalone MCP catalog assertions | ||
|
|
5e66f065e9 | test(plugins): align namespace and stable hook shims | ||
|
|
e332689b8c | fix(release): harden hooks, statusline, security, and plugin MCP integration | ||
|
|
6d685574b9 | test(agentbbs): allow lazy runtime loading | ||
|
|
72875da937 | chore(release): prepare stable Ruflo 3.32.1 | ||
|
|
d20f1323b1 | fix(codex): ship stable Windows-safe Ruflo integration | ||
|
|
4a37e55173 |
fix(#2677): doctor memory — integrity + content + embedding-coverage checks (stuinfla checks 1-3) (#2681)
* fix(#2677): doctor memory — add integrity + content + embedding-coverage checks Closes half of #2677 (@stuinfla's proposed checks 1–3; check 4 recall probe deferred to a follow-up since it needs end-to-end write+search+delete). ## Problem Before: `ruflo doctor --component memory` was existsSync + statSync — literally CANNOT fail on any file that exists. Reported PASS on a 99.97%-empty and even a SQLite-malformed DB (both from stuinfla's 81-store fleet report). "A check that cannot fail protects nothing." ## Fix — 3 new checks stacked on the existing existence probe Ordered so the earliest chain-break is always the first red the user sees: 1. **Memory Integrity** — opens via sql.js, runs `PRAGMA integrity_check`. Two failure modes handled distinctly: - Open fails / query refused: WARN (encrypted DB or corrupt — doctor can't distinguish from outside) - Open succeeds but pragma != 'ok': FAIL (definite corruption) 2. **Memory Content** — counts rows in `memory_entries` where `content` is non-empty. FAIL if ratio < 95%. Message includes the actual ratio ("content 3/11133 (0.03%)") per stuinfla's "Print the measurement, not a checkmark" rule. 3. **Memory Embedding Coverage** — counts rows with a vector on populated-content rows. FAIL if ratio < 95%. Schema-shape discovery handles the two known variants (inline `embedding`/`vector` column, or a paired `embeddings`/`vector_indexes` table). ## Design rules honored (from #2677) - **A check that cannot fail protects nothing** — every check has a demonstrable red state, verified live against this repo's DB (which happens to be broken enough to WARN on all three). - **UNKNOWN is never PASS** — encrypted / unreadable / schema-mismatch cases return warn or fail, never a reassuring pass. Encrypted-DB case gets an operator-actionable "DB refused query: … (encrypted DB or corruption)" message that names what the user needs to know. - **Print the measurement** — ratios in-line ("content 3/11133 (0.03%)"), not just checkmarks. - **Exit non-zero on failing dimension** — inherits from the existing doctor exit-code aggregation. ## componentMap now supports arrays `--component memory` runs the whole memory-health suite instead of a single check. Type of map values widened to `(() => Promise<HealthCheck>) | Array<() => Promise<HealthCheck>>`; `checksToRun` uses `Array.isArray()` at expansion time. No behavior change for the other 24 components — all still single functions. ## Verified live on this repo Before: `✓ Memory Database: 0.00 MB — 1 passed. All checks passed! System is healthy.` After: `✓ Memory Database` + 3 × ⚠ (integrity refused / content unknowable / embedding unknowable) with the diagnostic message on each. ## Deferred to follow-up - Check 4 (recall probe): needs actual write+search+delete round-trip through the CLI's memory pipeline. Slower + higher-blast-radius than read-only SQL. Its own PR. - Checks 5–8 (distillation, reflexion, skills, continuity) + the underlying distill→reflexion write-path fix stuinfla identified. Bigger change; his own PR proposal per issue thread. Closes half of #2677. Full closure requires stuinfla's second PR. Co-Authored-By: RuFlo <ruv@ruv.net> * ci(env-audit): allowlist RUFLO_AI_BUDGET_{DIR,DISABLE} + RUFLO_METAHARNESS_SKIP_LOCAL These env vars ship in main from PR #2663 (repo-supervisor + AI-cost fuse) and the metaharness invoke shim, but were never added to KNOWN_ESCAPE_HATCHES — so the env-var-precedence audit fails on every PR that touches related files. Drive-by fix while adding the doctor memory checks in PR #2681. Each entry follows the existing allowlist convention: rationale comment naming the file + why it's intentionally env-only (background service / plugin script — no CLI-flag surface to attach to). (Local audit surfaces ~12 more missing entries but the CI flagged only these 3 for this PR — the rest are pre-existing debt on main and warrant their own cleanup pass.) Co-Authored-By: RuFlo <ruv@ruv.net> * ci(env-audit): also allowlist RUFLO_HELPERS_LOCKED (v3.30.0 .LOCKED escape hatch) The env-audit's fail-level 'x' now surfaces our own new escape hatch from PR #2676 (.LOCKED marker + env opt-out for the concurrent-session helper clobber fix). Adding to KNOWN_ESCAPE_HATCHES with rationale. Follows the same pattern as the other 'runs from a hook, no CLI surface' entries above — helper-refresh is invoked from a session-restore hook, not a user-typed command, so there's no argv position for a --helpers- locked flag to attach to. Co-Authored-By: RuFlo <ruv@ruv.net> |
||
|
|
30e41c23f3 |
fix(#2679): sync statusline-generator with committed helper (read+substitute pattern) (#2680)
* fix(#2679): sync statusline-generator with committed .claude/helpers/statusline.cjs Closes #2679. Replaces the 946-line inline template string in generateStatuslineScript() with a read-committed-helper-and-substitute pattern. Single source of truth = the shipped .claude/helpers/statusline.cjs. ## Root cause of the drift The generator's inline template shipped as the pre-#2195 non-delegation build. Meanwhile the deployed helper got continuous v3.29.0 improvements (whole-row-clickable OSC 8, (domain) suffix, ellipsis on truncation, bright-white bold command styling, 300s cache TTL, windowsHide on every subprocess spawn). No mechanism kept them in sync — every `ruflo init` regressed a user's install to the older shape. ## Fix generateStatuslineScript() now: 1. Walks up from `__dirname_sg` looking for `@claude-flow/cli`'s package.json. Reads `.claude/helpers/statusline.cjs` relative to that. Falls back to `createRequire.resolve` for the installed-CLI case where walk-up misses (shipped tarball extracted outside a workspace). 2. Substitutes two known values: - `maxAgents: 15,` → `maxAgents: <options.runtime.maxAgents>,` - `let ver = "…";` → `let ver = <bakedVersion>;` (with non-downgrade guard so test environments that resolve to an older node_modules install can't clobber a fresh committed helper) Function body: ~1050 lines → ~60 lines. Every future edit to the helper now propagates to init output automatically. ## Also synced (was silently drifted) `v3/@claude-flow/cli/.claude/helpers/statusline.cjs` was a stale pre-v3.29.0 copy. Copied the root `.claude/helpers/statusline.cjs` (the current v3.29.0 shape) over the top. Re-signed helpers manifest. ## Test updates Three test assertions updated to match the current v3.29.0 helper shape: 1. `statusline-cost-display.test.ts` drift guard — restored to strict byte-comparison after normalizing the version line (the ONE legitimately environment-dependent line — vitest resolves to a different pnpm-installed CLI than production). 2. `funnel.test.ts` "re-sanitizes promo text" — was asserting the old `.slice(0, 100)` truncation; now asserts the v3.29.0 MAX_LEN + ellipsis path. 3. `funnel.test.ts` "styles the label as a link" — was asserting the old `UL_ON + safeTerminalLink(label, promo.url) + UL_OFF` label-only OSC 8 wrap; now asserts the v3.29.0 whole-row wrap via `wrapWholeRowInHyperlink` + the bright-white bold command styling. ## Verified - Local: 135/135 pass across funnel.test.ts + statusline-cost-display.test.ts. - TypeScript build clean. - Direct node call: generator produces exact byte-for-byte match with committed helper (modulo the substituted maxAgents + version). ## Follow-ups tracked None blocking. The `wrapWholeRowInHyperlink` name assertion pins one implementation detail — worth revisiting if we ever refactor that path. Related: v3.30.0 release ( |
||
|
|
215840536e |
fix(daemon): opt-in AI workers, global launch budget, cross-worktree dedup, stop --all (#2661) (#2662)
* fix(daemon): make AI workers opt-in, add user-global launch budget + stop --all (#2661) Containment patch for the P0 worktree-daemon fanout: N worktree daemons each auto-enabled headless claude --print workers whenever the Claude CLI was on PATH, so autonomous quota use scaled linearly with worktree count (13 launches/hour/daemon) and could silently exhaust a user's Claude quota. - AI workers are now OPT-IN (invariant 1: default install → zero autonomous Claude launches). Consent via `daemon start --headless`, `daemon.aiWorkers.enabled: true` in .claude-flow/config.json, or RUFLO_DAEMON_AI_WORKERS=1. Without it the daemon never probes `claude --version` and every worker runs its $0 local path. - `--headless` is now a real authorization gate: threaded into DaemonConfig.aiWorkersEnabled and re-checked in runWorkerLogic (defence in depth); consent is never restored from stale daemon-state.json. `daemon trigger --headless` grants per-run consent. - New user-global AI budget (services/global-ai-budget.ts): every launch must atomically reserve a slot in a shared owner-only ledger under ~/.claude-flow before any process is created. Defaults: maxConcurrentGlobal 1, 2 launches/hour, 12/day. Quota/429 failures open a user-global circuit breaker (60 min) so ALL daemons stop retrying. Every launch/denial/pause emits a receipt (no prompts/source persisted). Escape hatch: RUFLO_AI_BUDGET_DISABLE=1; limits tunable via RUFLO_AI_MAX_PER_HOUR / _PER_DAY / _CONCURRENT. - `daemon stop --all`: stops every ruflo daemon across all workspaces/worktrees (positive argv identification only), SIGTERM-first so each daemon's shutdown path reaps its Claude process group, then removes only ruflo-owned PID files. `stop()` now cancels in-flight headless children; cancel/cancelAll signal the whole process group. - `daemon start` warns when multiple daemons are running across worktrees; `daemon status` shows the AI-consent state of the RUNNING daemon; `daemon status --all` shows global AI budget usage and the circuit-breaker state. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01AHD8okFGN5JhCgu1MJmeUS * feat(daemon): repository identity, cross-worktree job dedup, worktree-removal shutdown (#2661) Root-fix phase 2 for the worktree daemon fanout — separates worktree identity from repository identity and stops duplicate model work across worktrees (issue invariants 5 and 6, containment form). - New git-workspace-identity service: resolves worktreeRoot (rev-parse --show-toplevel), commonGitDir (rev-parse --git-common-dir), a stable repositoryId (sha256 of the canonical common git dir — shared by ALL worktrees of one repository), and HEAD. Non-git dirs degrade to a path-hash id. Stable parts cached per process; HEAD never cached. - New ai-job-dedup registry (~/.claude-flow/ai-jobs.json, owner-only, symlink-rejecting): before a model launch the executor computes jobKey = sha256(repositoryId, HEAD, workerType, configHash) and skips the launch when the same job succeeded within the freshness window (the worker's own interval, floor 10 min; RUFLO_AI_DEDUP_WINDOW_SECS to tune, RUFLO_AI_DEDUP_DISABLE=1 to turn off). HEAD moves -> new key -> job runs again. Dedup is best-effort UNDER the budget: the atomic budget reservation remains the hard launch bound. Skips happen before budget spend, return dedupSkipped results, and never overwrite persisted metrics or fall back to redundant local runs. - Lifecycle monitor now always runs: a daemon whose workspace/worktree directory was removed self-shuts within one 60s check interval, even with ttl/idle disabled (previously the monitor was skipped entirely when both were 0). Tick logic extracted into a testable predicate. - daemon status --all budget panel now attributes 24h launches per workspace, so the worktree spending the shared budget is identifiable. Verified end-to-end with two real git worktrees: a success recorded at HEAD in worktree A makes worktree B's executor skip pre-spawn with zero budget spend; a new commit in B bypasses dedup and proceeds to the budget gate. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01AHD8okFGN5JhCgu1MJmeUS * fix(security): register #2661's new daemon/dedup env vars in the precedence audit RUFLO_AI_DEDUP_DISABLE and RUFLO_AI_DEDUP_WINDOW_SECS are background daemon tuning knobs with no CLI command surface to attach a flag to. RUFLO_DAEMON_AI_WORKERS does have CLI-flag precedence (`daemon start --headless`), but it's wired via constructor-injected config in commands/daemon.ts rather than a same-file check the audit's local-context heuristic can see from worker-daemon.ts, where the env read actually lives. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
d0ac73d798 |
feat: Cognitum customer lifecycle funnel — ADR-301..310 + implementation (#2622)
* docs(adr): add ADR-301..305 — Cognitum customer lifecycle funnel New 300-series ADRs defining the ruflo → cognitum.one conversion funnel: - ADR-301: promotional status surface (rotating bottom status row; reaches existing installs via the ADR-174/177 signed helper auto-refresh channel) - ADR-302: one-time post-init capability enrollment prompt - ADR-303: contextual credit-exhaustion recovery/upgrade experience - ADR-304: optional local Meta LLM proxy (localhost:11435 → api.cognitum.one, OpenAI-compatible, local-first) - ADR-305: funnel overview, design principles, success + guardrail metrics Also indexes the new series in the ADR README. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_013FqZGy1zZ49JivKUSyqDKH * docs(adr): harden ADR-301..305 per review — trust and funnel-integrity gates - ADR-301: signed content boundaries — messages are schema-validated data only, length-bounded, URL-allowlisted, expiry-cached, and sanitized so copy can never emit terminal control sequences - ADR-302: split enrollment into four independent consent domains (account, proxy-install, telemetry, cloud-routing) with versioned consent receipts; acceptance authorizes auth login only - ADR-303: deterministic error taxonomy with explicit provider codes and a fail-closed classifier; no text-matching of provider messages - ADR-304: mandatory data-plane disclosure before cloud routing; proxy installs local-only, cloud routing off until explicit opt-in - ADR-305: attribution rules (event vocabulary, anonymous rotating funnel ID, 7/30-day attribution windows, 90-day retention, verifiable deletion), ordered gate hierarchy with hard integrity/ health/trust gates, automatic circuit breaker via the signed config channel, and an end-to-end acceptance test for existing installs Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_013FqZGy1zZ49JivKUSyqDKH * docs(adr): amend ADR-301/303/304/305, add ADR-306..310 — deployable funnel system Amendments: - ADR-301: existing-install disclosure gate (never promo before disclosure, once per user, decline disables all surfaces), 4:1 educational:promotional content ratio, 30-min repeat cap, no dark patterns, marquee off for screen readers/reduced motion, control precedence chain - ADR-303: Cognitum account service as sole credit authority (GET /v1/credits), canonical CreditErrorCode enum; only COGNITUM_CREDIT_EXHAUSTED triggers the funnel surface - ADR-304: retitled as the proxy *product* ADR; explicit relationship to the internal metallm dev-bridge (shared routing core, versioned compat layer, no implicit contract dependency); runtime deferred to ADR-307 - ADR-305: kill-switch claim corrected — release-bound vs opt-in freshness mechanisms with honest latencies, 24h revocation TTL, last-valid-signed-policy failure mode; normative control precedence (env > enterprise > user > default > remote); two measurement planes (CI release qualification enforces gates; production analytics measures adoption only) New ADRs: - ADR-306: Cognitum auth — PKCE + device flow, keychain-only refresh tokens, incremental scopes mapped 1:1 to consent domains - ADR-307: proxy runtime — Rust single binary, loopback bind + per-user token, platform user services, explicit lifecycle commands, signed no-self-update packaging - ADR-308: versioned public API contract — OpenAPI in both repos, idempotent event ingestion, normative client failure policy - ADR-309: governance — npm-policy commitments, GDPR/CCPA basis, constrained closed event schema with timestamp bucketing, content approval ownership - ADR-310: staged rollout phases 0-6, promotion criteria, three- package rollback discipline, CI gate matrix, final acceptance test Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_013FqZGy1zZ49JivKUSyqDKH * feat(funnel): implement ADR-301/302/303/305 funnel system with release gates Core module (v3/@claude-flow/cli/src/funnel/, local-only, zero network I/O): - precedence.ts: ADR-305 control chain (env > enterprise policy > user config > project config > package default > remote policy); a lower source can never re-enable a higher disable - disclosure.ts: ADR-301 existing-install disclosure gate (never_seen / disclosed_enabled / disclosed_disabled), 72h grace window before any message, decline disables all surfaces - messages.ts: signed-content boundaries: schema validation, 80-col grapheme-aware bound, exact-host URL allowlist, expiry, and hard drop of any C0/C1/ANSI/OSC/bidi sequence (drop, never repair) - rotation.ts: deterministic scheduler; promo only in 1-of-5 slots, 30-min repeat cap, educational fills all other slots - consent.ts: ADR-302 versioned receipts for 5 separate domains; grant AND decline recorded; stale policyVersion means re-ask, never carry forward - credit-errors.ts: ADR-303 CreditErrorCode enum + fail-closed classifier over machine-readable provider codes (never message text); only COGNITUM_CREDIT_EXHAUSTED fires the recovery surface, once per session - events.ts: ADR-309 closed event schema, daily timestamp buckets, bounded local queue, telemetry-consent-gated, opt-out deletes ID+queue - enrollment.ts + promo.ts: surface orchestrators Surface integrations: - hooks statusline --json now computes a promo field (all gates run server-side; failures never break the statusline) - generated statusline.cjs renders the bottom promo row with defense-in-depth re-sanitization + env/CI gates at render time; repo-root artifact regenerated (drift guard green) - init: one-time post-init capability enrollment (--no-signup flag, CI/non-TTY auto-skip, records only the account consent domain) - new command: ruflo funnel status|disable|enable|id - doctor gains a funnel component reporting effective state + deciding precedence source (enterprise audit verification) Contracts and gates: - v3/docs/api/cognitum-v1.openapi.yaml: ADR-308 v1 contract (8 endpoints, error taxonomy mirrored 1:1, normative failure policy, signed policy feed schema with 24h TTL cap) - .github/workflows/funnel-gates.yml: ADR-310 hard gates: invariant suite + runtime suppression proofs (promo in CI = 0, RUFLO_FUNNEL=0 suppression, opt-out persistence, non-interactive init) - __tests__/funnel.test.ts: 50 tests mapping 1:1 to the ADR-310 gate list Verified: cli-core/swarm/memory/neural/security/cli builds green; funnel suite 50/50; statusline-cost-display drift guard + runtime 8/8; init-wizard-bugs and p1-commands green; live smoke of the funnel lifecycle, statusline promo gates (fresh shows disclosure, CI shows nothing, RUFLO_FUNNEL=0 shows nothing, post-grace rotates educational), doctor component, and init under CI. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_013FqZGy1zZ49JivKUSyqDKH * fix(funnel-gates): mirror the green Build V3 setup (pnpm 8, frozen lockfile, workspace build) The runtime-suppression job failed on the PR because pnpm 9 with a filtered install resolved the bare-semver workspace deps (@claude-flow/cli-core etc.) from the npm registry instead of linking the workspace packages, so the cli tsc build could not find cli-core's dist. Copy the exact setup the passing Build V3 job in v3-ci.yml uses: pnpm/action-setup@v6 pinned to pnpm 8, pnpm install --frozen-lockfile over the full workspace, and a workspace-wide pnpm build. Also install pyyaml explicitly before the OpenAPI parse check. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_013FqZGy1zZ49JivKUSyqDKH * chore(helpers): pending-insights runaway-storage guard in intelligence.cjs Captured from the session's self-learning hook runtime: recordEdit now caps the append-only pending-insights file at ~512KB by keeping the most recent 2000 lines when consolidation has not drained it, so it can never grow unbounded. Non-fatal, statSync-per-edit cost only. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_013FqZGy1zZ49JivKUSyqDKH * feat(funnel): rotating sponsor disclosure, statusline redesign, auto-update ## Statusline (client render) Reorganized 4 rows by purpose (Architecture / Runtime / Health / AgentDB): - suppress zero/healthy fields; glyph-only for 🧠 memory % and 💾 RAM MB - drop DDD % duplicate, drop ADR denominator, drop opaque "◆ DB" marker - "Target 150x-12500x" → "Goal 150x-12500x" (plain language) - Health row hides on NONE; collapses to "✓ Security ✓ No CVEs" when clean, reads "N vulnerabilities" (not "CVE checks 0/3") on unfixed findings - kind-based row color: brightCyan for disclosure, brightPurple for promotional sponsor, yellow for educational — reads as *what it is* ## Funnel promo row (ADR-301/305/309) - Rotating disclosure: 3 copy variants on a deterministic 5-min slot with a shared Cognitum sponsor URL. Neutral framing — no "Sponsored by"/ "Powered by" (would read as advertising and lose OSS trust). - OSC 8 hyperlink wrapper: allowlist ships in renderer (cognitum.one hosts only), https-only, TMUX/CI/TERM=dumb suppression, kill-switch env var, defense-in-depth label re-sanitization. Broken URL / non-https / non-allowed host → plain label, never a raw escape. - Attribution: attributionUrl() decorates every outbound funnel link with utm_source=ruflo · utm_medium=statusline · utm_campaign=<kind> · utm_content=<msg-id>. Client-side link builder, ZERO runtime network call — API-down has no effect on render. `fid` (pseudonymous funnel ID) appended only under telemetry consent (ADR-309 privacy default). - Defense in depth: attributionUrl rejects non-https schemes at build time in addition to renderer's OSC 8 allowlist. ## Auto-update for existing installs - statusline.cjs added to CRITICAL_HELPERS + executor.ts + sign-helpers.mjs. - helper-refresh.ts fallback generates statusline.cjs when the installed package isn't resolvable. - Regen script scripts/regen-statusline-artifact.mjs keeps root artifact in lockstep with the generator (drift-guard test enforces). PUBLISH-TIME REQUIRED (only ruvnet-owned key can do): re-run scripts/sign-helpers.mjs with the Ed25519 signing key so helpers.manifest.json includes statusline.cjs's SHA-256. Without it, auto-refresh fail-closes (intended: tampered helper refuses install). ## Tests: 50 → 57 (+ statusline-cost-display drift guard passes) - parameterized invariant over every DISCLOSURE_TEXTS variant - sponsor URL shape (https + cognitum.one) - 5-min rotation determinism - attributionUrl: UTM shape, absent fid without consent, non-https rejection, malformed base URL verbatim - API-down design lock: promo.ts contains no fetch/https/XMLHttpRequest Co-Authored-By: RuFlo <ruv@ruv.net> * fix(statusline): promo row flicker — stale-while-revalidate cache The funnel promo row would appear on cache warm-up then vanish on the next render whenever the 60s cache TTL expired mid-session and the CLI call either failed, timed out, or was resolved to a bin that returned no promo. Root cause: getStatuslineData() fell through to buildLocalFallback() on CLI failure, and the fallback returned a stub object with no `promo` field. Fix: stale-while-revalidate on the /tmp cache. - readCache() now returns { fresh, data }. The TTL only decides whether the data is fresh; the payload comes back either way. - getStatuslineData(): fresh cache → return immediately. Else try the CLI. On CLI failure, if we have any prior cache data, re-apply local overlays and return it. Only if we've never had a good CLI response do we fall to buildLocalFallback(). Effect: the promo row survives CLI hiccups, timeouts, and cache-expiry mid-render. Live verified: cache backdated 5 min, all 3 CLI bins disabled — row still renders the last known disclosure. Test: pins the generator template shape so a future edit breaking the readCache freshness split or the stale-fallback path trips CI. 57 → 58. Co-Authored-By: RuFlo <ruv@ruv.net> * fix(statusline): 3-line design + persistent promo memo Two related fixes for observed live-session bugs. ## 3-line design (promo as line 1) Claude Code truncates the statusline past line 4 with the system-guidance / input-prompt line. The previous 6-row layout (header + separator + 4 purpose rows + promo) put the funnel row on line 6, so it was silently dropped by the viewport. New layout, exactly 3 lines: Line 1 — Promo / disclosure (funnel row, ADR-301 surface) Line 2 — Header (version · git · model · timing · ctx · cost) Line 3 — Compressed ops (Swarm · Hooks · 🧠 · 💾 · Health) Everything on the previous 4 individual rows is now on the ops line, dot- separated. Diagnostic detail (Domains, ADRs count, vectors, MCP count, DB marker) moves behind `ruflo status --verbose` per the earlier design discipline you documented ("Show healthy state only when it confirms readiness. Show zero values only when zero is actionable. Show failures immediately."). ## Persistent promo memo Even with stale-while-revalidate cache, promo would blink out when an older `@claude-flow/cli` cached by npx succeeded but returned JSON with NO promo field. Cause: the older version predates ADR-301, so its statusline JSON schema is missing our field. The cache path only fires when the CLI THROWS — a promoless success clean-writes. Fix: `~/.ruflo/statusline-promo.json` — a 6h-TTL memo of the last known good promo. Every successful render writes it. Every code path (fresh-cache / CLI-success / stale-cache / cold-fallback) calls `overlayMemoPromo(data)` which injects the memo IFF `data.promo` is missing. The funnel row now survives: - fresh cold cache + working CLI ← memo not needed - stale cache + CLI timeout ← stale-while-revalidate path - cache wiped + CLI down ← memo overlay (proven live) - working CLI, promoless response ← memo overlay (the new fix) Live proof: cache wiped, all 5 known `cli.js` bins renamed to `.disabled`, render still emits the 3-line output with promo on line 1. ## Tests: 58 → 60 in funnel suite, 66 → 68 total Pinned in the generator template: - 3-line ordering (promo push before header push before ops push) - PROMO_MEMO_FILE + readPromoMemo + overlayMemoPromo helpers exist - overlay is called on every code path (spot-checked by call count) Co-Authored-By: RuFlo <ruv@ruv.net> * fix(statusline): 3-line order — RuFlo header · ops · promo (top-to-bottom) Per user preference: RuFlo header must be line 1 (not the promo/disclosure row). Reordering the render: Line 1 — Header (▊ RuFlo · git · model · timing · ctx · cost) Line 2 — Ops (Swarm · Hooks · 🧠 · 💾 · Health) Line 3 — Promo (funnel disclosure / educational tip / sponsor) Promo stays within the visible 3-line viewport so Claude Code doesn't truncate it with the system-guidance line. Memo + stale-while-revalidate safety net from the previous commit is unchanged — promo remains visible even when the CLI returns a promoless response. Test invariant updated to pin the new order (header push < ops push < promo push in the generator source). 68/68 tests still pass. Co-Authored-By: RuFlo <ruv@ruv.net> * feat(statusline): underline the promo label as a clickable CTA The sponsor / disclosure row currently reads as flat text — users don't know it's clickable. Two visual affordances added: 1. ANSI underline ([4m / [24m) on the label portion so terminals show it as a clickable link even when they don't render OSC 8 hyperlinks (a universal cue: underline = interactive text). Combined with the existing OSC 8 wrap, modern terminals get a real hyperlink; legacy terminals get a visual affordance. 2. The trailing "· disable: ruflo funnel disable" instruction now renders dim + non-underlined + outside the OSC 8 wrap. It reads as metadata ("here's how to turn it off") rather than as part of the link target (which would falsely imply that clicking anywhere on that text goes to Cognitum). Split happens on the exact anchor " · disable" — educational tips with no disable tail keep their whole text rendered plainly (no underline, no OSC 8, since they have no URL). Test locks the split-and-underline pattern in the generator template so a future refactor that reunifies the row trips CI. 60 → 61 funnel tests · 68 → 69 total. Co-Authored-By: RuFlo <ruv@ruv.net> * feat(funnel): ADR-308 event transport + Cloud Function reference impl Phase 2 client-side telemetry + reference server. Wires the funnel event queue (v3/@claude-flow/cli/src/funnel/events.ts already exists) to the Cognitum analytics endpoint via a fail-silent HTTP batcher, plus an out-of-band credit-exhaustion notifier. ## Client transport (v3/@claude-flow/cli/src/funnel/event-transport.ts) ADR-308 contract: - POST /v1/events with Idempotency-Key header (UUIDv4 per batch) - Consent-gated: zero network activity when `telemetry` consent is off - Bounded batches (MAX_BATCH = 100), exponential backoff on failure, capped at 30x base interval (~30 min) - 4s POST timeout — telemetry never stalls the CLI - Rejects non-https endpoints inside the transport (defense in depth on top of the client-side allowlist) - Credit-exhaustion detection: 402 response OR body containing the exact ADR-303 machine-readable code triggers the notifier ## Credit notifier (v3/@claude-flow/cli/src/funnel/credit-notifier.ts) Persistent user-visible flag at ~/.ruflo/credit-status.json: - markCreditExhausted() — idempotent, stable `since` timestamp - clearCreditStatus() — stamps `cleared`, drops the exhausted flag - creditExhaustedNotice() — user-facing single line with humanized age - readCreditStatus() — inspector for status/tools ## Server reference (services/cognitum-analytics/) Cloud Function reference implementation: - index.js — POST /v1/events handler, Firestore writes to funnel_events + funnel_aggregates + funnel_credit + funnel_idem collections - package.json — Node 22 + functions-framework + Firestore SDK - deploy.sh — gcloud CLI deploy (gen2, us-central1, 402-on-cap ceiling) - sample-batch.json — fixture for smoke tests - README.md — layout, local run, deploy, verification DEPLOY IS HUMAN-DRIVEN. The autonomous loop does not run deploy.sh — that requires a live gcloud session with cloudfunctions.admin on cognitum-20260110 and is outside the hard-fence for automated actions. Everything up to the deploy is scripted; the human runs `./deploy.sh` when ready. ## Tests: 68 total funnel tests (61 → 68, +7 new) - Transport defaults (https + batch cap + backoff floor + timeout ceiling) - No-consent no-op (zero network activity) - Non-https endpoint rejected inside transport - markCreditExhausted idempotence (stable `since`) - clearCreditStatus stamps `cleared` - creditExhaustedNotice humanized age copy - Null return when not exhausted (no false surface) Co-Authored-By: RuFlo <ruv@ruv.net> * feat(funnel): funnel.ruv.io domain mapping for analytics endpoint The cognitum-analytics Cloud Function is now deployed on cognitum-20260110 and reachable at funnel.ruv.io/v1/events (Cloud Run domain mapping) as well as the raw Cloud Run URL. The client's DEFAULT_ENDPOINT points at the ruv.io subdomain so it: 1. survives Cloud Run hostname-hash changes on redeploy, 2. keeps analytics attribution on rUv's own domain (not cognitum.one, which is a separate product surface), 3. reads honestly to users: telemetry for the OSS CLI lives with the OSS author, not with the cognitum.one commercial product. DNS is a Cloudflare CNAME funnel.ruv.io → ghs.googlehosted.com, unproxied so Cloud Run terminates TLS directly. README documents the recreate-from-scratch flow using the CLOUDFLARE_API_TOKEN in Secret Manager on cognitum-20260110. Live infrastructure state: - Cloud Function cognitum-analytics deployed us-central1, revision 00001 - Firestore collections funnel_events / funnel_aggregates / funnel_credit / funnel_idem writing on first accepted batch - Smoke test at deploy time returned {"ok":true,"accepted":1} - Domain mapping created, DNS resolving, cert provisioning pending (Cloud Run polls hourly) Co-Authored-By: RuFlo <ruv@ruv.net> * docs(adr-311): funnel analytics — server-side split to dedicated repo Server implementation of the ADR-308 /v1/events endpoint has been extracted to its own repo: github.com/cognitum-one/ruflo-funnel-api Reasoning captured in ADR-311: - Commercial-side changes (rate limits, tenant model, BigQuery export) don't churn the OSS CLI PR history. - ADR-308 client contract remains the single source of truth for the wire format; the server can be forked or swapped without touching the CLI. - Server security surface (Firestore rules, IAM, key rotation) belongs with whoever runs the endpoint, not tangled with CLI release cadence. Also captures the concrete deployment decisions that came out of standing up the endpoint against cognitum-20260110: - Runtime: Cloud Function gen2, Node 22, us-central1, 256 MiB - Domain: funnel.ruv.io (Cloud Run mapping, Cloudflare CNAME unproxied) - Storage: 4 Firestore collections (funnel_events / _aggregates / _credit / _idem) with ADR-309 retention rules - Credit signal: HTTP 402 + body 'COGNITUM_CREDIT_EXHAUSTED' wires directly into the ADR-303 recovery surface via credit-notifier.ts services/cognitum-analytics/ in this repo is now a pointer README to the dedicated repo; the four source files were verbatim-committed as the initial commit there. Live-verified 8/8 API contract tests + Firestore writes across all 4 collections at time of adoption. Co-Authored-By: RuFlo <ruv@ruv.net> * feat(funnel): remote-served message rotation + agentics.org sponsor + settings command Three linked changes to the funnel rotation surface. ## Remote-served messages (ADR-311 amendment) New client transport `message-transport.ts`: - GET https://funnel.ruv.io/v1/messages (raw: https://cognitum-analytics-63rzcdswba-uc.a.run.app/v1/messages) - 6h TTL local cache at ~/.ruflo/funnel-messages-cache.json - Every fetched message validated through ADR-301 isValidMessage() — schema, host allowlist, control-char strip, 80-col cap — before it can display - Best-effort: fetch failures leave cache intact; no network call blocks render (fire-and-forget from rotation.selectMessage) - Kill switch: RUFLO_FUNNEL_MESSAGES=0 or RUFLO_FUNNEL=0 - Bounded cache: ≤ 128 KiB, ≤ 200 messages - Non-https rejected at builder AND host allowlist (defense in depth) New pool merger `eligibleMessagesFromPools()` in messages.ts: - Remote pool authoritative (admin edits without a CLI release) - In-code pool fallback (cold starts, API-down, kill-switched) - Dedup by `id` — remote wins over in-code for the same id Rotation kicks a background refresh on every selectMessage() call so the first fresh render after CLI startup lands within seconds. ## Server side (cognitum-one/ruflo-funnel-api@521205a) - Added GET /v1/messages handler (single router by req.path in the same function; POST /v1/events unchanged) - Firestore collection funnel_messages with { id, class, text, url?, active } - Cache-Control: public, max-age=21600 for CDN + client caches - Firestore seeded with 23 initial messages (via seed-messages-rest.sh): 16 educational (ruflo tips + tricks) 4 promotional Cognitum 3 promotional agentics.org - Verified live: 200 OK, 23 messages, all 0 rejected by client pipeline ## Copy discipline + settings command - ALLOWED_URL_HOSTS + PROMO_LINK_HOSTS gain agentics.org (both wire layers need it for OSC 8 to render the link) - DISCLOSURE_TEXTS copy: "· disable: ruflo funnel disable" → "· manage: ruflo settings" — user-facing command surface no longer leaks the internal "funnel" term - New `ruflo settings` command (settings.ts) as a friendly wrapper over the existing funnel primitives: ruflo settings Overview + current state ruflo settings notices status Notices row state ruflo settings notices off Turn off (persistent) ruflo settings notices on Re-enable ruflo settings notices id Pseudonymous notices ID - getPromoRow split anchor moved from " · disable" → " · manage" (so only the linked label is OSC 8 wrapped, tail stays dim/plain) ## Tests: 68 → 76 (+8) - Every disclosure variant: no `\bfunnel\b`, contains `manage`, contains `ruflo settings`, ≤ 80 cols, Cognitum-attributed - Split anchor in generator: " · manage" (not " · disable") Co-Authored-By: RuFlo <ruv@ruv.net> * feat(funnel): impressions + click tracking + coarse geo (ADR-311 amend) Adds the analytics coverage that PR #2622's initial funnel work deferred: impressions, clicks, and coarse geo. Consent-gated end-to-end; nothing new about privacy — same closed vocabulary, same daily-bucket timestamps, same pseudonymous UUID discipline. Wired against the live Cloud Function on cognitum-20260110 + Firestore. ## Client — impression firing + click-tracked URLs - FunnelEventName vocabulary gains `promo_impression` + `promo_open` (matches server; ADR-305 amendment recorded in ADR-311 §6) - FunnelEvent schema gains optional `messageId` (≤ 64 chars, [a-z0-9-]) — the join key across impression → click → conversion - recordFunnelEvent() accepts an options object for `messageId` + `now` (backwards-compat with the Date positional form) - rotation.selectMessage() fires `promo_impression` for the selected message on every render — consent-gated, batched via existing transport - attribution.clickTrackedUrl() wraps promotional URLs in the server-side click-redirect so `promo_open` fires before the browser leaves. Disclosure/educational rows still use attributionUrl direct (no click tracking on disclosure) - promo.ts uses clickTrackedUrl for `msg.class === 'promotional'` ## Server (cognitum-one/ruflo-funnel-api@37f584b — deployed live) - KNOWN_EVENTS gains promo_impression + promo_open - validEvent() accepts optional messageId via MESSAGE_ID_RE allowlist - New `GET /v1/click/{messageId}?to=<https-url>` handler: - messageId validated against MESSAGE_ID_RE (prevents path-traversal or forged doc lookups) - `to` must be https AND host must be in CLICK_ALLOWED_HOSTS (cognitum.one + agentics.org variants — compiled in, not Firestore-editable; no open-redirect vector) - Coarse geo from CF-IPCountry / X-Appengine-Country (ISO-3166 alpha-2 country ONLY — never city, never lat/long; ADR-309 privacy invariant) - Firestore write: funnel_events doc + funnel_aggregates row keyed by (surface, event, day, release, messageId, country) - Firestore hiccup NEVER blocks the redirect — user intent > analytics - 302 with Cache-Control: no-store so aggregators see every click - Router extended to match /v1/click/{msgId} paths ## Verified live at commit | Test | Result | |---|---| | Click w/ good target + fake CF-IPCountry:US | 302 → cognitum.one; Firestore captured US | | Click w/ non-allowed target (evil.com) | 400 DISALLOWED_TARGET | | Click w/ http:// target | 400 DISALLOWED_TARGET | | Click w/ `..` path traversal | 405 (routed as method-not-allowed) | | POST promo_impression event | 200 accepted:1 | | POST promo_open event | 200 accepted:1 | | POST event w/ malformed messageId | 200 accepted:0 dropped:1 | Firestore verified: promo_open with `country: US`, `via: click-redirect`, `messageId: promo-cognitum-meta-llm`. Aggregate row present with `count: 1` keyed by messageId+country. ## Coverage summary (ADR-311 §8) - Impressions per message per day: `promo_impression` events + aggregates - Clicks per message per day: `promo_open` events + aggregates - CTR: ratio of the two above - Geo distribution: `country` field on `promo_open` - Conversions: existing `signup_opened` / `account_created` / `proxy_activated` - Notices disabled rate: existing `funnel_disabled` Zero PII, zero prompt content, zero paths — everything remains closed enum, allowlisted message id, ISO country code, or daily bucket. Co-Authored-By: RuFlo <ruv@ruv.net> * fix(statusline): style "ruflo settings" as bold command, not a dead link User feedback: "the area don't link to anything, manage: ruflo settings" — clarified they expected "manage: ruflo settings" to be clickable/actionable. It can't be a real link: a terminal has no safe way to execute a shell command from a click — that would let any server-served promo message run arbitrary commands on the user's machine. So instead of leaving it looking like inert plain text (which reads as broken/half-finished), it now gets its own visual treatment: ✨ <label, underlined + OSC 8 link> · manage: <ruflo settings, BOLD> - "manage: " stays dim (a connector word, no action implied) - "ruflo settings" renders BOLD, deliberately WITHOUT underline or OSC 8 — bold signals "this is the important bit, copy/type it" without the false affordance of "click me" that underline would carry Split changed from `text.indexOf(' · manage')` to the more precise `text.indexOf(' · manage: ')` so the exact command substring can be isolated for its own styling. getPromoRow() now returns three concatenated segments instead of two: label(+OSC8), dim connector, bold command. Test updated to assert: label sandwiched in UL_ON/UL_OFF + OSC8, connector in DIM_ON/DIM_OFF, command in BOLD_ON/BOLD_OFF, and — critically — that the command is NEVER wrapped in underline or safeTerminalLink (regression guard against accidentally making a "command" look clickable again). Verified via hex dump of the live render: ESC[1m immediately before "ruflo settings", ESC[22m immediately after, no ESC[4m or OSC 8 escape anywhere near it. Co-Authored-By: RuFlo <ruv@ruv.net> * fix(funnel): use raw Cloud Run URL until funnel.ruv.io cert lands funnel.ruv.io's TLS cert has not yet provisioned (Cloud Run domain mapping created 2026-07-10 18:33 UTC; Google's cert issuance is async and still pending as of this commit — verified: curl to funnel.ruv.io returns curl exit 35 / HTTP 000, raw Cloud Run URL returns 200). Every client fetch to messages/events/click was silently timing out (by design — fail-silent), so: - the remote 23-message pool never reached any installed CLI - message-transport's cache file never got created - rotation only ever showed the 3 static in-code disclosure variants All three endpoint defaults now point at the raw Cloud Run URL: https://cognitum-analytics-63rzcdswba-uc.a.run.app/v1/{events,messages,click} Marked TEMP in comments. Flip back to funnel.ruv.io once its cert is live (monitor: curl -o /dev/null -w '%{http_code}' https://funnel.ruv.io/v1/messages). ## Live verification performed this commit 1. Forced message-pool refresh — 23/23 messages accepted, 0 rejected 2. Selected messages across 8 consecutive 20s rotation slots — 8 distinct ids returned (7 educational + 1 promotional), confirming rotation moves 3. Impressions queued locally (8/8), consent-gated 4. Flushed queue to live server — 8/8 accepted, queue drained to 0 5. Fired the click-redirect for the selected promotional message with a forged CF-IPCountry: CA header — 302 to agentics.org with UTM+fid intact 6. Firestore confirmed both events landed: promo_impression messageId=promo-agentics-community promo_open messageId=promo-agentics-community country=CA via=click-redirect 7. Total promo_impression docs in funnel_events: 9 (8 new + 1 prior) Co-Authored-By: RuFlo <ruv@ruv.net> * feat(funnel): zero local promo content — fully remote-served, fail-closed Per explicit direction: no local promo content or URLs should ship in the CLI at all. This completes the ADR-311 remote-message architecture: disclosure text, educational tips, and promotional messages are now ALL served exclusively from the Cloud Function / Firestore feed. If the feed is unreachable and no prior fetch has succeeded, the promo row renders nothing — there is no local fallback content to degrade to. ## What changed - messages.ts: MESSAGES array emptied to []. Zero message text/URLs ship in the package. isValidMessage() extended to accept a new 'disclosure' class alongside educational/promotional, with an invariant that disclosure-class text must retain the exact " · manage: ruflo settings" tail (dropped, never repaired, if missing). - types.ts: FunnelMessageClass extended to 'educational' | 'promotional' | 'disclosure'. - disclosure.ts: removed the hardcoded DISCLOSURE_TEXTS/DISCLOSURE_TEXT/ DISCLOSURE_SPONSOR_URL constants and selectDisclosureText(). Replaced with selectDisclosureMessage() — deterministic 5-min-slot selection over the remote pool filtered to class==='disclosure', returning null when that pool is empty (cold start / API down). The disclosure state MACHINE (never_seen / disclosed_enabled / disclosed_disabled tracking) stays 100% local — that's app state, not promo content. - promo.ts: getFunnelPromo() now sources the disclosure row from selectDisclosureMessage(); returns null (fail-closed) when unavailable. Also wires the previously-defined-but-never-fired 'disclosure_shown' event so disclosure impressions are now tracked alongside promo_impression. - statusline-generator.ts: PROMO_LINK_HOSTS gains the click-redirect hosts (funnel.ruv.io + the raw Cloud Run hostname) — these are DIFFERENT from the final-destination hosts (cognitum.one/agentics.org); without this the renderer's independent host-allowlist check correctly refused to OSC-8- wrap click-tracked URLs, which was silently breaking every clickable link this session had just added. ## Server (cognitum-one/ruflo-funnel-api@698aa73 — deployed live) - All 16 educational tips now carry url: https://cognitum.one and a trailing 🔗 glyph (checked to fit the 80-col bound, max observed 78/78). - 3 disclosure-class messages seeded (disclosure-1/2/3), matching the copy this session iterated on, now server-authoritative instead of hardcoded. - promo.ts routes EVERY message with a url (not just class==='promotional') through the server click-redirect, so click tracking is uniform across the whole rotation. - Firestore funnel_messages collection: 26 total (16 educational + 7 promotional + 3 disclosure), all verified live via GET /v1/messages. ## Verified live end-to-end - GET /v1/messages returns 26 messages, 16/16 educational have url set. - Rendered statusline shows a real educational tip (edu-doctor) with a working OSC 8 hyperlink to the click-redirect, underline, 🔗ical suffix. - Click-redirect → 302 to cognitum.one with UTM + fid intact; Firestore captured both promo_impression and promo_open for the same messageId. - Fail-closed proven via test: selectMessage()/selectDisclosureMessage()/ getFunnelPromo() all return null when the remote cache is unseeded. ## Tests: 68 → 70 funnel (78 total incl. drift-guard), all green - New: "ships ZERO local messages" pins MESSAGES === []. - New: disclosure-class validation (manage tail required). - New: 3 fail-closed tests (selectDisclosureMessage / selectMessage / getFunnelPromo all null on unseeded cache). - Rewrote disclosure-variant + promo-orchestrator tests to seed a mock remote cache (matching message-transport.ts's on-disk shape) instead of asserting against removed local constants. Co-Authored-By: RuFlo <ruv@ruv.net> * docs(adr-312): usage-limit downtime prevention — researched, phased plan Direct instruction was to review Claude Code's actual CLI source (not just docs) to determine feasibility of detecting approaching/hit usage limits and surfacing a Cognitum meta-LLM proxy CTA as a downtime- prevention / session-extension mechanism. ## Research performed (grounded against @anthropic-ai/claude-code@2.1.107) Inspected /usr/lib/node_modules/@anthropic-ai/claude-code/cli.js directly. Findings: 1. A real internal rate-limit state object exists (module var, referred to in the ADR by its minified name since it will rename on any version bump): { status: allowed|allowed_warning|rejected, resetsAt (epoch sec), rateLimitType: five_hour|seven_day|seven_day_opus| seven_day_sonnet|overage, utilization (0-1 float), unifiedRateLimitFallbackAvailable, isUsingOverage }. 2. It's sourced from REAL Anthropic API response headers (anthropic-ratelimit-unified-status/-reset/-fallback/-{type}- utilization etc.) — confirmed via string match in the bundle. The exact "You've used N% of your session limit · resets HH:MM" banner text is generated from this exact object by a UI-only formatter. 3. This state is NOT exposed through any current extension point — checked exhaustively, empirically: - statusLine stdin JSON: confirmed absent (only model/context_window/ cost fields exist — verified against ruflo's own parser) - Hook events: 28 hook names exist, none confirmed to fire on rate-limit transitions - Debug logs: EMPIRICALLY checked an existing 11,424-line debug log at default level — zero occurrences of any rate-limit header - No CLI command / MCP resource surfaces it - The only consumer is an internal React hook wired to Claude Code's own TUI, unreachable from outside its process ## Decision: 3-phase plan - Phase 0 (buildable now): manual/self-reported flag — `ruflo settings notices rate-limited` — reusing the exact credit-notifier.ts pattern from ADR-303/311. Honest that this is user-reported, not detected. - Phase 1 (blocked on upstream): file a Claude Code feature request to expose the real signal via statusLine JSON or a new hook event. Explicitly does NOT recommend building against the current minified internal names — fragile, version-coupled, would silently break. - Phase 2 (once Phase 1 lands): wire the real signal into rotation with a priority override analogous to ADR-303's credit-exhaustion surface; new closed-vocabulary events rate_limit_warning_shown / rate_limit_exhausted_shown; same consent-gated click-tracking as every other promo message. No code changes in this commit — ADR only, per the explicit request. Co-Authored-By: RuFlo <ruv@ruv.net> * feat(funnel): sponsored-downtime capacity via cognitum-one/meta-proxy (ADR-312/313) ADR-312 documents grounded research into detecting an approaching/hit Claude usage limit: direct inspection of the installed Claude Code CLI (v2.1.107) found the rate-limit state is populated from response headers (anthropic-ratelimit-unified-status/-reset/-fallback/-*-utilization) that are NOT exposed to hooks, the statusline JSON, or the debug log today (confirmed by grep — zero occurrences in an 11k-line debug log). Automatic detection is blocked on Anthropic exposing this state; documents a 3-phase plan (manual flag now, wait on upstream, auto-wire once available). ADR-313 implements Phase 0: a manually-set rate-limit flag (`ruflo settings notices rate-limited [--clear]`) that, once set, preempts normal promo rotation with a sponsored-capacity CTA — same precedent as ADR-303's credit-exhaustion override. Two states depending on consent: an enable CTA, or a quiet "active" confirmation once `ruflo proxy sponsor-enable --yes` has been run. Client-side (this commit): - funnel/rate-limit-notifier.ts — TTL'd flag read/write/clear, mirrors credit-notifier.ts's shape - funnel/promo.ts — priority-override hook, reachable only after the disclosure invariant is satisfied (ADR-301 compliance preserved) - commands/proxy.ts — sponsor-enable/disable/status/clear subcommands; writes both the consent receipt (source of truth) and a mirror flag into ~/.ruflo/proxy-config.toml, the file the Rust proxy binary reads - funnel/types.ts, consent.ts, events.ts — new 'sponsored-downtime' consent domain and sponsor_mode_enabled/disabled/capacity_exhausted events - 10 new tests (rate-limit notifier + priority override), 80/80 passing Server-side (separate private repo, already built/tested/pushed): cognitum-one/meta-proxy — Rust/axum binary routing local/cloud/sponsored data planes, per-user bearer token, loopback-only bind by default, never falls back to sponsored capacity implicitly. Verified end-to-end: the Rust binary correctly reads the same ~/.ruflo/proxy-config.toml this commit's `sponsor-enable` command writes. * docs(adr-313): addendum — proxy needed /v1/messages, not /v1/chat/completions Verification found the shipped meta-proxy scaffold spoke the wrong wire protocol for Claude Code (OpenAI chat-completions instead of the Anthropic Messages API Claude Code actually POSTs to). Fixed in cognitum-one/meta-proxy and verified live end-to-end with the real installed Claude Code CLI against the live apicompletions Cloud Run service. Documents the fix and the remaining gap (no automated sponsored-key provisioning yet). * docs(adr-313): addendum — fourth Passthrough plane preserves subscription usage Setting the env vars needed to reach the proxy at all previously meant giving up subscription-based auth entirely, all the time, not just when rate-limited. Documents the new default Passthrough plane (real Anthropic via the user's own OAuth token, read-only from Claude Code's own credentials file) and the live verification of all four planes switching correctly within one continuous session. * fix(statusline): trailing blank line so the input prompt gets breathing room The statusline's last row butted directly against Claude Code's input prompt with no visual separation. generateStatusline() now returns lines.join('\n') + '\n', so combined with console.log's own trailing newline the rendered output ends in exactly one blank line. Regenerated both committed .claude/helpers/statusline.cjs copies (root + package) so the drift-guard test stays in lockstep. Two new tests: a source-level pin on the generator (can't silently regress) and a runtime check on the actual rendered bytes (exactly one blank line — not zero, not two). * fix(helpers): re-sign manifest for the statusline trailing-newline fix The earlier statusline fix ( |
||
|
|
a02b46e4fb | fix(verification): unblock ADR-104 and witness source checks | ||
|
|
11f34ec947 |
chore(release): 3.25.3 — 10 fixes + CI guards (#2602)
* fix(ci): neural status ReasoningBank row shows Empty despite persisted patterns (#2575) Derive the Status cell from the same count Details displays so the label matches the number (7798 patterns → Active) instead of the stale in-memory handle. Co-Authored-By: RuFlo <ruv@ruv.net> * fix(ci): default memory store --upsert=true to fix store→delete→store UNIQUE violation (#2594) memory_entries has UNIQUE(namespace, key) that does not exclude soft-deleted rows, so a store→delete→store cycle reliably hits UNIQUE constraint failed when --upsert defaults to false. storeEntry() already honors upsert via INSERT OR REPLACE; flipping the CLI default closes the footgun without touching schema or plumbing. Co-Authored-By: RuFlo <ruv@ruv.net> * fix(ci): remove reverse-substring collision in GAIA isAnswerCorrect (#2566) The reverse check normExpected.includes(normModel) scored fragmentary model answers as correct whenever they normalized to any substring of the expected answer (e.g. "a" vs "Paris, France" → true), inflating GAIA scores via normalization collision. ADR-169 R1 forbids this vector. Forward-substring and numeric-tolerance paths remain. Co-Authored-By: RuFlo <ruv@ruv.net> * fix(ci): skip onnxruntime-node postinstall to unblock v3 memory smoke (#2590) onnxruntime-node's postinstall fetches a GPU nupkg from nuget.org which ETIMEDOUTs from GitHub runners, taking pnpm install down. Skip it via neverBuiltDependencies (CPU prebuilds ship in the npm tarball, so runtime is unaffected) and pass --ignore-scripts in the memory smoke workflow as a belt-and-braces safety net. Co-Authored-By: RuFlo <ruv@ruv.net> * fix(ci): drop verifyMemoryInit writeback that races better-sqlite3 handle on Windows (#2596) sql.js verification holds an in-memory DB copy; writing it back via atomic rename fights the open better-sqlite3 WAL handle owned by ControllerRegistry / repairVectorIndexes, producing EPERM on Windows. Verification is read-only — close and discard the copy instead. Co-Authored-By: RuFlo <ruv@ruv.net> * fix(ci): auto-heal doctor Learning Bridge sidecar on plain run (#2599) Co-Authored-By: RuFlo <ruv@ruv.net> * fix(ci): prune CLI optionalDependencies to fix cold npx timeout (#2561) Cold `npx -y @claude-flow/cli@alpha --version` (and the ruflo wrapper) timed out because npm had to resolve and place ~30 optional deps before Node ever ran the in-process --version fast-path in bin/cli.js. Trim optionalDependencies to the 5 actually used by the default CLI path (agentdb, ruvector, agentic-flow, @claude-flow/memory, @claude-flow/security); everything else is already gated behind try/require or the plugins-store lazy-install path. Mirror the pruning in the ruflo wrapper. Add a pre-warm npm install step in verification-pipeline.yml as defense-in-depth. Co-Authored-By: RuFlo <ruv@ruv.net> * fix(ci): align ruflo-hook.cjs npx fallback with .sh dist-tag (#2600) The three shipped .cjs Windows shims hardcoded ruflo@latest while the bash shim used ruflo@alpha, breaking the "mirrors ruflo-hook.sh" contract from #2132. Point all three .cjs shims at ruflo@alpha and extend the smoke test with a static parity assertion so future drift fails CI. Co-Authored-By: RuFlo <ruv@ruv.net> * fix(ci): replace phantom agentic-flow/transport/loader marker in ADR-104 witness (#2578) Co-Authored-By: RuFlo <ruv@ruv.net> * fix(ci): re-sign + verify helpers manifest in prepublishOnly (#2593) Manifest drift (intelligence.cjs was bumped in 3.24/3.25 but the manifest still carried the 3.23.0 hash) triggered writeCriticalHelpers' fail-closed tamper warning on every CLI run in stamped projects. Root cause: sign-helpers.mjs existed but was never wired into publish. This wires sign+verify into prepublishOnly so drift fails the release, not every user. Co-Authored-By: RuFlo <ruv@ruv.net> * test(2590): guard against regression of #2590 Adds a CI guard step to the memory-smoke job that fails fast if either part of the #2590 fix regresses: 1) v3/package.json drops "onnxruntime-node" from pnpm.neverBuiltDependencies 2) the memory-smoke pnpm install loses --ignore-scripts Either regression would let onnxruntime-node's postinstall fetch a GPU nupkg from nuget.org and ETIMEDOUT on GitHub runners, taking pnpm install down as it did on main. Co-Authored-By: RuFlo <ruv@ruv.net> * test(2594): guard against regression of #2594 Co-Authored-By: RuFlo <ruv@ruv.net> * test(2561): guard against regression of #2561 Co-Authored-By: RuFlo <ruv@ruv.net> * test(2593): guard against regression of #2593 Co-Authored-By: RuFlo <ruv@ruv.net> * test(2596): guard against regression of #2596 Adds a vitest regression test asserting verifyMemoryInit() does not modify the on-disk DB file — snapshots bytes+mtime before, calls verify, asserts unchanged after. Fails if the sql.js writeback is re-added (bytes change from re-serialization, mtime bumps), passes on the fix. Co-Authored-By: RuFlo <ruv@ruv.net> * test(2566): guard against regression of #2566 Locks in the removal of the reverse-substring branch in isAnswerCorrect() (v3/@claude-flow/cli/src/benchmarks/gaia-agent.ts). The reverse rule `normExpected.includes(normModel)` scored fragmentary model answers (e.g. "a") as correct against any longer expected answer that contained them ("Paris, France"), inflating GAIA scores via normalization-collision — the vector ADR-167/169 R1 forbid. Verified: FAILS if the reverse-substring branch is reintroduced, PASSES on the current fix. Co-Authored-By: RuFlo <ruv@ruv.net> * test(2578): guard against regression of #2578 Co-Authored-By: RuFlo <ruv@ruv.net> * test(2599): guard against regression of #2599 Co-Authored-By: RuFlo <ruv@ruv.net> * chore(release): 3.25.3 — 10 fixes + CI guards Bumps @claude-flow/cli, claude-flow (umbrella), ruflo (wrapper) to 3.25.3. Regens v3 pnpm-lock.yaml after optionalDependencies pruning (#2561) and neverBuiltDependencies additions (#2590). Fixes included: - #2561 npx cold-install timeout — pruned CLI optionalDependencies - #2566 GAIA isAnswerCorrect reverse-substring collision - #2575 neural status ReasoningBank Empty vs 7798 patterns display - #2578 ADR-104 phantom agentic-flow/transport/loader witness marker - #2590 CI Node24/ubuntu memory smoke — onnxruntime-node postinstall block - #2593 helpers.manifest.json auto-refresh — verify + sign in prepublishOnly - #2594 memory store UNIQUE violation — flip --upsert default to true - #2596 memory init Windows EPERM — drop sql.js writeback race - #2599 doctor Learning Bridge — self-heal via recordMemoryPackagePath - #2600 Windows shim dist-tag parity — align @alpha across all shims CI guards added per fix. Co-Authored-By: RuFlo <ruv@ruv.net> |
||
|
|
865dd7dd2b |
fix(ci): prime-radiant TS2307 install-safety failure — indirect the optional-wasm import (#2586)
* fix(ci): prime-radiant TS2307 — indirect the optional-wasm dynamic import
The Plugin package install-safety job fails to build prime-radiant with
'TS2307: Cannot find module prime-radiant-advanced-wasm' whenever that
optionalDependency isn't installed (the CI condition). The 3 call sites used a
static string literal (await import('prime-radiant-advanced-wasm')), which tsc
statically resolves at build time even though each is already wrapped in a
runtime try/catch fallback.
Route the specifier through a string-typed variable (const pkg: string = ...;
await import(pkg)) — the same idiom used for the optional better-sqlite3 import
in memory-initializer.ts — so tsc no longer requires the optional module's types
at build time. Runtime behavior is unchanged (the package still loads when
present, falls back to the mock/null path when absent).
Verified: with the optional dep ABSENT locally (the exact CI condition), the
plugin now builds clean (tsc, no TS2307).
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01S7GYqnVUVxBfZ5W8znqry3
* fix(ci): indirect optional-wasm imports in ruvector-upstream + teammate-plugin too
install-safety was red for MORE than prime-radiant — that plugin's TS2307 just
masked the same anti-pattern in sibling plugins (surfaced once prime-radiant was
fixed): ruvector-upstream's 8 @ruvector/* bridge imports and teammate-plugin's 2
@ruvnet/bmssp imports are all runtime-guarded (.catch / try-catch) optional deps,
but tsc statically resolves the string-literal specifier and fails TS2307 when
they aren't installed (the CI condition).
Route each through a widened specifier (import('pkg' as string)) so tsc no longer
resolves it at build time; runtime behavior unchanged (still loads when present,
falls back when absent). Verified: both plugins build clean (tsc, 0 TS2307) with
the optional deps ABSENT.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01S7GYqnVUVxBfZ5W8znqry3
* fix(ci): register 3.25.x embedder env vars as precedence-audit escape hatches
The env-var-precedence audit (ADR-125/ADR-130) in the install-safety job flags 4
env vars introduced by the 3.25.x no-stub/wasm-embedder work as read without
CLI-flag precedence. They are env-only substrate toggles with no per-invocation
CLI surface (opt-in optional WASM embedder tier + a fail-closed 'no stubs' ops
flag), so register them in KNOWN_ESCAPE_HATCHES per the audit's Option B —
matching how the other opt-in/ops env vars are handled.
This was pre-existing on main since 3.25.1, masked behind the plugin TS2307
failures the earlier commits on this branch resolved.
Co-Authored-By: RuFlo <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01S7GYqnVUVxBfZ5W8znqry3
|