@claude-flow/cli 3.51.1 → 3.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/helpers/.helpers-version +1 -1
- package/.claude/helpers/helpers.manifest.json +2 -2
- package/.claude/helpers/router.cjs +0 -0
- package/README.md +19 -2
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/guidance.js +43 -2
- package/dist/src/guidance/mod-projection.d.ts +120 -0
- package/dist/src/guidance/mod-projection.js +155 -0
- package/dist/src/guidance/mod-sources.d.ts +22 -0
- package/dist/src/guidance/mod-sources.js +123 -0
- package/dist/src/init/helpers-generator.js +75 -4
- package/package.json +1 -1
- package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +20 -4
- package/plugins/ruflo-metaharness/README.md +12 -0
- package/plugins/ruflo-metaharness/hooks/guard.ts +27 -0
- package/plugins/ruflo-metaharness/hooks/hooks.json +4 -0
- package/plugins/ruflo-metaharness/hooks/options.ts +14 -0
- package/plugins/ruflo-metaharness/hooks/register.ts +108 -0
- package/plugins/ruflo-metaharness/hooks/screen.ts +260 -0
- package/plugins/ruflo-metaharness/scripts/bench-parse-mcp-scan.mjs +13 -4
- package/plugins/ruflo-metaharness/scripts/smoke.sh +29 -7
- package/plugins/ruflo-metaharness/tests/register.test.ts +117 -0
- package/plugins/ruflo-metaharness/tests/textsof.test.ts +22 -0
- package/plugins/ruflo-metaharness/tsconfig.json +3 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Structural smoke test for ruflo-metaharness v0.
|
|
2
|
+
# Structural smoke test for ruflo-metaharness v0.2.3 (ADR-150 Phase 1).
|
|
3
3
|
set -u
|
|
4
4
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
5
|
PASS=0
|
|
@@ -32,10 +32,10 @@ else
|
|
|
32
32
|
bad "extraction-regex-rot: EXPECTED_TOOLS=$EXPECTED_TOOLS EXPECTED_SUBS=$EXPECTED_SUBS"
|
|
33
33
|
fi
|
|
34
34
|
|
|
35
|
-
step "1. plugin.json declares 0.
|
|
35
|
+
step "1. plugin.json declares 0.2.3 with adr-150 keywords"
|
|
36
36
|
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
|
37
|
-
if [[ "$v" != "0.
|
|
38
|
-
bad "expected 0.
|
|
37
|
+
if [[ "$v" != "0.2.3" ]]; then
|
|
38
|
+
bad "expected 0.2.3, got '$v'"
|
|
39
39
|
else
|
|
40
40
|
miss=""
|
|
41
41
|
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
|
|
@@ -1066,10 +1066,13 @@ done
|
|
|
1066
1066
|
grep -q "from './_harness.mjs'" "$F" 2>/dev/null || miss="$miss not-using-production-parser"
|
|
1067
1067
|
# Gate flag exposed
|
|
1068
1068
|
grep -q -- "--max-mean-us" "$F" 2>/dev/null || miss="$miss no-gate-flag"
|
|
1069
|
-
# Runtime: bench
|
|
1070
|
-
|
|
1069
|
+
# Runtime: the parse bench stays an order of magnitude inside its regression ceiling. 30us on the MEDIAN: the CI runner measured p50 6.05us for the 5-finding payload (local 1us), so the old 5us ceiling failed there on speed alone; 30us is 5x the CI median and still trips on a 10x regression.
|
|
1070
|
+
# The median, not the mean: a scheduler pause on a loaded CI runner moves the mean and flaked this step in the fleet smoke.
|
|
1071
|
+
grep -q -- "--gate-stat" "$F" 2>/dev/null || miss="$miss no-gate-stat-flag"
|
|
1072
|
+
# On failure keep the bench's own output on stderr (the fleet JSON report captures stderrTail): the reason was invisible in CI before.
|
|
1073
|
+
bench_out=$(node "$F" --iters 10000 --max-mean-us 30 --gate-stat p50 2>&1) || { miss="$miss runtime-fails-or-perf-blew-30us"; printf '%s\n' "$bench_out" | tail -15 >&2; }
|
|
1071
1074
|
# Runtime: gate trips on absurd ceiling
|
|
1072
|
-
if node "$F" --iters 10000 --max-mean-us 0.0001 >/dev/null 2>&1; then
|
|
1075
|
+
if node "$F" --iters 10000 --max-mean-us 0.0001 --gate-stat p50 >/dev/null 2>&1; then
|
|
1073
1076
|
miss="$miss gate-failed-to-trip"
|
|
1074
1077
|
fi
|
|
1075
1078
|
[[ -z "$miss" ]] && ok || bad "$miss"
|
|
@@ -2449,5 +2452,24 @@ LOADER="$ROOT/../../v3/@claude-flow/cli/src/commands/index.ts"
|
|
|
2449
2452
|
grep -q "eject: () => import" "$LOADER" 2>/dev/null || miss="$miss not-registered-in-loader"
|
|
2450
2453
|
[[ -z "$miss" ]] && ok || bad "$miss"
|
|
2451
2454
|
|
|
2455
|
+
# M1-M3. The mod (ADR-445): hooks module registered, files within the 500-line rule, no network or process access in the hooks
|
|
2456
|
+
step "M1. mod: hooks.json names register.ts, every hook file is present and under 500 lines"
|
|
2457
|
+
mod_ok=1
|
|
2458
|
+
grep -q '"./register.ts"' "$ROOT/hooks/hooks.json" || mod_ok=0
|
|
2459
|
+
for f in options screen guard register; do
|
|
2460
|
+
[[ -f "$ROOT/hooks/$f.ts" ]] || mod_ok=0
|
|
2461
|
+
[[ $(wc -l < "$ROOT/hooks/$f.ts" 2>/dev/null || echo 9999) -le 500 ]] || mod_ok=0
|
|
2462
|
+
done
|
|
2463
|
+
[[ $mod_ok -eq 1 ]] && ok || bad "mod hooks incomplete or too long"
|
|
2464
|
+
|
|
2465
|
+
step "M2. mod: guard defaults on (userConfig), the command name is metaharness-mod"
|
|
2466
|
+
node -e '
|
|
2467
|
+
const c = require(process.argv[1]).userConfig || {}
|
|
2468
|
+
process.exit(c.guard && c.guard.default === "on" ? 0 : 1)
|
|
2469
|
+
' "$ROOT/.claude-plugin/plugin.json" && grep -q "'metaharness-mod'" "$ROOT/hooks/register.ts" && ok || bad "userConfig defaults or command name wrong"
|
|
2470
|
+
|
|
2471
|
+
step "M3. mod: hooks never touch the network or spawn a process"
|
|
2472
|
+
if grep -nE '\$\.(http|process)\.|child_process|fetch\(' "$ROOT"/hooks/*.ts >/dev/null; then bad "network or process call in hooks"; else ok; fi
|
|
2473
|
+
|
|
2452
2474
|
printf "\n%s passed, %s failed\n" "$PASS" "$FAIL"
|
|
2453
2475
|
[[ $FAIL -eq 0 ]] || exit 1
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { On } from 'claude-code'
|
|
2
|
+
import { describe, expect, test, tier } from 'claude-code/testing'
|
|
3
|
+
|
|
4
|
+
tier('user')
|
|
5
|
+
|
|
6
|
+
const ROOT = '/work'
|
|
7
|
+
const P = 'mcp__plugin_ruflo-core_ruflo__'
|
|
8
|
+
const START = { surface: 'terminal', isInteractive: true, cwd: ROOT } as const
|
|
9
|
+
const slash = (args: string) => ({ command: 'metaharness-mod', args, origin: { kind: 'composer' }, presentation: { isFullscreen: false, columns: 100 } }) as const
|
|
10
|
+
const TOOLS = ["metaharness_score","metaharness_flywheel"].map(name => ({ name: P + name, description: '', mcp: true }))
|
|
11
|
+
|
|
12
|
+
/** The world beneath the mod: a project root, a file map, the connected tools, and a stub that answers every tool call. */
|
|
13
|
+
function world(on: On, tools = TOOLS) {
|
|
14
|
+
const files = new Map<string, string>()
|
|
15
|
+
const registered: string[] = []
|
|
16
|
+
on('session.start', ($, e) => ({ cwd: e.cwd }))
|
|
17
|
+
on('session.root', () => ({ value: ROOT }))
|
|
18
|
+
on('command.register', ($, e) => (registered.push(e.name), { value: { command: e.name } }))
|
|
19
|
+
on('fs.write', ($, e) => (files.set(e.path, e.text), { value: undefined }))
|
|
20
|
+
on('tool.list', () => ({ value: tools }))
|
|
21
|
+
on('clock.now', () => ({ value: Date.now() }))
|
|
22
|
+
on('tool.call', () => ({ result: 'ran' }))
|
|
23
|
+
return { files, registered, status: () => JSON.parse(files.get(`${ROOT}/.claude-flow/metaharness-mod/status.json`) ?? '{}') }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const attempt = async ($: Parameters<Parameters<typeof test>[2]>[0], call: object) =>
|
|
27
|
+
JSON.stringify(await $.tool.call(call as never).then(r => r, (e: unknown) => ({ text: String(e) })))
|
|
28
|
+
|
|
29
|
+
describe('guard', () => {
|
|
30
|
+
test('refuses: flywheel promote without confirm, and never echoes the secret', async ($, on) => {
|
|
31
|
+
world(on)
|
|
32
|
+
await $.session.start(START)
|
|
33
|
+
const out = await attempt($, { tool: P + 'metaharness_flywheel', operation: 'promote', receiptId: 'r1' })
|
|
34
|
+
expect(out).not.toContain('ran')
|
|
35
|
+
expect(out).toContain('ruflo-metaharness')
|
|
36
|
+
expect(out).not.toContain('ghp_')
|
|
37
|
+
expect(out).not.toContain('abcd1234')
|
|
38
|
+
expect(out).not.toContain('Zx9Qm2')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('refuses: secret in an input, and never echoes the secret', async ($, on) => {
|
|
42
|
+
world(on)
|
|
43
|
+
await $.session.start(START)
|
|
44
|
+
const out = await attempt($, { tool: P + 'metaharness_score', path: `/repo token ghp_${'a1B2'.repeat(10)}` })
|
|
45
|
+
expect(out).not.toContain('ran')
|
|
46
|
+
expect(out).toContain('ruflo-metaharness')
|
|
47
|
+
expect(out).not.toContain('ghp_')
|
|
48
|
+
expect(out).not.toContain('abcd1234')
|
|
49
|
+
expect(out).not.toContain('Zx9Qm2')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('passes good calls through', async ($, on) => {
|
|
53
|
+
world(on)
|
|
54
|
+
await $.session.start(START)
|
|
55
|
+
expect(await attempt($, { tool: P + 'metaharness_flywheel', operation: 'promote', receiptId: 'r1', confirm: true })).toContain('ran') // promote with confirm
|
|
56
|
+
expect(await attempt($, { tool: P + 'metaharness_flywheel', operation: 'status' })).toContain('ran') // flywheel status
|
|
57
|
+
expect(await attempt($, { tool: P + 'metaharness_score', path: '.' })).toContain('ran') // score
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('turning that option off lets the call through', { options: { guard: 'off' } }, async ($, on) => {
|
|
61
|
+
world(on)
|
|
62
|
+
await $.session.start(START)
|
|
63
|
+
expect(await attempt($, { tool: P + 'metaharness_flywheel', operation: 'promote', receiptId: 'r1' })).toContain('ran')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('ignores tools that belong to someone else', async ($, on) => {
|
|
67
|
+
world(on)
|
|
68
|
+
await $.session.start(START)
|
|
69
|
+
expect(await attempt($, { tool: P + 'some_other_tool', value: `token ghp_${'a1B2'.repeat(10)}` })).toContain('ran')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('status file and /metaharness-mod', () => {
|
|
74
|
+
test('the status file is written at session start with the shape the console reads', async ($, on) => {
|
|
75
|
+
const w = world(on)
|
|
76
|
+
await $.session.start(START)
|
|
77
|
+
expect(w.registered).toEqual(['metaharness-mod'])
|
|
78
|
+
expect(w.status()).toMatchObject({ version: 1, guard: true, total: 0, blocked: 0, calls: {} })
|
|
79
|
+
expect(typeof w.status().updatedMs).toBe('number')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('counters move when own tools run and when the guard refuses', async ($, on) => {
|
|
83
|
+
const w = world(on)
|
|
84
|
+
await $.session.start(START)
|
|
85
|
+
await attempt($, { tool: P + 'metaharness_score', path: '.' })
|
|
86
|
+
await attempt($, { tool: P + 'metaharness_flywheel', operation: 'promote', receiptId: 'r1' })
|
|
87
|
+
expect(w.status()).toMatchObject({ total: 1, blocked: 1 })
|
|
88
|
+
expect(Object.keys(w.status().calls)).toHaveLength(1)
|
|
89
|
+
expect(JSON.stringify(w.status())).not.toContain('ghp_')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('status, tools, recent, scan and help answer locally', async ($, on) => {
|
|
93
|
+
world(on)
|
|
94
|
+
await $.session.start(START)
|
|
95
|
+
expect((await $.command.run(slash('status'))).text).toContain('guard on')
|
|
96
|
+
expect((await $.command.run(slash('tools'))).text).toContain('metaharness_score')
|
|
97
|
+
expect((await $.command.run(slash('recent'))).text).toContain('Nothing')
|
|
98
|
+
expect((await $.command.run(slash(`scan key ghp_${'a1B2'.repeat(10)}`))).text).toContain('github token')
|
|
99
|
+
expect((await $.command.run(slash('scan plain words'))).text).toContain('Nothing found')
|
|
100
|
+
expect((await $.command.run(slash(''))).text).toContain('/metaharness-mod status')
|
|
101
|
+
expect((await $.command.run(slash('what is this'))).text).toContain('Unknown')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('tools says so plainly when none are connected', async ($, on) => {
|
|
105
|
+
world(on, [])
|
|
106
|
+
await $.session.start(START)
|
|
107
|
+
expect((await $.command.run(slash('tools'))).text).toContain('None of this plugin')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('defaults', () => {
|
|
112
|
+
test('with no options the guard is on', async ($, on) => {
|
|
113
|
+
const w = world(on)
|
|
114
|
+
await $.session.start(START)
|
|
115
|
+
expect(w.status().guard).toBe(true)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, test, tier } from 'claude-code/testing'
|
|
2
|
+
|
|
3
|
+
import { textsOf } from '../hooks/guard'
|
|
4
|
+
import { hasSecret } from '../hooks/screen'
|
|
5
|
+
|
|
6
|
+
tier('user')
|
|
7
|
+
|
|
8
|
+
const AWS = `AKIA${'IOSFODNN7EXAMPLE'}`
|
|
9
|
+
const BIG = 'QUJD'.repeat(10_000)
|
|
10
|
+
const found = (input: unknown) => textsOf(input).some(hasSecret)
|
|
11
|
+
|
|
12
|
+
describe('what the guard reads', () => {
|
|
13
|
+
test('a key name leads its string value, so an object-shaped assignment is caught', () => {
|
|
14
|
+
expect(found({ meta: { api_key: 'abcdef1234567890abcdef' } })).toBe(true)
|
|
15
|
+
expect(found({ meta: { maxTokens: 4096, name: 'refresh policy' } })).toBe(false)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('a long field does not starve the fields after it', () => {
|
|
19
|
+
expect(found({ value: BIG, note: AWS })).toBe(true)
|
|
20
|
+
expect(found({ a: BIG, b: BIG, c: BIG, d: 'plain text' })).toBe(false)
|
|
21
|
+
})
|
|
22
|
+
})
|