@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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # Structural smoke test for ruflo-metaharness v0.1.1 (ADR-150 Phase 1).
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.1.1 with adr-150 keywords"
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.1.1" ]]; then
38
- bad "expected 0.1.1, got '$v'"
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 produces sub-5μs results across all categories
1070
- node "$F" --iters 10000 --max-mean-us 5 >/dev/null 2>&1 || miss="$miss runtime-fails-or-perf-blew-5us"
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
+ })
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "./.claude-plugin/types/tsconfig.json"
3
+ }