@claude-flow/cli 3.32.26 → 3.32.29

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.
Files changed (36) hide show
  1. package/.claude/helpers/.helpers-version +1 -1
  2. package/.claude/helpers/helpers.manifest.json +2 -2
  3. package/catalog-manifest.json +4 -4
  4. package/dist/src/commands/index.d.ts +1 -0
  5. package/dist/src/commands/index.js +5 -3
  6. package/dist/src/commands/memory.js +49 -5
  7. package/dist/src/commands/metaharness.js +25 -5
  8. package/dist/src/commands/policy.d.ts +4 -0
  9. package/dist/src/commands/policy.js +107 -0
  10. package/dist/src/index.js +18 -0
  11. package/dist/src/mcp-client.js +25 -1
  12. package/dist/src/mcp-tools/capability-brain.d.ts +134 -0
  13. package/dist/src/mcp-tools/capability-brain.js +697 -0
  14. package/dist/src/mcp-tools/guidance-tools.d.ts +2 -0
  15. package/dist/src/mcp-tools/guidance-tools.js +369 -37
  16. package/dist/src/mcp-tools/index.d.ts +4 -1
  17. package/dist/src/mcp-tools/index.js +3 -1
  18. package/dist/src/mcp-tools/memory-tools.js +26 -0
  19. package/dist/src/mcp-tools/metaharness-tools.js +27 -1
  20. package/dist/src/mcp-tools/policy-tools.d.ts +3 -0
  21. package/dist/src/mcp-tools/policy-tools.js +121 -0
  22. package/dist/src/memory/memory-bridge.d.ts +11 -0
  23. package/dist/src/memory/memory-bridge.js +100 -21
  24. package/dist/src/memory/memory-initializer.d.ts +22 -1
  25. package/dist/src/memory/memory-initializer.js +184 -39
  26. package/dist/src/services/bounded-worker-pool.d.ts +28 -0
  27. package/dist/src/services/bounded-worker-pool.js +90 -0
  28. package/dist/src/services/harness-flywheel-runtime.d.ts +2 -0
  29. package/dist/src/services/harness-flywheel-runtime.js +18 -0
  30. package/dist/src/services/harness-flywheel.d.ts +4 -1
  31. package/dist/src/services/harness-flywheel.js +27 -6
  32. package/dist/src/services/policy-runtime.d.ts +38 -0
  33. package/dist/src/services/policy-runtime.js +340 -0
  34. package/package.json +22 -6
  35. package/plugins/ruflo-metaharness/scripts/smoke.sh +22 -14
  36. package/plugins/ruflo-metaharness/scripts/test-mcp-tools.mjs +3 -1
@@ -10,6 +10,7 @@ import { runFlywheelGeneration, checkServedChampionDrift } from './harness-flywh
10
10
  import { loadFrozenHumanEval } from './harness-frozen-eval.js';
11
11
  import { proposeFlywheelCandidates, } from './flywheel-proposer.js';
12
12
  import { sha256Ref } from './flywheel-receipt.js';
13
+ import { evaluatePolicyRequest } from './policy-runtime.js';
13
14
  /** The human-labeled ADR-081 anchor — the never-regress relevance set. */
14
15
  const ANCHOR = [
15
16
  ['how was the Opus model alias fixed', ['opus 4.8', 'opus alias', 'opus model alias', '#2232']],
@@ -31,6 +32,18 @@ export async function runFlywheelWorker(projectRoot, opts = {}) {
31
32
  try {
32
33
  if (!(opts.optInOverride ?? harnessLoopOptedIn()))
33
34
  return { ran: false, reason: 'opt-in required (RUFLO_HARNESS_LOOP=1)' };
35
+ const policy = await evaluatePolicyRequest({
36
+ identity: { id: process.env.CLAUDE_FLOW_PRINCIPAL_ID ?? 'metaharness-local', type: 'agent', roles: ['optimizer'] },
37
+ action: {
38
+ type: 'metaharness.flywheel.run',
39
+ resource: projectRoot,
40
+ environment: 'development',
41
+ concurrency: opts.maxConcurrency ?? 2,
42
+ },
43
+ }, projectRoot);
44
+ if (policy.enforcedOutcome !== 'allowed') {
45
+ return { ran: false, reason: `policy-${policy.enforcedOutcome}:${policy.reason}; receipt=${policy.receiptId}` };
46
+ }
34
47
  const neural = await import('../mcp-tools/neural-tools.js');
35
48
  const applier = await import('../config/harness-feedback-applier.js');
36
49
  const tool = neural.neuralTools.find((t) => t.name === 'neural_patterns');
@@ -64,6 +77,7 @@ export async function runFlywheelWorker(projectRoot, opts = {}) {
64
77
  maxFailureRate: 0.01,
65
78
  maxEvaluationCostMicros: 1_000_000,
66
79
  };
80
+ // CLI flag opts.proposer takes precedence over RUFLO_FLYWHEEL_PROPOSER.
67
81
  const proposerMode = opts.proposer
68
82
  ?? (process.env.RUFLO_FLYWHEEL_PROPOSER ?? 'auto');
69
83
  if (!['auto', 'local', 'darwin'].includes(proposerMode)) {
@@ -86,6 +100,8 @@ export async function runFlywheelWorker(projectRoot, opts = {}) {
86
100
  })),
87
101
  darwinInvoker: opts.darwinInvoker,
88
102
  allowSubstitutionPromotion: opts.allowSubstitutionPromotion,
103
+ maxConcurrency: Math.max(1, Math.min(opts.maxConcurrency ?? 2, 8)),
104
+ maxWallTimeMs: opts.evaluationTimeoutMs ?? 120_000,
89
105
  });
