fix(mods): shared textsOf reports truncation and every guard fails closed on it

The walker dropped input past its node, character and per-string budgets
silently, so 32 guards passed a secret placed after the cap. textsOf now
appends a TRUNCATED marker; names/hasSecretIn treat it as a finding. Regenerated
the 38 copies, bumped the 39 plugins, emptied the known-holes baseline.

Co-Authored-By: RuFlo <ruv@ruv.net>
This commit is contained in:
ruv committed 2026-10-05 00:27:13 -04:00
1 parent d855f75264
commit d06b0122be
123 files changed
+1230 -431

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "ruflo-adr",
"description": "ADR lifecycle management — create, index, reconcile, supersede, check compliance, and link Architecture Decision Records to code via AgentDB hierarchical store + causal edges (supersedes/amends/depends-on/related)",
"version": "0.5.2",
"version": "0.5.3",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+4 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Structural smoke test for ruflo-adr v0.5.2 (ADR-0001, ADR-0002).
# Structural smoke test for ruflo-adr v0.5.3 (ADR-0001, ADR-0002).
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PASS=0
@@ -9,10 +9,10 @@ ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
# 1. plugin.json bump + new keywords
step "1. plugin.json declares 0.5.2 with new keywords"
step "1. plugin.json declares 0.5.3 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.5.2" ]]; then
bad "expected 0.5.2, got '$v'"
if [[ "$v" != "0.5.3" ]]; then
bad "expected 0.5.3, got '$v'"
else
miss=""
for k in lifecycle compliance causal-graph mcp; do
@@ -1,7 +1,7 @@
{
"name": "ruflo-agent",
"description": "Agent runtimes for ruflo — local WASM-sandboxed agents (rvagent: 10 wasm_agent_*/wasm_gallery_* MCP tools, built on @ruvector/rvagent-wasm + @ruvector/ruvllm-wasm per ADR-070) plus Anthropic Claude Managed Agents as a cloud backend (managed_agent_* MCP tools per ADR-115). One interface, local-vs-cloud runtimes.",
"version": "0.3.1",
"version": "0.3.2",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+2 -2
View File
@@ -7,11 +7,11 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json is ruflo-agent 0.3.1 with both-runtime keywords"
step "1. plugin.json is ruflo-agent 0.3.2 with both-runtime keywords"
P="$ROOT/.claude-plugin/plugin.json"
v=$(grep -E '"version"' "$P" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
n=$(grep -E '"name"' "$P" | grep -oE 'ruflo-agent' | head -1)
if [[ "$v" != "0.3.1" ]]; then bad "expected version 0.3.1, got '$v'";
if [[ "$v" != "0.3.2" ]]; then bad "expected version 0.3.2, got '$v'";
elif [[ "$n" != "ruflo-agent" ]]; then bad "expected name ruflo-agent";
else
miss=""
@@ -1,7 +1,7 @@
{
"name": "ruflo-agentdb",
"description": "Substrate plugin for Ruflo memory: AgentDB controller bridge (15 agentdb_* MCP tools), RuVector ONNX embeddings (10 embeddings_* tools incl. RaBitQ 32x quantization), and WASM HNSW pattern router (3 ruvllm_hnsw_* tools). As a mod (ADR-445): opt-in safe recall into the prompt, a write guard that keeps secrets out of memory, /agentdb, and a status file the console shows",
"version": "0.4.3",
"version": "0.4.4",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+1 -1
View File
@@ -205,7 +205,7 @@ bash plugins/ruflo-agentdb/scripts/smoke.sh
The smoke script is the contract. It calls each documented MCP tool, exercises the RaBitQ workflow, and source-inspects the fallback path (no env-var gate exists to force the fallback live).
## As a mod (0.4.3, ADR-445)
## As a mod (0.4.4, ADR-445)
A function-hook mod ships beside the skills. Needs a Claude Code with mods (2.1.287+); older builds ignore it.
+27 -5
View File
@@ -148,8 +148,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -191,33 +198,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -228,6 +248,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+3 -3
View File
@@ -48,10 +48,10 @@ grep -q "## Namespace convention" "$ROOT/README.md" \
&& ok || bad "Namespace convention section missing"
# 1. Plugin version + new keywords
step "1. plugin.json declares version 0.4.3 with rabitq + namespace-convention keywords"
step "1. plugin.json declares version 0.4.4 with rabitq + namespace-convention keywords"
v=$(grep -E '"version"[[:space:]]*:' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.4.3" ]]; then
bad "expected 0.4.3, got '$v'"
if [[ "$v" != "0.4.4" ]]; then
bad "expected 0.4.4, got '$v'"
else
miss=""
for k in rabitq quantization namespace-convention controller-bridge; do
+39 -1
View File
@@ -4,7 +4,7 @@ import { verdict } from '../hooks/guard'
import { readOptions } from '../hooks/options'
import { cacheKey, frame, keywords, parse, screen as screenItems, worthRecalling } from '../hooks/recall'
import { expandTruncated } from '../hooks/expand'
import { hasSecret, scan, tidy } from '../hooks/screen'
import { TRUNCATED, hasSecret, hasSecretIn, names, scan, textsOf, tidy, COMMON_SECRETS } from '../hooks/screen'
import { isWriter, pickReaders, splitName } from '../hooks/tools'
tier('user')
@@ -322,3 +322,41 @@ describe('tools and guard', () => {
expect(readOptions({ recall: 'on', recallLimit: 99, recallDeadlineMs: 5, guard: 'off', source: 'bogus' })).toMatchObject({ recall: true, recallLimit: 5, recallDeadlineMs: 200, guard: false, source: 'auto' })
})
})
describe('truncation fails closed', () => {
const flagged = (input: unknown, limits?: Parameters<typeof textsOf>[1]) => textsOf(input, limits).includes(TRUNCATED)
test('a secret after the node budget is refused, not dropped', () => {
const list: unknown[] = Array.from({ length: 25_000 }, () => 'x')
list.push(aws)
expect(flagged(list)).toBe(true)
expect(textsOf(list).some(hasSecret)).toBe(true)
expect(verdict('mcp__x__memory_store', { value: list })).toMatch(/too large|secret/)
})
test('a wide object past the node budget is flagged', () => {
const wide: Record<string, unknown> = {}
for (let i = 0; i < 30_000; i++) wide[`field${i}`] = i
expect(flagged(wide)).toBe(true)
})
test('the character budget and a string past the per-string limit are flagged', () => {
expect(flagged(['a'.repeat(900), 'b'.repeat(900), 'c'], { chars: 1000 })).toBe(true)
expect(flagged({ value: 'z'.repeat(3000) }, { perString: 1000 })).toBe(true)
})
test('input inside every budget is not flagged, and a custom node budget flags only when something was dropped', () => {
expect(flagged({ a: 'one', b: ['two', 'three'] })).toBe(false)
expect(flagged(['a', 'b'], { nodes: 2 })).toBe(false)
expect(flagged(['a', 'b', 'c'], { nodes: 2 })).toBe(true)
})
test('the refusal names a finding, never echoes input', () => {
expect(names(COMMON_SECRETS, TRUNCATED)).toEqual(['input too large to screen'])
expect(hasSecretIn(COMMON_SECRETS, TRUNCATED)).toBe(true)
const list: unknown[] = Array.from({ length: 25_000 }, () => 'x')
list.push(ghp)
const msg = verdict('mcp__x__memory_store', { value: list }) ?? ''
expect(msg).not.toContain(ghp)
})
})
+2 -2
View File
@@ -24,7 +24,7 @@ describe('the shared walker every guard reads through', () => {
const started = performance.now()
const texts = textsOf(wide)
expect(performance.now() - started).toBeLessThan(500)
expect(texts.length).toBeLessThanOrEqual(20_000)
expect(texts.length).toBeLessThanOrEqual(20_001) // the walked texts and the truncation marker
expect(texts.some(hasSecret)).toBe(true)
})
@@ -56,6 +56,6 @@ describe('the shared walker every guard reads through', () => {
})
test('limits bound the work: a node budget of 2 stops the walk', () => {
expect(textsOf({ a: 'one', b: 'two', c: 'three' }, { nodes: 2 }).length).toBeLessThanOrEqual(2)
expect(textsOf({ a: 'one', b: 'two', c: 'three' }, { nodes: 2 }).length).toBeLessThanOrEqual(3) // two texts and the truncation marker
})
})
@@ -1,7 +1,7 @@
{
"name": "ruflo-agntcy",
"description": "AGNTCY/Outshift Internet-of-Cognition runtime integration for Ruflo — SLIM secure transport for cross-host agent coordination, CASA intent-scoped tool authorization enforcement, optional IOC Layer 9 coordination events, and AGNTCY identity/observability spans (ADR-380). Optional, removable augmentation per ADR-150; degrades gracefully when AGNTCY/SLIM packages are not installed.",
"version": "0.2.1",
"version": "0.2.2",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+2 -2
View File
@@ -8,9 +8,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "0. plugin.json declares 0.2.1"
step "0. plugin.json declares 0.2.2"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
[[ "$v" == "0.2.1" ]] && ok || bad "expected 0.2.1, got '$v'"
[[ "$v" == "0.2.2" ]] && ok || bad "expected 0.2.2, got '$v'"
# M1. The mod (ADR-445 pattern): hooks module registered, files within the 500-line rule
step "M1. mod: hooks.json names register.ts, every hook file is present and under 500 lines"
@@ -1,7 +1,7 @@
{
"name": "ruflo-ai-team",
"description": "Give Claude a tenant-isolated AI team that divides work, coordinates progress, recalls approved context, and returns evidence-backed results.",
"version": "0.2.1",
"version": "0.2.2",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
@@ -1,7 +1,7 @@
{
"name": "ruflo-aidefence",
"description": "AI safety scanning, PII detection, prompt injection defense, and adaptive threat learning",
"version": "0.3.1",
"version": "0.3.2",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+4 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Structural smoke test for ruflo-aidefence v0.3.1 (ADR-0001).
# Structural smoke test for ruflo-aidefence v0.3.2 (ADR-0001).
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PASS=0
@@ -8,10 +8,10 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.3.1 with new keywords"
step "1. plugin.json declares 0.3.2 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.3.1" ]]; then
bad "expected 0.3.1, got '$v'"
if [[ "$v" != "0.3.2" ]]; then
bad "expected 0.3.2, got '$v'"
else
miss=""
for k in prompt-injection defense-in-depth mcp; do
@@ -1,6 +1,6 @@
{
"name": "ruflo-arena",
"version": "0.2.1",
"version": "0.2.2",
"description": "Competitive ruliology for Ruflo swarms — arenas, tournaments, and adaptive co-evolution of program strategies (ADR-147/148). Strategies-as-programs compete under payoff games; tournaments produce Wolfram-style competitive arrays; hill-climb and mutual co-evolution discover winners empirically.",
"author": {
"name": "ruvnet",
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+2 -2
View File
@@ -8,9 +8,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "0. plugin.json declares 0.2.1"
step "0. plugin.json declares 0.2.2"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
[[ "$v" == "0.2.1" ]] && ok || bad "expected 0.2.1, got '$v'"
[[ "$v" == "0.2.2" ]] && ok || bad "expected 0.2.2, got '$v'"
# M1. The mod (ADR-445 pattern): hooks module registered, files within the 500-line rule
step "M1. mod: hooks.json names register.ts, every hook file is present and under 500 lines"
@@ -1,7 +1,7 @@
{
"name": "ruflo-autopilot",
"description": "Autonomous /loop-driven task completion with learning, prediction, and progress tracking — wraps 10 autopilot_* MCP tools (status/enable/disable/config/reset/log/progress/learn/history/predict)",
"version": "0.3.1",
"version": "0.3.2",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+4 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Structural smoke test for ruflo-autopilot v0.3.1 (ADR-0001).
# Structural smoke test for ruflo-autopilot v0.3.2 (ADR-0001).
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PASS=0
@@ -8,10 +8,10 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.3.1 with new keywords"
step "1. plugin.json declares 0.3.2 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.3.1" ]]; then
bad "expected 0.3.1, got '$v'"
if [[ "$v" != "0.3.2" ]]; then
bad "expected 0.3.2, got '$v'"
else
miss=""
for k in prediction progress-tracking cache-aware mcp; do
@@ -1,7 +1,7 @@
{
"name": "ruflo-bbs-federation",
"description": "AgentBBS cross-host agent federation for ruflo (ADR-164). Phase 1 exposes room coordination — federation_bbs_register / federation_bbs_publish / federation_bbs_watch / federation_bbs_human_join. Phase 2 (v3.40.0) adds signed cross-host federation — federation_bbs_identity (persistent Ed25519 node identity), federation_bbs_peer_add (pin a peer by nodeId + public key), federation_bbs_peers (audit/unpin), federation_bbs_serve (read-only pull endpoint), and federation_bbs_sync (pull pinned peers and union-merge what verifies). Every merged envelope is Ed25519-verified against a pinned key; unsigned/misattributed/oversize/over-hop envelopes are dropped and counted. Transport is HTTP pull and works on any network (Tailscale, LAN, VPN, loopback) — no tailnet required. Coordinates work ownership via claim messages (ClaimIssued/Released/Handoff/Ack). agentbbs lives in optionalDependencies; every handler gracefully degrades to {degraded:true} when the package is missing (ADR-150 pattern). As a mod (ADR-445 pattern): a tighten-only tool guard, a local slash command, and a status file the console shows.",
"version": "0.3.2",
"version": "0.3.3",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Structural smoke test for ruflo-bbs-federation v0.3.2: the plugin manifest and the mod (ADR-445 pattern).
# Structural smoke test for ruflo-bbs-federation v0.3.3: the plugin manifest and the mod (ADR-445 pattern).
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PASS=0
@@ -8,10 +8,10 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares version 0.3.2 and the mod keywords"
step "1. plugin.json declares version 0.3.3 and the mod keywords"
v=$(grep -E '"version"[[:space:]]*:' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.3.2" ]]; then
bad "expected 0.3.2, got '$v'"
if [[ "$v" != "0.3.3" ]]; then
bad "expected 0.3.3, got '$v'"
else
miss=""
for k in mod function-hooks guard; do grep -q "\"$k\"" "$ROOT/.claude-plugin/plugin.json" || miss="$miss $k"; done
@@ -1,7 +1,7 @@
{
"name": "ruflo-browser",
"description": "Session-as-skill browser automation: Playwright + RVF cognitive containers + ruvector trajectories + AgentDB selector memory + AIDefence PII/injection gates As a mod (ADR-445 pattern): a tighten-only tool guard, a local slash command, and a status file the console shows.",
"version": "0.3.3",
"version": "0.3.4",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+3 -3
View File
@@ -15,10 +15,10 @@ ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
# 1. plugin.json version + keywords
step "plugin.json declares version 0.3.3 with new keywords"
step "plugin.json declares version 0.3.4 with new keywords"
v=$(grep -E '"version"[[:space:]]*:' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.3.3" ]]; then
bad "expected 0.3.3, got '$v'"
if [[ "$v" != "0.3.4" ]]; then
bad "expected 0.3.4, got '$v'"
else
missing=""
for kw in rvf replay trajectory agentdb aidefence; do
@@ -1,7 +1,7 @@
{
"name": "ruflo-business-pods",
"description": "ADR-164 Phase 2 — business-domain pod templates for the federated business autopilot. Ships the sales pod template (templates/sales.json) and a single-tick runner (scripts/pod-tick.mjs) that pre-flights templates via business_pod_validate, reserves budget through a file-based stub ledger (atomic SQLite tracker is Phase 3 per ADR-164.1), resolves each agent role against ruflo's agent registry, constructs dry-run prompts, and posts an envelope summary to the room via federation_bbs_publish. Dry-run by default — --live is reserved for Phase 3 Managed-Agent + claude-p wiring. As a mod (ADR-445 pattern): a tighten-only tool guard, a local slash command, and a status file the console shows.",
"version": "0.2.2",
"version": "0.2.3",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+5 -5
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# Structural + functional smoke test for ruflo-business-pods v0.2.2
# Structural + functional smoke test for ruflo-business-pods v0.2.3
# (ADR-164 Phase 2). Verifies:
#
# 1. plugin.json declares 0.2.2 with the expected adr-164/phase-2 keywords
# 1. plugin.json declares 0.2.3 with the expected adr-164/phase-2 keywords
# 2. templates/sales.json exists and validates against the pod-schema
# 3. scripts/pod-tick.mjs exists, parses, and is executable
# 4. Vitest suite passes (cli/__tests__/business-pod-tools.test.ts)
@@ -23,10 +23,10 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.2.2 with adr-164 + phase-2 keywords"
step "1. plugin.json declares 0.2.3 with adr-164 + phase-2 keywords"
v=$(grep -E '"version"' "$PLUGIN/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.2.2" ]]; then
bad "expected 0.2.2, got '$v'"
if [[ "$v" != "0.2.3" ]]; then
bad "expected 0.2.3, got '$v'"
else
miss=""
for k in ruflo pods business sales adr-164 adr-164.1 phase-2 pod-template pod-tick budget-reservation dry-run-default agent-registry-resolution; do
@@ -1,7 +1,7 @@
{
"name": "ruflo-chatgpt-federation",
"description": "Dedicated ChatGPT Federation MCP publisher with isolated Nostr signing-key custody, OAuth-scoped publishing, and signed public-channel coordination over the Ruflo relay. As a mod (ADR-445 pattern): a tighten-only tool guard, a local slash command, and a status file the console shows.",
"version": "0.2.2",
"version": "0.2.3",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Structural smoke test for ruflo-chatgpt-federation v0.2.2: the plugin manifest and the mod (ADR-445 pattern).
# Structural smoke test for ruflo-chatgpt-federation v0.2.3: the plugin manifest and the mod (ADR-445 pattern).
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PASS=0
@@ -8,10 +8,10 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares version 0.2.2 and the mod keywords"
step "1. plugin.json declares version 0.2.3 and the mod keywords"
v=$(grep -E '"version"[[:space:]]*:' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.2.2" ]]; then
bad "expected 0.2.2, got '$v'"
if [[ "$v" != "0.2.3" ]]; then
bad "expected 0.2.3, got '$v'"
else
miss=""
for k in mod function-hooks guard; do grep -q "\"$k\"" "$ROOT/.claude-plugin/plugin.json" || miss="$miss $k"; done
@@ -1,7 +1,7 @@
{
"name": "ruflo-console",
"description": "ruflo's cockpit inside Claude Code and the one /ruflo command for every ruflo mod (function hooks, early access). Views: overview with health alerts, swarm topology graph, claims flow with TTL rings, federation map, plugin health matrix, learning curve and pipeline, MetaHarness radar and audit trend, memory, cost gauge, agent timeline, approvals queue, event stream, a main menu, an x.ruv.io federation board, an AI terminal (codex, claude or both in remembered sessions, streamed), Skills (npx skills), a Hive-Mind view, a MetaHarness lab, agent drill-down; a command palette for confirm-gated actions through the ruflo CLI; a band above the prompt. Reads ruflo's files and the CLI's local JSON; anything not measured reads n/a. The one network call it makes by itself is a daily read of the published plugin.json at github.com/ruvnet/ruflo to offer an update (Settings → Updates: ask, auto, or off); it installs only through Claude Code's own claude plugin update, and never a new major version without asking.",
"version": "0.33.5",
"version": "0.33.6",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -146,8 +146,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -189,33 +196,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -226,6 +246,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+1 -1
View File
@@ -2,4 +2,4 @@
* The console's version, shown on the header so a person can tell which build is running. Kept equal to the plugin's manifest
* (`.claude-plugin/plugin.json`); tests/version.spec.ts fails if the two drift.
*/
export const CONSOLE_VERSION = '0.33.5'
export const CONSOLE_VERSION = '0.33.6'
+2 -2
View File
@@ -11,9 +11,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares ruflo-console 0.33.5"
step "1. plugin.json declares ruflo-console 0.33.6"
grep -q '"name": "ruflo-console"' "$ROOT/.claude-plugin/plugin.json" \
&& grep -q '"version": "0.33.5"' "$ROOT/.claude-plugin/plugin.json" && ok || bad "name/version"
&& grep -q '"version": "0.33.6"' "$ROOT/.claude-plugin/plugin.json" && ok || bad "name/version"
step "2. hooks.json names exactly one module and no classic hook commands"
grep -q '"modules": \["./register.ts"\]' "$HOOKS/hooks.json" && ! grep -q '"command"' "$HOOKS/hooks.json" \
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "ruflo-daa",
"description": "Dynamic Agentic Architecture — 8 daa_* MCP tools for adaptive agents (create/adapt), cognitive patterns, workflows (create/execute), knowledge sharing, and learning/performance metrics. Feeds the JUDGE phase of the 4-step intelligence pipeline.",
"version": "0.3.1",
"version": "0.3.2",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+2 -2
View File
@@ -7,9 +7,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.3.1 with new keywords"
step "1. plugin.json declares 0.3.2 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.3.1" ]]; then bad "expected 0.3.1, got '$v'"; else
if [[ "$v" != "0.3.2" ]]; then bad "expected 0.3.2, got '$v'"; else
miss=""
for k in cognitive-patterns workflows mcp; do
grep -q "\"$k\"" "$ROOT/.claude-plugin/plugin.json" || miss="$miss $k"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "ruflo-ddd",
"description": "Domain-Driven Design scaffolding — bounded contexts, aggregate roots, domain events, value objects, repositories, and anti-corruption layers; navigable domain graph stored in AgentDB",
"version": "0.3.2",
"version": "0.3.3",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+2 -2
View File
@@ -7,9 +7,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.3.2 with new keywords"
step "1. plugin.json declares 0.3.3 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.3.2" ]]; then bad "expected 0.3.2, got '$v'"; else
if [[ "$v" != "0.3.3" ]]; then bad "expected 0.3.3, got '$v'"; else
miss=""
for k in acl value-objects repositories mcp; do
grep -q "\"$k\"" "$ROOT/.claude-plugin/plugin.json" || miss="$miss $k"
@@ -1,7 +1,7 @@
{
"name": "ruflo-deepseek-harness",
"description": "DeepSeek model harness for ruflo — agents, commands, scripts, and skills for running and evaluating DeepSeek-backed workflows within the ruflo swarm.",
"version": "0.2.2",
"version": "0.2.3",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
@@ -9,8 +9,8 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares version 0.2.2"
grep -q '"version": "0.2.2"' "$ROOT/.claude-plugin/plugin.json" && ok || bad "version is not 0.2.2"
step "1. plugin.json declares version 0.2.3"
grep -q '"version": "0.2.3"' "$ROOT/.claude-plugin/plugin.json" && ok || bad "version is not 0.2.3"
step "2. scripts, both skills, the command and the agent are present"
miss=""
@@ -1,7 +1,7 @@
{
"name": "ruflo-docs",
"description": "Documentation generation, API docs (JSDoc/TSDoc/OpenAPI), and drift detection — drives the `document` background worker via hooks_worker-dispatch; uses Haiku model for cost-efficient docs work",
"version": "0.3.1",
"version": "0.3.2",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+2 -2
View File
@@ -7,9 +7,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.3.1 with new keywords"
step "1. plugin.json declares 0.3.2 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.3.1" ]]; then bad "expected 0.3.1, got '$v'"; else
if [[ "$v" != "0.3.2" ]]; then bad "expected 0.3.2, got '$v'"; else
miss=""
for k in jsdoc openapi mcp; do
grep -q "\"$k\"" "$ROOT/.claude-plugin/plugin.json" || miss="$miss $k"
@@ -1,7 +1,7 @@
{
"name": "ruflo-federation",
"description": "Cross-installation agent federation with zero-trust security, peer discovery, consensus-based task routing, and per-call budget circuit breaker (ADR-097)",
"version": "0.3.3",
"version": "0.3.4",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+2 -2
View File
@@ -7,9 +7,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.3.3 with new keywords"
step "1. plugin.json declares 0.3.4 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.3.3" ]]; then bad "expected 0.3.3, got '$v'"; else
if [[ "$v" != "0.3.4" ]]; then bad "expected 0.3.4, got '$v'"; else
miss=""
for k in mcp pii-pipeline audit-log; do
grep -q "\"$k\"" "$ROOT/.claude-plugin/plugin.json" || miss="$miss $k"
@@ -1,7 +1,7 @@
{
"name": "ruflo-goals",
"description": "Long-horizon goal planning, deep research orchestration, and adaptive replanning using GOAP algorithms",
"version": "0.4.1",
"version": "0.4.2",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+1 -1
View File
@@ -96,7 +96,7 @@ bash plugins/ruflo-goals/scripts/smoke.sh
# Expected: "12 passed, 0 failed"
```
## As a mod (0.4.1)
## As a mod (0.4.2)
A function-hook mod ships beside the skills (ADR-445 pattern). Needs a Claude Code with mods (2.1.287+); older builds ignore it. No network, no process spawning: it only tightens calls to this plugin's own tools and reads through tools already connected.
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+2 -2
View File
@@ -7,9 +7,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.4.1 with new keywords"
step "1. plugin.json declares 0.4.2 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.4.1" ]]; then bad "expected 0.4.1, got '$v'"; else
if [[ "$v" != "0.4.2" ]]; then bad "expected 0.4.2, got '$v'"; else
miss=""
for k in mcp evidence-grading legacy-namespaces; do
grep -q "\"$k\"" "$ROOT/.claude-plugin/plugin.json" || miss="$miss $k"
@@ -1,7 +1,7 @@
{
"name": "ruflo-graph-intelligence",
"description": "RuFlo Graph Intelligence Engine — real-time relationship intelligence with complexity-aware execution. Single-entry personalized PageRank, streaming delta updates, witness-signed reasoning artifacts, federation-distributable PR vectors. Built on sublinear-time-solver@1.7.0.",
"version": "0.2.0-alpha.2",
"version": "0.2.0-alpha.3",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+1 -1
View File
@@ -2,7 +2,7 @@
Graph Intelligence Engine for RuFlo: single-entry personalized PageRank, streaming delta updates and witness-signed reasoning artifacts (ADR-123), built on sublinear-time-solver. Library: `npm test`, `npm run build`.
## As a mod (0.2.0-alpha.2)
## As a mod (0.2.0-alpha.3)
A function-hook mod ships beside the skills (ADR-445 pattern). Needs a Claude Code with mods (2.1.287+); older builds ignore it. No network, no process spawning: it only tightens calls to this plugin's own tools and reads through tools already connected.
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
@@ -1,7 +1,7 @@
{
"name": "ruflo-intelligence",
"description": "User-facing surface for Ruflo's self-learning system: 6 neural_* + 10 hooks_intelligence_* + 6 routing/meta hooks + 3 hooks_model-* + 4 SONA/MicroLoRA tools (29 total). Implements the 4-step pipeline (RETRIEVE → JUDGE → DISTILL → CONSOLIDATE) and IPFS-based cross-project pattern transfer.",
"version": "0.4.1",
"version": "0.4.2",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+1 -1
View File
@@ -127,7 +127,7 @@ Configure once via `mcp tool call hooks_intelligence -- '{"mode": "moe", "enable
- `intelligence-route` — Route tasks using learned patterns; produces a `hooks_explain` rationale
- `intelligence-transfer` — Publish/fetch patterns via IPFS (`hooks_transfer`)
## As a mod (0.4.1)
## As a mod (0.4.2)
A function-hook mod ships beside the skills (ADR-445 pattern). Needs a Claude Code with mods (2.1.287+); older builds ignore it. No network, no process spawning: it only tightens calls to this plugin's own tools and reads through tools already connected.
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+4 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Structural smoke test for ruflo-intelligence v0.4.1.
# Structural smoke test for ruflo-intelligence v0.4.2.
# Per ADR-0001 §6 Verification: 13 contract checks + 3 doc invariants.
# Offline-safe; no live MCP calls.
set -u
@@ -27,10 +27,10 @@ grep -qE "IPFS.+pattern|pattern.+IPFS|Pinata" "$ROOT/README.md" \
&& ok || bad "README missing IPFS / Pinata transfer docs"
# 1. plugin.json bump + new keywords
step "1. plugin.json declares 0.4.1 with new keywords"
step "1. plugin.json declares 0.4.2 with new keywords"
v=$(grep -E '"version"[[:space:]]*:' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.4.1" ]]; then
bad "expected 0.4.1, got '$v'"
if [[ "$v" != "0.4.2" ]]; then
bad "expected 0.4.2, got '$v'"
else
miss=""
for k in microlora ewc attention moe pattern-transfer model-routing; do
@@ -1,7 +1,7 @@
{
"name": "ruflo-iot-cognitum",
"description": "IoT device lifecycle, telemetry anomaly detection, fleet management, and witness chain verification for Cognitum Seed hardware",
"version": "0.3.1",
"version": "0.3.2",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+1 -1
View File
@@ -154,7 +154,7 @@ This plugin owns five AgentDB namespaces, all compliant with the [ruflo-agentdb
Reserved namespaces (`pattern`, `claude-memories`, `default`) MUST NOT be shadowed.
## As a mod (0.3.1)
## As a mod (0.3.2)
A function-hook mod ships beside the skills (ADR-445 pattern). Needs a Claude Code with mods (2.1.287+); older builds ignore it. No network, no process spawning: it only tightens calls to this plugin's own tools and reads through tools already connected.
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+2 -2
View File
@@ -7,9 +7,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.3.1 with new keywords"
step "1. plugin.json declares 0.3.2 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.3.1" ]]; then bad "expected 0.3.1, got '$v'"; else
if [[ "$v" != "0.3.2" ]]; then bad "expected 0.3.2, got '$v'"; else
miss=""
for k in mcp cognitum-seed 5-tier-trust; do
grep -q "\"$k\"" "$ROOT/.claude-plugin/plugin.json" || miss="$miss $k"
@@ -1,7 +1,7 @@
{
"name": "ruflo-jujutsu",
"description": "Advanced git workflows with diff analysis, risk scoring, change classification (feature/bugfix/refactor/...), and reviewer recommendations — wraps 6 analyze_* MCP tools (diff, diff-risk, diff-classify, diff-reviewers, file-risk, diff-stats). As a mod (ADR-445): a PR-text and merge/close guard, /jujutsu-mod, and a status file the console shows",
"version": "0.3.2",
"version": "0.3.3",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+2 -2
View File
@@ -7,9 +7,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.3.2 with new keywords"
step "1. plugin.json declares 0.3.3 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.3.2" ]]; then bad "expected 0.3.2, got '$v'"; else
if [[ "$v" != "0.3.3" ]]; then bad "expected 0.3.3, got '$v'"; else
miss=""
for k in mcp change-classification reviewer-recommendation; do
grep -q "\"$k\"" "$ROOT/.claude-plugin/plugin.json" || miss="$miss $k"
@@ -1,7 +1,7 @@
{
"name": "ruflo-knowledge-graph",
"description": "Knowledge graph construction — entity extraction, relation mapping, and pathfinder graph traversal. As a mod (ADR-445): keeps secrets out of the graph, confirms graph deletes, /kg-mod, and a status file the console shows",
"version": "0.3.2",
"version": "0.3.3",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
@@ -7,9 +7,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.3.2 with new keywords"
step "1. plugin.json declares 0.3.3 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.3.2" ]]; then bad "expected 0.3.2, got '$v'"; else
if [[ "$v" != "0.3.3" ]]; then bad "expected 0.3.3, got '$v'"; else
miss=""
for k in mcp pathfinder-traversal entity-extraction; do
grep -q "\"$k\"" "$ROOT/.claude-plugin/plugin.json" || miss="$miss $k"
@@ -1,7 +1,7 @@
{
"name": "ruflo-loop-workers",
"description": "Cache-aware /loop workers and CronCreate background automation — wraps 5 hooks_worker-* MCP tools (list/dispatch/status/detect/cancel) and exposes 12 background worker triggers (ultralearn, optimize, consolidate, predict, audit, map, preload, deepdive, document, refactor, benchmark, testgaps). As a mod (ADR-445): a per-session dispatch cap, no secrets in worker context, /loop-mod, and a status file the console shows",
"version": "0.3.2",
"version": "0.3.3",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+2 -2
View File
@@ -7,9 +7,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.3.2 with new keywords"
step "1. plugin.json declares 0.3.3 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.3.2" ]]; then bad "expected 0.3.2, got '$v'"; else
if [[ "$v" != "0.3.3" ]]; then bad "expected 0.3.3, got '$v'"; else
miss=""
for k in mcp background-workers cache-aware schedule-wakeup; do
grep -q "\"$k\"" "$ROOT/.claude-plugin/plugin.json" || miss="$miss $k"
@@ -1,7 +1,7 @@
{
"name": "ruflo-market-data",
"description": "Market data ingestion — feed normalization, OHLCV vectorization, and HNSW-indexed pattern matching. As a mod (ADR-445): keeps credentials and credentialed feed URLs out of market memory, /market-mod, and a status file the console shows",
"version": "0.3.2",
"version": "0.3.3",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+2 -2
View File
@@ -7,9 +7,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.3.2 with new keywords"
step "1. plugin.json declares 0.3.3 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.3.2" ]]; then bad "expected 0.3.2, got '$v'"; else
if [[ "$v" != "0.3.3" ]]; then bad "expected 0.3.3, got '$v'"; else
miss=""
for k in mcp candlestick-patterns namespace-routing; do
grep -q "\"$k\"" "$ROOT/.claude-plugin/plugin.json" || miss="$miss $k"
@@ -1,7 +1,7 @@
{
"name": "ruflo-metaharness",
"description": "MetaHarness integration for ruflo — surfaces score/genome/mint/mcp-scan/threat-model via skills; pairs with @metaharness/router (ADR-148/149) for cost-optimal model routing; honors ADR-150's architectural constraint that MetaHarness remains an optional augmentation, never a required runtime dependency. As a mod (ADR-445): confirms flywheel promotion, keeps secrets out of inputs, /metaharness-mod, and a status file the console shows",
"version": "0.2.2",
"version": "0.2.3",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+4 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Structural smoke test for ruflo-metaharness v0.2.2 (ADR-150 Phase 1).
# Structural smoke test for ruflo-metaharness v0.2.3 (ADR-150 Phase 1).
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PASS=0
@@ -32,10 +32,10 @@ else
bad "extraction-regex-rot: EXPECTED_TOOLS=$EXPECTED_TOOLS EXPECTED_SUBS=$EXPECTED_SUBS"
fi
step "1. plugin.json declares 0.2.2 with adr-150 keywords"
step "1. plugin.json declares 0.2.3 with adr-150 keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.2.2" ]]; then
bad "expected 0.2.2, got '$v'"
if [[ "$v" != "0.2.3" ]]; then
bad "expected 0.2.3, got '$v'"
else
miss=""
for k in ruflo metaharness harness scorecard genome mcp-scan threat-model router adr-150 adr-148 adr-149 optional-dependency graceful-degradation subprocess phase-1-mvp; do
@@ -1,7 +1,7 @@
{
"name": "ruflo-migrations",
"description": "Schema migration management — generate, validate, dry-run, and rollback database migrations. As a mod (ADR-445): a tighten-only guard on its tools, /migrations-mod, and a status file the console shows",
"version": "0.3.1",
"version": "0.3.2",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+2 -2
View File
@@ -7,9 +7,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.3.1 with new keywords"
step "1. plugin.json declares 0.3.2 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.3.1" ]]; then bad "expected 0.3.1, got '$v'"; else
if [[ "$v" != "0.3.2" ]]; then bad "expected 0.3.2, got '$v'"; else
miss=""
for k in mcp dry-run up-down-pairs; do
grep -q "\"$k\"" "$ROOT/.claude-plugin/plugin.json" || miss="$miss $k"
@@ -1,7 +1,7 @@
{
"name": "ruflo-music",
"description": "AI music generation via Cognitum Music (cogmusic MCP) — compose lyrics/prompts, generate tracks, separate stems, extract MIDI, and master through your own music.cognitum.one account. As a mod (ADR-445): a tighten-only guard on its tools, /music-mod, and a status file the console shows",
"version": "0.2.1",
"version": "0.2.2",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+2 -2
View File
@@ -7,9 +7,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.2.1 with expected keywords"
step "1. plugin.json declares 0.2.2 with expected keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.2.1" ]]; then bad "expected 0.2.1, got '$v'"; else
if [[ "$v" != "0.2.2" ]]; then bad "expected 0.2.2, got '$v'"; else
miss=""
for k in cogmusic cognitum mcp; do
grep -q "\"$k\"" "$ROOT/.claude-plugin/plugin.json" || miss="$miss $k"
@@ -1,7 +1,7 @@
{
"name": "ruflo-neural-trader",
"description": "Neural trading via npx neural-trader — self-learning strategies, Rust/NAPI backtesting, 112+ MCP tools, swarm coordination, and portfolio optimization. As a mod (ADR-445): a tighten-only guard on its tools, /trader-mod, and a status file the console shows",
"version": "0.3.2",
"version": "0.3.3",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+2 -2
View File
@@ -7,9 +7,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.3.2 with new keywords"
step "1. plugin.json declares 0.3.3 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.3.2" ]]; then bad "expected 0.3.2, got '$v'"; else
if [[ "$v" != "0.3.3" ]]; then bad "expected 0.3.3, got '$v'"; else
miss=""
for k in neural-trader-runtime walk-forward monte-carlo; do
grep -q "\"$k\"" "$ROOT/.claude-plugin/plugin.json" || miss="$miss $k"
@@ -1,7 +1,7 @@
{
"name": "ruflo-observability",
"description": "Structured logging, distributed tracing, and metrics — correlate agent swarm activity with application telemetry. As a mod (ADR-445): a tighten-only guard on its tools, /observe-mod, and a status file the console shows",
"version": "0.3.1",
"version": "0.3.2",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+27 -5
View File
@@ -144,8 +144,15 @@ function matches(name: string, re: RegExp, text: string): boolean {
return false
}
/**
* The text textsOf appends when it had to drop input (a node, character or per-string budget ran out). It is never matched against a rule:
* `names` reports it as a finding of its own, so every guard that asks "is there a secret in these texts" refuses what it could not read in full.
*/
export const TRUNCATED = 'ruflo-screen: input exceeded the screening budget'
export const TRUNCATED_NAME = 'input too large to screen'
/** Names of the rules that match `text` (already bare'd). A rule named 'key assignment' is judged by assignmentSecret, whatever its regex. */
export const names = (rules: Rules, text: string) => rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export const names = (rules: Rules, text: string) => text === TRUNCATED ? [TRUNCATED_NAME] : rules.filter(([name, re]) => matches(name, re, text)).map(([name]) => name)
export type Findings = { readonly secrets: readonly string[]; readonly injection: readonly string[] }
@@ -187,33 +194,46 @@ function windows(text: string, out: string[]): void {
* breadth-first (siblings before depth, so a long list cannot hide a nested value). A string under an object key comes back as `key=value`,
* so a secret-named key is judged with its value; a key whose value is not a string is returned bare. Strings longer than the screen window
* come back as overlapping windows; one over `perString` keeps its head and tail. Work is bounded by `nodes` slots and `chars` characters.
* Anything dropped (slots or characters ran out, or a string lost its middle) is reported by a final TRUNCATED text, which `names` and
* `hasSecretIn` count as a finding, so the screen fails closed instead of passing what it did not read.
*/
export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
const out: string[] = []
let slots = limits.nodes ?? NODES
let chars = limits.chars ?? CHARS
const perString = limits.perString ?? PER_STRING
let truncated = false
const take = (text: string): void => {
if (chars <= 0) return
if (chars <= 0) {
truncated = true
return
}
if (text.length <= Math.min(perString, chars)) {
chars -= text.length
windows(text, out)
return
}
truncated = true
const half = Math.floor(Math.min(perString, chars) / 2)
chars -= 2 * half
windows(text.slice(0, half), out)
windows(text.slice(-half), out)
}
const queue: unknown[] = [input]
for (let head = 0; head < queue.length && chars > 0; head++) {
let head = 0
for (; head < queue.length && chars > 0; head++) {
const node = queue[head]
if (typeof node === 'string') take(node)
else if (Array.isArray(node)) {
for (let i = 0; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
let i = 0
for (; i < node.length && slots > 0; i++, slots--) if (i in node) queue.push(node[i])
if (i < node.length) truncated = true
} else if (typeof node === 'object' && node !== null) {
for (const k in node) {
if (slots-- <= 0) break
if (slots-- <= 0) {
truncated = true
break
}
if (!Object.prototype.hasOwnProperty.call(node, k)) continue
const v = (node as Record<string, unknown>)[k]
if (typeof v === 'string') queue.push(k + '=' + v)
@@ -224,6 +244,8 @@ export function textsOf(input: unknown, limits: TextLimits = {}): string[] {
}
}
}
if (queue.length > head) truncated = true
if (truncated) out.push(TRUNCATED)
return out
}
// END SHARED SCREEN
+2 -2
View File
@@ -7,9 +7,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.3.1 with new keywords"
step "1. plugin.json declares 0.3.2 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.3.1" ]]; then bad "expected 0.3.1, got '$v'"; else
if [[ "$v" != "0.3.2" ]]; then bad "expected 0.3.2, got '$v'"; else
miss=""
for k in mcp distributed-tracing anomaly-detection; do
grep -q "\"$k\"" "$ROOT/.claude-plugin/plugin.json" || miss="$miss $k"
@@ -1,7 +1,7 @@
{
"name": "ruflo-rag-memory",
"description": "RuVector memory with HNSW search, AgentDB, and semantic retrieval. As a mod (ADR-445): a write guard that keeps secrets out of memory, /rag-mod, and a status file the console shows",
"version": "0.3.3",
"version": "0.3.4",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff. Show more