90
106
  const candidatePolicies = archive.paretoCandidates.map((candidate) => candidate.policy);
91
107
  if (!candidatePolicies.length)
@@ -109,6 +125,8 @@ export async function runFlywheelWorker(projectRoot, opts = {}) {
109
125
  effectiveProposer: archive.effectiveProposer,
110
126
  proposerSubstitution: archive.proposerSubstitution,
111
127
  candidatePolicies,
128
+ maxConcurrency: Math.max(1, Math.min(opts.maxConcurrency ?? 2, 8)),
129
+ evaluationTimeoutMs: opts.evaluationTimeoutMs,
112
130
  };
113
131
  return await runFlywheelTick(projectRoot, deps);
114
132
  }
@@ -22,7 +22,7 @@ export interface AnchorTask {
22
22
  }
23
23
  export interface FlywheelDeps {
24
24
  getPatterns: () => HarvestPattern[] | Promise<HarvestPattern[]>;
25
- search: (query: string, config: RetrievalConfig) => Promise<RankedItem[]> | RankedItem[];
25
+ search: (query: string, config: RetrievalConfig, signal?: AbortSignal) => Promise<RankedItem[]> | RankedItem[];
26
26
  anchorTasks: AnchorTask[];
27
27
  activeParams?: () => Partial<RetrievalConfig> | null;
28
28
  sample?: number;
@@ -38,6 +38,9 @@ export interface FlywheelDeps {
38
38
  bootstrapIterations?: number;
39
39
  /** Candidate policies supplied by an external proposer archive (ADR-322B). */
40
40
  candidatePolicies?: RetrievalConfig[];
41
+ /** ADR-324: hard local cap for concurrent candidate/task evaluation. */
42
+ maxConcurrency?: number;
43
+ evaluationTimeoutMs?: number;
41
44
  }
42
45
  export interface FlywheelResult {
43
46
  ran: boolean;
@@ -28,6 +28,7 @@ import { applyChampionParams } from '../config/harness-feedback-applier.js';
28
28
  import { appendLedger, bootstrapDeltaCILow } from './harness-improvement-ledger.js';
29
29
  import { createFlywheelReceipt, sha256Ref, } from './flywheel-receipt.js';
30
30
  import { readFlywheelTransactionState, registerFlywheelReceipt, } from './flywheel-transaction.js';
31
+ import { runBoundedPool } from './bounded-worker-pool.js';
31
32
  export const DEFAULT_CONFIG = { alpha: 0.5, subjectWeight: 2.0, mmrLambda: 0.7, bodyWeight: 1.0, typePenaltyFactor: 1.0 };
32
33
  const EPS = 1e-3;
33
34
  const cfgCanon = (c) => JSON.stringify(Object.fromEntries(Object.keys(c).sort().map((k) => [k, c[k]])));
@@ -115,12 +116,32 @@ export async function evaluateFlywheelCandidate(projectRoot, deps) {
115
116
  // Precompute retrieval for baseline + all candidates over every task (async
116
117
  // I/O up front → the harness scoring stays pure/sync).
117
118
  const cache = new Map();
118
- const configs = [baseline, ...candidates];
119
- for (const cfg of configs) {
120
- for (const t of blended.tasks) {
121
- const q = t.input.q;
122
- cache.set(`${t.id}::${cfgKey(cfg)}`, (await deps.search(q, cfg)) || []);
123
- }
119
+ const configs = [...new Map([baseline, ...candidates].map((config) => [cfgKey(config), config])).values()];
120
+ const evalTasks = configs.flatMap((cfg) => blended.tasks.map((t) => {
121
+ const cacheKey = `${t.id}::${cfgKey(cfg)}`;
122
+ return {
123
+ id: cacheKey,
124
+ run: async (signal) => {
125
+ if (signal.aborted)
126
+ throw signal.reason;
127
+ return (await deps.search(t.input.q, cfg, signal)) || [];
128
+ },
129
+ };
130
+ }));
131
+ const batch = await runBoundedPool(evalTasks, {
132
+ maxConcurrency: deps.maxConcurrency ?? 2,
133
+ timeoutMs: deps.evaluationTimeoutMs ?? 120_000,
134
+ });
135
+ for (const item of batch.results) {
136
+ if (item.status === 'fulfilled')
137
+ cache.set(item.id, item.value ?? []);
138
+ }
139
+ const failedEvaluations = batch.results.filter((item) => item.status !== 'fulfilled');
140
+ if (failedEvaluations.length > 0) {
141
+ return {
142
+ ran: false,
143
+ reason: `candidate evaluation incomplete (${failedEvaluations.length}/${batch.results.length}); peak concurrency ${batch.peakConcurrency}`,
144
+ };
124
145
  }
125
146
  const evalFn = (input, cfg) => cache.get(`${input.id}::${cfgKey(cfg)}`) ?? [];
126
147
  const gradeFn = (output, expected) => grade(output, expected);
@@ -0,0 +1,38 @@
1
+ import { AgenticPolicyEngine, type BudgetLimit, type CapabilityEnvelope, type PolicyApproval, type PolicyDecision, type PolicyRequest, type PolicyRule, type PolicyState } from '@claude-flow/security';
2
+ export declare function loadPolicyState(projectRoot?: string): PolicyState;
3
+ export declare function autoMigratePolicyStateIfNeeded(projectRoot?: string): Promise<{
4
+ migrated: boolean;
5
+ statePath?: string;
6
+ mode?: PolicyState['mode'];
7
+ }>;
8
+ export declare function withPolicyTransaction<T>(projectRoot: string, operation: (engine: AgenticPolicyEngine) => T | Promise<T>, options?: {
9
+ approvalIssuerVerifier?: (issuer: string) => boolean;
10
+ }): Promise<T>;
11
+ export declare function evaluatePolicyRequest(request: PolicyRequest, projectRoot?: string): Promise<PolicyDecision>;
12
+ export declare function setPolicyMode(mode: PolicyState['mode'], projectRoot?: string): Promise<void>;
13
+ export declare function upsertPolicyRule(rule: PolicyRule, projectRoot?: string): Promise<void>;
14
+ export declare function setPolicyBudget(limit: BudgetLimit, projectRoot?: string): Promise<void>;
15
+ export declare function issuePolicyApproval(approval: Omit<PolicyApproval, 'uses' | 'issuedAt'> & {
16
+ uses?: number;
17
+ issuedAt?: number;
18
+ }, projectRoot?: string, approvalIssuerVerifier?: (issuer: string) => boolean): Promise<PolicyApproval>;
19
+ export declare function revokePolicyApproval(id: string, projectRoot?: string): Promise<boolean>;
20
+ export declare function verifyPolicyLedger(projectRoot?: string): Promise<ReturnType<AgenticPolicyEngine['verifyLedger']>>;
21
+ export declare function authorizeMcpTool(toolName: string, input: Record<string, unknown>, context?: Record<string, unknown>, attributes?: Readonly<{
22
+ actionType?: string;
23
+ network?: boolean;
24
+ destructive?: boolean;
25
+ namespaceAccess?: 'read' | 'write';
26
+ envelope?: CapabilityEnvelope;
27
+ costUsd?: number;
28
+ tokens?: number;
29
+ concurrency?: number;
30
+ }>): Promise<PolicyDecision>;
31
+ /** Trusted classification derived from the registered tool name, never input. */
32
+ export declare function classifyMcpTool(toolName: string): {
33
+ actionType: string;
34
+ network: boolean;
35
+ destructive: boolean;
36
+ namespaceAccess?: 'read' | 'write';
37
+ };
38
+ //# sourceMappingURL=policy-runtime.d.ts.map
@@ -0,0 +1,340 @@
1
+ import { AgenticPolicyEngine, createLegacyCompatibleState, } from '@claude-flow/security';
2
+ import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { userInfo } from 'node:os';
7
+ const POLICY_DIR = join('.claude-flow', 'policy');
8
+ const POLICY_FILE = 'state.json';
9
+ const LOCK_FILE = 'state.lock';
10
+ const LOCK_STALE_MS = 30_000;
11
+ const LOCK_WAIT_MS = 5_000;
12
+ function paths(projectRoot) {
13
+ const root = resolve(projectRoot);
14
+ const dir = join(root, POLICY_DIR);
15
+ return { dir, state: join(dir, POLICY_FILE), lock: join(dir, LOCK_FILE) };
16
+ }
17
+ function sleep(ms) {
18
+ return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
19
+ }
20
+ async function acquireLock(lockPath) {
21
+ const started = Date.now();
22
+ while (Date.now() - started < LOCK_WAIT_MS) {
23
+ try {
24
+ const fd = openSync(lockPath, 'wx', 0o600);
25
+ writeFileSync(fd, JSON.stringify({ pid: process.pid, acquiredAt: Date.now() }));
26
+ closeSync(fd);
27
+ return () => {
28
+ try {
29
+ unlinkSync(lockPath);
30
+ }
31
+ catch { /* already released */ }
32
+ };
33
+ }
34
+ catch {
35
+ try {
36
+ if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS)
37
+ unlinkSync(lockPath);
38
+ }
39
+ catch { /* another process changed the lock */ }
40
+ await sleep(10);
41
+ }
42
+ }
43
+ throw new Error('policy-state-lock-timeout');
44
+ }
45
+ function writeJsonAtomic(file, value) {
46
+ mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
47
+ const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
48
+ writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
49
+ renameSync(temporary, file);
50
+ }
51
+ function trustPaths(projectRoot) {
52
+ const trustRoot = join(userInfo().homedir, '.config', 'ruflo', 'policy-trust');
53
+ const projectId = createHash('sha256').update(realpathSync(projectRoot)).digest('hex');
54
+ const dir = join(trustRoot, projectId);
55
+ return { key: join(dir, 'anchor.key'), anchor: join(dir, 'state.anchor.json') };
56
+ }
57
+ function trustKey(projectRoot, create) {
58
+ const { key } = trustPaths(projectRoot);
59
+ if (!existsSync(key)) {
60
+ if (!create)
61
+ return undefined;
62
+ mkdirSync(dirname(key), { recursive: true, mode: 0o700 });
63
+ writeFileSync(key, randomBytes(32), { mode: 0o600, flag: 'wx' });
64
+ }
65
+ const material = readFileSync(key);
66
+ if (material.length !== 32)
67
+ throw new Error('invalid-policy-trust-key');
68
+ return material;
69
+ }
70
+ function stateAuthentication(state, key) {
71
+ return createHmac('sha256', key).update(JSON.stringify(state)).digest('hex');
72
+ }
73
+ function verifyStateAnchor(projectRoot, state) {
74
+ const { anchor } = trustPaths(projectRoot);
75
+ if (!existsSync(anchor))
76
+ return;
77
+ if (!state)
78
+ throw new Error('policy-state-missing-for-anchored-project');
79
+ const key = trustKey(projectRoot, false);
80
+ if (!key)
81
+ throw new Error('policy-trust-key-missing');
82
+ const record = JSON.parse(readFileSync(anchor, 'utf8'));
83
+ const expected = stateAuthentication(state, key);
84
+ const actual = record.authentication ?? '';
85
+ if (!/^[a-f0-9]{64}$/.test(actual)
86
+ || !timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(actual, 'hex'))) {
87
+ throw new Error('policy-state-authentication-failed');
88
+ }
89
+ }
90
+ function writePolicyState(projectRoot, statePath, state) {
91
+ const anchorPath = trustPaths(projectRoot).anchor;
92
+ if (state.mode === 'enforce' || existsSync(anchorPath)) {
93
+ const key = trustKey(projectRoot, true);
94
+ const anchor = {
95
+ version: 1,
96
+ projectRoot: realpathSync(projectRoot),
97
+ mode: state.mode,
98
+ authentication: stateAuthentication(state, key),
99
+ updatedAt: Date.now(),
100
+ };
101
+ // On first enforcement, establish the external trust record first. A
102
+ // crash then leaves either a valid pair or an anchored mismatch that
103
+ // fails closed; it can never leave enforce state silently unanchored.
104
+ if (!existsSync(anchorPath)) {
105
+ writeJsonAtomic(anchorPath, anchor);
106
+ writeJsonAtomic(statePath, state);
107
+ return;
108
+ }
109
+ writeJsonAtomic(statePath, state);
110
+ writeJsonAtomic(anchorPath, anchor);
111
+ return;
112
+ }
113
+ writeJsonAtomic(statePath, state);
114
+ }
115
+ function detectLegacyCapabilities(projectRoot) {
116
+ const candidates = [
117
+ '.swarm/memory.db',
118
+ '.claude-flow/memory.db',
119
+ '.claude-flow/data/memory.db',
120
+ 'agentdb.rvf',
121
+ 'agentdb-memory.db',
122
+ ];
123
+ const found = candidates.filter((candidate) => existsSync(join(projectRoot, candidate)));
124
+ const flags = [
125
+ process.env.CLAUDE_FLOW_STRICT_AUTH === 'true' ? 'strict-auth' : null,
126
+ process.env.CLAUDE_FLOW_STRICT_MEMORY === 'true' ? 'strict-memory' : null,
127
+ ].filter(Boolean);
128
+ return `pre-ADR-324; capabilities=${[...found, ...flags].join(',') || 'none-detected'}`;
129
+ }
130
+ function configuredPolicyMode(projectRoot) {
131
+ let configured;
132
+ for (const relative of ['.agents/config.toml', '.codex/config.toml']) {
133
+ const file = join(resolve(projectRoot), relative);
134
+ if (!existsSync(file))
135
+ continue;
136
+ const content = readFileSync(file, 'utf8');
137
+ const section = content.match(/(?:^|\n)\[policy\]\s*\n([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[1];
138
+ const mode = section?.match(/(?:^|\n)\s*mode\s*=\s*"(legacy|observe|enforce)"/)?.[1];
139
+ if (mode)
140
+ configured = mode;
141
+ }
142
+ return configured;
143
+ }
144
+ export function loadPolicyState(projectRoot = process.cwd()) {
145
+ const target = paths(projectRoot);
146
+ if (!existsSync(target.state)) {
147
+ verifyStateAnchor(projectRoot, undefined);
148
+ return createLegacyCompatibleState(detectLegacyCapabilities(projectRoot));
149
+ }
150
+ const parsed = JSON.parse(readFileSync(target.state, 'utf8'));
151
+ if (parsed.version !== 1 || !Array.isArray(parsed.rules) || !Array.isArray(parsed.receipts)) {
152
+ throw new Error(`unsupported-policy-state-version:${String(parsed.version)}`);
153
+ }
154
+ verifyStateAnchor(projectRoot, parsed);
155
+ return parsed;
156
+ }
157
+ export async function autoMigratePolicyStateIfNeeded(projectRoot = process.cwd()) {
158
+ const target = paths(projectRoot);
159
+ if (existsSync(target.state)) {
160
+ const configured = configuredPolicyMode(projectRoot);
161
+ const current = loadPolicyState(projectRoot);
162
+ if (configured && current.configuredMode !== configured) {
163
+ await withPolicyTransaction(projectRoot, (engine) => engine.setConfiguredMode(configured));
164
+ }
165
+ return { migrated: false, statePath: target.state, mode: loadPolicyState(projectRoot).mode };
166
+ }
167
+ // Only upgrade existing Ruflo installations. A random directory should not
168
+ // acquire policy state merely because `ruflo --version` ran there.
169
+ if (!existsSync(join(resolve(projectRoot), '.claude-flow'))
170
+ && !existsSync(join(resolve(projectRoot), '.swarm')))
171
+ return { migrated: false };
172
+ mkdirSync(target.dir, { recursive: true, mode: 0o700 });
173
+ const release = await acquireLock(target.lock);
174
+ try {
175
+ if (!existsSync(target.state)) {
176
+ const state = createLegacyCompatibleState(detectLegacyCapabilities(projectRoot));
177
+ const configured = configuredPolicyMode(projectRoot);
178
+ if (configured) {
179
+ state.mode = configured;
180
+ state.configuredMode = configured;
181
+ }
182
+ writePolicyState(projectRoot, target.state, state);
183
+ }
184
+ }
185
+ finally {
186
+ release();
187
+ }
188
+ return { migrated: true, statePath: target.state, mode: loadPolicyState(projectRoot).mode };
189
+ }
190
+ export async function withPolicyTransaction(projectRoot, operation, options = {}) {
191
+ const target = paths(projectRoot);
192
+ mkdirSync(target.dir, { recursive: true, mode: 0o700 });
193
+ const release = await acquireLock(target.lock);
194
+ try {
195
+ const engine = AgenticPolicyEngine.fromState(loadPolicyState(projectRoot), {
196
+ signingKey: process.env.CLAUDE_FLOW_POLICY_SIGNING_KEY,
197
+ keyId: process.env.CLAUDE_FLOW_POLICY_KEY_ID,
198
+ evidenceVerifier: verifyPolicyEvidence,
199
+ approvalIssuerVerifier: options.approvalIssuerVerifier,
200
+ });
201
+ const result = await operation(engine);
202
+ const nextState = engine.exportState();
203
+ if (!engine.verifyLedger().valid)
204
+ throw new Error('policy-ledger-verification-failed');
205
+ writePolicyState(projectRoot, target.state, nextState);
206
+ return result;
207
+ }
208
+ finally {
209
+ release();
210
+ }
211
+ }
212
+ function verifyPolicyEvidence(evidence) {
213
+ if (!evidence.keyId || !evidence.contentHash || !evidence.signature)
214
+ return false;
215
+ let keys;
216
+ try {
217
+ keys = JSON.parse(process.env.CLAUDE_FLOW_POLICY_EVIDENCE_KEYS ?? '{}');
218
+ }
219
+ catch {
220
+ return false;
221
+ }
222
+ const key = keys[evidence.keyId];
223
+ if (!key || key.length < 16 || !/^sha256:[a-f0-9]{64}$/i.test(evidence.contentHash))
224
+ return false;
225
+ const signedClaims = JSON.stringify({
226
+ id: evidence.id,
227
+ provenance: evidence.provenance,
228
+ attestor: evidence.attestor,
229
+ observedAt: evidence.observedAt,
230
+ contentHash: evidence.contentHash,
231
+ keyId: evidence.keyId,
232
+ });
233
+ const expected = createHmac('sha256', key).update(signedClaims).digest('hex');
234
+ const provided = evidence.signature.replace(/^hmac-sha256:/, '');
235
+ if (!/^[a-f0-9]{64}$/i.test(provided))
236
+ return false;
237
+ return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(provided, 'hex'));
238
+ }
239
+ export async function evaluatePolicyRequest(request, projectRoot = process.cwd()) {
240
+ return withPolicyTransaction(projectRoot, (engine) => engine.evaluate(request));
241
+ }
242
+ export async function setPolicyMode(mode, projectRoot = process.cwd()) {
243
+ return withPolicyTransaction(projectRoot, (engine) => engine.setMode(mode));
244
+ }
245
+ export async function upsertPolicyRule(rule, projectRoot = process.cwd()) {
246
+ return withPolicyTransaction(projectRoot, (engine) => engine.upsertRule(rule));
247
+ }
248
+ export async function setPolicyBudget(limit, projectRoot = process.cwd()) {
249
+ return withPolicyTransaction(projectRoot, (engine) => engine.setBudget(limit));
250
+ }
251
+ export async function issuePolicyApproval(approval, projectRoot = process.cwd(), approvalIssuerVerifier) {
252
+ return withPolicyTransaction(projectRoot, (engine) => engine.issueApproval(approval), { approvalIssuerVerifier });
253
+ }
254
+ export async function revokePolicyApproval(id, projectRoot = process.cwd()) {
255
+ return withPolicyTransaction(projectRoot, (engine) => engine.revokeApproval(id));
256
+ }
257
+ export async function verifyPolicyLedger(projectRoot = process.cwd()) {
258
+ return withPolicyTransaction(projectRoot, (engine) => engine.verifyLedger());
259
+ }
260
+ export async function authorizeMcpTool(toolName, input, context = {}, attributes = {}) {
261
+ let projectRoot = typeof context.projectRoot === 'string' ? context.projectRoot : process.cwd();
262
+ let processEnvelope;
263
+ if (process.env.CLAUDE_FLOW_CAPABILITY_ENVELOPE) {
264
+ try {
265
+ const parsed = JSON.parse(process.env.CLAUDE_FLOW_CAPABILITY_ENVELOPE);
266
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
267
+ throw new Error('not an object');
268
+ }
269
+ processEnvelope = parsed;
270
+ }
271
+ catch {
272
+ throw new Error('invalid-worker-capability-envelope');
273
+ }
274
+ // Linked git worktrees share one immutable common git directory. Derive
275
+ // the coordinator checkout from that directory so a worker cannot fall
276
+ // back to independent legacy policy state in its isolated worktree.
277
+ try {
278
+ const cwd = realpathSync(process.cwd());
279
+ const common = execFileSync('git', ['-C', cwd, 'rev-parse', '--path-format=absolute', '--git-common-dir'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
280
+ projectRoot = dirname(realpathSync(common));
281
+ }
282
+ catch {
283
+ throw new Error('authoritative-worker-policy-root-unavailable');
284
+ }
285
+ }
286
+ return evaluatePolicyRequest({
287
+ identity: {
288
+ id: process.env.CLAUDE_FLOW_PRINCIPAL_ID ?? 'legacy-cli',
289
+ type: process.env.CLAUDE_FLOW_PRINCIPAL_ID ? 'agent' : 'legacy',
290
+ },
291
+ action: {
292
+ type: attributes.actionType ?? 'mcp.tool.call',
293
+ resource: toolName,
294
+ tool: toolName,
295
+ server: typeof context.serverId === 'string' ? context.serverId : 'ruflo',
296
+ namespace: typeof input.namespace === 'string' ? input.namespace : undefined,
297
+ environment: typeof context.environment === 'string' ? context.environment : undefined,
298
+ costUsd: attributes.costUsd,
299
+ tokens: attributes.tokens,
300
+ concurrency: attributes.concurrency,
301
+ network: attributes.network === true,
302
+ destructive: attributes.destructive === true,
303
+ },
304
+ context: {
305
+ envelope: attributes.envelope ?? processEnvelope,
306
+ approvalIds: Array.isArray(context.approvalIds) ? context.approvalIds.map(String) : undefined,
307
+ evidence: Array.isArray(context.evidence) ? context.evidence : undefined,
308
+ metadata: {
309
+ inputDigest: `sha256:${createHash('sha256').update(JSON.stringify(input)).digest('hex')}`,
310
+ },
311
+ },
312
+ }, projectRoot);
313
+ }
314
+ /** Trusted classification derived from the registered tool name, never input. */
315
+ export function classifyMcpTool(toolName) {
316
+ const normalized = toolName.toLowerCase();
317
+ const policyAdmin = normalized.startsWith('policy_')
318
+ && !['policy_evaluate', 'policy_status'].includes(normalized);
319
+ const memoryRead = /^(?:memory|agentdb)_(?:pattern-)?(?:search|query|get|retrieve|list|status|stats)/.test(normalized);
320
+ const memoryWrite = /^(?:memory|agentdb)_(?:pattern-)?(?:store|insert|update|delete|clear|purge|init)/.test(normalized);
321
+ const terminal = /^(?:terminal_execute|bash|shell|exec)/.test(normalized);
322
+ const destructive = policyAdmin
323
+ || terminal
324
+ || /(delete|remove|clear|purge|revoke|promote|deploy|integrate|cleanup|terminate|stop)/.test(normalized);
325
+ const network = terminal
326
+ || /(github|browser|web_|http_|fetch|managed_agent|federation|ipfs|openrouter|provider)/.test(normalized);
327
+ return {
328
+ actionType: policyAdmin
329
+ ? `policy.admin.${normalized.slice('policy_'.length)}`
330
+ : memoryRead
331
+ ? 'memory.read'
332
+ : memoryWrite
333
+ ? 'memory.write'
334
+ : 'mcp.tool.call',
335
+ network,
336
+ destructive,
337
+ namespaceAccess: memoryRead ? 'read' : memoryWrite ? 'write' : undefined,
338
+ };
339
+ }
340
+ //# sourceMappingURL=policy-runtime.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.32.26",
3
+ "version": "3.32.29",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",
@@ -105,24 +105,40 @@
105
105
  "@claude-flow/mcp": "3.0.0-alpha.8",
106
106
  "@claude-flow/neural": "3.0.0-alpha.9",
107
107
  "@claude-flow/shared": "3.0.0-alpha.7",
108
- "@metaharness/router": "^0.3.2",
109
108
  "@noble/ed25519": "2.3.0",
110
109
  "@ruvector/rabitq-wasm": "0.1.0",
111
- "metaharness": "^0.4.1",
112
110
  "semver": "7.7.3",
113
111
  "sql.js": "^1.13.0",
114
112
  "yaml": "^2.8.0"
115
113
  },
116
114
  "optionalDependencies": {
117
115
  "@claude-flow/memory": "^3.0.0-alpha.21",
118
- "@claude-flow/security": "^3.0.0-alpha.12",
119
- "@metaharness/darwin": "^0.8.0",
120
- "@metaharness/flywheel": "^0.1.7",
116
+ "@claude-flow/security": "^3.0.0-alpha.14",
121
117
  "agentdb": "^3.0.0-alpha.17",
122
118
  "agentic-flow": "^3.0.0-alpha.1",
123
119
  "better-sqlite3": "^12.9.0",
124
120
  "ruvector": "^0.2.27"
125
121
  },
122
+ "peerDependencies": {
123
+ "@metaharness/darwin": "^0.8.0",
124
+ "@metaharness/flywheel": "^0.1.7",
125
+ "@metaharness/router": "^0.3.2",
126
+ "metaharness": "^0.4.1"
127
+ },
128
+ "peerDependenciesMeta": {
129
+ "@metaharness/darwin": {
130
+ "optional": true
131
+ },
132
+ "@metaharness/flywheel": {
133
+ "optional": true
134
+ },
135
+ "@metaharness/router": {
136
+ "optional": true
137
+ },
138
+ "metaharness": {
139
+ "optional": true
140
+ }
141
+ },
126
142
  "publishConfig": {
127
143
  "access": "public",
128
144
  "tag": "latest"
@@ -21,6 +21,7 @@ TOOLS_SRC="$ROOT/../../v3/@claude-flow/cli/src/mcp-tools/metaharness-tools.ts"
21
21
  SUBS_SRC="$ROOT/../../v3/@claude-flow/cli/src/commands/metaharness.ts"
22
22
  EXPECTED_TOOLS=$(grep -cE "name: 'metaharness_" "$TOOLS_SRC" 2>/dev/null; true)
23
23
  EXPECTED_SUBS=$(grep -cE "^[[:space:]]+'?[a-z-]+'?:[[:space:]]*'[a-z-]+\.mjs'" "$SUBS_SRC" 2>/dev/null; true)
24
+ EXPECTED_IN_PROCESS_TOOLS=1 # ADR-322 metaharness_flywheel
24
25
  : "${EXPECTED_TOOLS:=0}"
25
26
  : "${EXPECTED_SUBS:=0}"
26
27
 
@@ -883,11 +884,17 @@ for f in $REFS; do
883
884
  COUNT=$((COUNT + 1))
884
885
  [[ -f "$SCRIPTS_DIR/$f" ]] || miss="$miss mcp-script-${f}-missing"
885
886
  done
886
- # One unique runScript() ref per MCP tool (mint deliberately excluded).
887
+ # One unique runScript() ref per subprocess-backed MCP tool (mint deliberately
888
+ # excluded). ADR-322's metaharness_flywheel is intentionally in-process so
889
+ # evaluation cancellation and atomic promotion share the CLI transaction code.
887
890
  # Cross-aspect check: runScript('*.mjs') call sites vs the derived
888
- # `name: 'metaharness_*'` declaration count in the same file — a handler
889
- # added without a script ref (or vice-versa) diverges these.
890
- [[ "$COUNT" == "$EXPECTED_TOOLS" ]] || miss="$miss mcp-script-count-stale:$COUNT-expected-$EXPECTED_TOOLS"
891
+ # `name: 'metaharness_*'` declaration count minus the explicitly governed
892
+ # in-process surface.
893
+ EXPECTED_SCRIPT_TOOLS=$((EXPECTED_TOOLS - EXPECTED_IN_PROCESS_TOOLS))
894
+ [[ "$COUNT" == "$EXPECTED_SCRIPT_TOOLS" ]] || miss="$miss mcp-script-count-stale:$COUNT-expected-$EXPECTED_SCRIPT_TOOLS"
895
+ grep -q "name: 'metaharness_flywheel'" "$WRAPPER" 2>/dev/null || miss="$miss no-in-process-flywheel"
896
+ grep -q "runFlywheelWorker" "$WRAPPER" 2>/dev/null || miss="$miss no-flywheel-evaluator"
897
+ grep -q "promoteFlywheelCandidate" "$WRAPPER" 2>/dev/null || miss="$miss no-flywheel-promoter"
891
898
  [[ -z "$miss" ]] && ok || bad "$miss"
892
899
 
893
900
  step "17z52. SUBCOMMANDS map entries point at existing script files (iter 89)"
@@ -1696,11 +1703,11 @@ CODE=$?
1696
1703
  step "17z9. MCP success-semantic footnote + audit_trend file inputs (iter 46)"
1697
1704
  miss=""
1698
1705
  WRAPPER="$ROOT/../../v3/@claude-flow/cli/src/mcp-tools/metaharness-tools.ts"
1699
- # Success-semantic constant declared + appended to N tool descriptions =
1700
- # N+1 occurrences. N is the derived tool count cross-aspect check within
1701
- # the wrapper: footnote appends vs `name:` declarations.
1706
+ # Success-semantic constant declared + appended to every subprocess-backed
1707
+ # tool description. The in-process flywheel has its own transaction semantics,
1708
+ # so total occurrences equal the total tool count (1 constant + N-1 tools).
1702
1709
  COUNT=$(grep -c "MCP_SUCCESS_SEMANTIC" "$WRAPPER" 2>/dev/null; true)
1703
- [[ "$COUNT" == "$((EXPECTED_TOOLS + 1))" ]] || miss="$miss footnote-count:$COUNT-expected-$((EXPECTED_TOOLS + 1))"
1710
+ [[ "$COUNT" == "$EXPECTED_TOOLS" ]] || miss="$miss footnote-count:$COUNT-expected-$EXPECTED_TOOLS"
1704
1711
  # audit_trend now exposes baselineFile / currentFile
1705
1712
  grep -q "baselineFile" "$WRAPPER" 2>/dev/null || miss="$miss no-baseline-file"
1706
1713
  grep -q "currentFile" "$WRAPPER" 2>/dev/null || miss="$miss no-current-file"
@@ -1738,9 +1745,10 @@ grep -q "success = exitCode === 0" "$WRAPPER" 2>/dev/null || miss="$miss no-exit
1738
1745
  COUNT_OLD=$(grep -c "success: !r.degraded" "$WRAPPER" 2>/dev/null; true)
1739
1746
  [[ "$COUNT_OLD" == "0" ]] || miss="$miss old-pattern-still-present:$COUNT_OLD"
1740
1747
  COUNT_NEW=$(grep -c "success: r.success" "$WRAPPER" 2>/dev/null; true)
1741
- # One `success: r.success` per handler = the derived tool count. Cross-aspect
1742
- # check within the wrapper: handler success-wiring vs `name:` declarations.
1743
- [[ "$COUNT_NEW" == "$EXPECTED_TOOLS" ]] || miss="$miss new-pattern-count:$COUNT_NEW-expected-$EXPECTED_TOOLS"
1748
+ # One `success: r.success` per subprocess-backed handler. The in-process
1749
+ # flywheel derives success from evaluation/promotion transaction results.
1750
+ EXPECTED_SCRIPT_TOOLS=$((EXPECTED_TOOLS - EXPECTED_IN_PROCESS_TOOLS))
1751
+ [[ "$COUNT_NEW" == "$EXPECTED_SCRIPT_TOOLS" ]] || miss="$miss new-pattern-count:$COUNT_NEW-expected-$EXPECTED_SCRIPT_TOOLS"
1744
1752
  # Runtime anchors: iter 44 success assertions present
1745
1753
  T="$ROOT/scripts/test-mcp-tools.mjs"
1746
1754
  grep -q "iter 44 fix" "$T" 2>/dev/null || miss="$miss no-iter44-anchors"
@@ -2099,10 +2107,10 @@ grep -q "result has 'success'" "$F" || miss="$miss no-success-assertion"
2099
2107
  grep -q "result has 'data'" "$F" || miss="$miss no-data-assertion"
2100
2108
  grep -q "result has 'degraded'" "$F" || miss="$miss no-degraded-assertion"
2101
2109
  grep -q "result has 'exitCode'" "$F" || miss="$miss no-exitcode-assertion"
2102
- # All 15 tool names enumerated (similarity iter 36, drift_from_history iter 54,
2110
+ # All tool names enumerated (similarity iter 36, drift_from_history iter 54,
2103
2111
  # ADR-153 added bench/evolve/security_bench, @metaharness/redblue added redblue,
2104
- # metaharness@0.3.0/darwin@0.8.0 added learn + gepa)
2105
- for tool in metaharness_score metaharness_genome metaharness_mcp_scan metaharness_threat_model metaharness_oia_audit metaharness_audit_list metaharness_audit_trend metaharness_similarity metaharness_drift_from_history metaharness_bench metaharness_evolve metaharness_security_bench metaharness_redblue metaharness_learn metaharness_gepa; do
2112
+ # metaharness@0.3.0/darwin@0.8.0 added learn + gepa, ADR-322 added flywheel)
2113
+ for tool in metaharness_score metaharness_genome metaharness_mcp_scan metaharness_threat_model metaharness_oia_audit metaharness_audit_list metaharness_audit_trend metaharness_similarity metaharness_drift_from_history metaharness_bench metaharness_evolve metaharness_security_bench metaharness_redblue metaharness_learn metaharness_gepa metaharness_flywheel; do
2106
2114
  grep -q "${tool}" "$F" || miss="$miss missing-${tool}"
2107
2115
  done
2108
2116
  # test-mcp-tools.mjs keeps its own hardcoded `tools.length === N` literal —
@@ -74,7 +74,7 @@ async function main() {
74
74
  // ──────────────────────────────────────────────────────────────────
75
75
  console.log('Phase 1 — module shape');
76
76
  assert(Array.isArray(tools), 'metaharnessTools is an array');
77
- assert(tools.length === 15, `15 tools registered (got ${tools.length})`);
77
+ assert(tools.length === 16, `16 tools registered (got ${tools.length})`);
78
78
 
79
79
  const expectedNames = new Set([
80
80
  'metaharness_score',
@@ -98,6 +98,8 @@ async function main() {
98
98
  'metaharness_learn',
99
99
  // @metaharness/darwin@0.8.0 — GEPA library surface (genome ops)
100
100
  'metaharness_gepa',
101
+ // ADR-322 — in-process governed evaluation and atomic promotion
102
+ 'metaharness_flywheel',
101
103
  ]);
102
104
  const actualNames = new Set(tools.map((t) => t.name));
103
105
  for (const name of expectedNames) {