@claude-flow/cli 3.32.25 → 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 (48) hide show
  1. package/.claude/helpers/.helpers-version +1 -1
  2. package/.claude/helpers/auto-memory-hook.mjs +430 -430
  3. package/.claude/helpers/helpers.manifest.json +6 -6
  4. package/.claude/helpers/hook-handler.cjs +565 -565
  5. package/.claude/helpers/intelligence.cjs +1058 -1058
  6. package/.claude/helpers/statusline.cjs +1060 -1060
  7. package/catalog-manifest.json +4 -4
  8. package/dist/src/commands/index.d.ts +1 -0
  9. package/dist/src/commands/index.js +5 -3
  10. package/dist/src/commands/memory.js +49 -5
  11. package/dist/src/commands/metaharness.js +100 -2
  12. package/dist/src/commands/policy.d.ts +4 -0
  13. package/dist/src/commands/policy.js +107 -0
  14. package/dist/src/index.js +18 -0
  15. package/dist/src/mcp-client.js +25 -1
  16. package/dist/src/mcp-tools/capability-brain.d.ts +134 -0
  17. package/dist/src/mcp-tools/capability-brain.js +697 -0
  18. package/dist/src/mcp-tools/guidance-tools.d.ts +2 -0
  19. package/dist/src/mcp-tools/guidance-tools.js +369 -37
  20. package/dist/src/mcp-tools/index.d.ts +4 -1
  21. package/dist/src/mcp-tools/index.js +3 -1
  22. package/dist/src/mcp-tools/memory-tools.js +26 -0
  23. package/dist/src/mcp-tools/metaharness-tools.js +106 -1
  24. package/dist/src/mcp-tools/policy-tools.d.ts +3 -0
  25. package/dist/src/mcp-tools/policy-tools.js +121 -0
  26. package/dist/src/memory/memory-bridge.d.ts +11 -0
  27. package/dist/src/memory/memory-bridge.js +100 -21
  28. package/dist/src/memory/memory-initializer.d.ts +22 -1
  29. package/dist/src/memory/memory-initializer.js +184 -39
  30. package/dist/src/services/bounded-worker-pool.d.ts +28 -0
  31. package/dist/src/services/bounded-worker-pool.js +90 -0
  32. package/dist/src/services/flywheel-proposer.d.ts +87 -0
  33. package/dist/src/services/flywheel-proposer.js +165 -0
  34. package/dist/src/services/flywheel-receipt.d.ts +136 -0
  35. package/dist/src/services/flywheel-receipt.js +309 -0
  36. package/dist/src/services/flywheel-transaction.d.ts +77 -0
  37. package/dist/src/services/flywheel-transaction.js +378 -0
  38. package/dist/src/services/harness-flywheel-runtime.d.ts +11 -0
  39. package/dist/src/services/harness-flywheel-runtime.js +85 -2
  40. package/dist/src/services/harness-flywheel.d.ts +27 -1
  41. package/dist/src/services/harness-flywheel.js +138 -27
  42. package/dist/src/services/policy-runtime.d.ts +38 -0
  43. package/dist/src/services/policy-runtime.js +340 -0
  44. package/package.json +23 -5
  45. package/plugins/ruflo-metaharness/scripts/smoke.sh +22 -14
  46. package/plugins/ruflo-metaharness/scripts/test-mcp-tools.mjs +3 -1
  47. package/.claude/.proven-config-version +0 -1
  48. package/.claude/proven-config.json +0 -42
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generation": 1,
4
- "generatedAt": "2026-07-28T15:08:58.657Z",
5
- "gitSha": "978851fd",
3
+ "generation": 3,
4
+ "generatedAt": "2026-07-29T04:47:25.187Z",
5
+ "gitSha": "39bc5fd9",
6
6
  "catalog": {
7
7
  "agents": 164,
8
- "tools": 387,
8
+ "tools": 395,
9
9
  "skills": 34
10
10
  },
11
11
  "benchmark": null
@@ -33,6 +33,7 @@ export declare function getProvidersCommand(): Promise<Command | undefined>;
33
33
  export declare function getPluginsCommand(): Promise<Command | undefined>;
34
34
  export declare function getDeploymentCommand(): Promise<Command | undefined>;
35
35
  export declare function getClaimsCommand(): Promise<Command | undefined>;
36
+ export declare function getPolicyCommand(): Promise<Command | undefined>;
36
37
  export declare function getEmbeddingsCommand(): Promise<Command | undefined>;
37
38
  export declare function getCompletionsCommand(): Promise<Command | undefined>;
38
39
  export declare function getAnalyzeCommand(): Promise<Command | undefined>;
@@ -40,6 +40,7 @@ const commandLoaders = {
40
40
  plugins: () => import('./plugins.js'),
41
41
  deployment: () => import('./deployment.js'),
42
42
  claims: () => import('./claims.js'),
43
+ policy: () => import('./policy.js'),
43
44
  embeddings: () => import('./embeddings.js'),
44
45
  // P0 Commands
45
46
  completions: () => import('./completions.js'),
@@ -175,6 +176,7 @@ export async function getProvidersCommand() { return loadCommand('providers'); }
175
176
  export async function getPluginsCommand() { return loadCommand('plugins'); }
176
177
  export async function getDeploymentCommand() { return loadCommand('deployment'); }
177
178
  export async function getClaimsCommand() { return loadCommand('claims'); }
179
+ export async function getPolicyCommand() { return loadCommand('policy'); }
178
180
  export async function getEmbeddingsCommand() { return loadCommand('embeddings'); }
179
181
  export async function getCompletionsCommand() { return loadCommand('completions'); }
180
182
  export async function getAnalyzeCommand() { return loadCommand('analyze'); }
@@ -230,14 +232,14 @@ export const commandsByCategory = {
230
232
  * Use this for help display and full command listings.
231
233
  */
232
234
  export async function getCommandsByCategory() {
233
- const [daemonCmd, doctorCmd, embeddingsCmd, neuralCmd, performanceCmd, securityCmd, ruvectorCmd, hiveMindCmd, configCmd, completionsCmd, migrateCmd, workflowCmd, analyzeCmd, routeCmd, progressCmd, providersCmd, pluginsCmd, deploymentCmd, claimsCmd, issuesCmd, updateCmd, processCmd, guidanceCmd, applianceCmd, cleanupCmd, autopilotCmd,] = await Promise.all([
235
+ const [daemonCmd, doctorCmd, embeddingsCmd, neuralCmd, performanceCmd, securityCmd, ruvectorCmd, hiveMindCmd, configCmd, completionsCmd, migrateCmd, workflowCmd, analyzeCmd, routeCmd, progressCmd, providersCmd, pluginsCmd, deploymentCmd, claimsCmd, issuesCmd, updateCmd, processCmd, guidanceCmd, applianceCmd, cleanupCmd, autopilotCmd, policyCmd,] = await Promise.all([
234
236
  loadCommand('daemon'), loadCommand('doctor'), loadCommand('embeddings'), loadCommand('neural'),
235
237
  loadCommand('performance'), loadCommand('security'), loadCommand('ruvector'), loadCommand('hive-mind'),
236
238
  loadCommand('config'), loadCommand('completions'), loadCommand('migrate'), loadCommand('workflow'),
237
239
  loadCommand('analyze'), loadCommand('route'), loadCommand('progress'), loadCommand('providers'),
238
240
  loadCommand('plugins'), loadCommand('deployment'), loadCommand('claims'), loadCommand('issues'),
239
241
  loadCommand('update'), loadCommand('process'), loadCommand('guidance'), loadCommand('appliance'),
240
- loadCommand('cleanup'), loadCommand('autopilot'),
242
+ loadCommand('cleanup'), loadCommand('autopilot'), loadCommand('policy'),
241
243
  ]);
242
244
  return {
243
245
  primary: [
@@ -246,7 +248,7 @@ export async function getCommandsByCategory() {
246
248
  mcpCommand, hooksCommand,
247
249
  ],
248
250
  advanced: [
249
- neuralCmd, securityCmd, performanceCmd, embeddingsCmd,
251
+ neuralCmd, securityCmd, policyCmd, performanceCmd, embeddingsCmd,
250
252
  hiveMindCmd, ruvectorCmd, guidanceCmd, autopilotCmd,
251
253
  ].filter(Boolean),
252
254
  utility: [
@@ -86,12 +86,23 @@ const storeCommand = {
86
86
  description: 'Scan the value for injection payloads before persisting (dream-cycle #2752 MemPoison gate)',
87
87
  type: 'boolean',
88
88
  },
89
+ {
90
+ // ADR-323 — typed memory provenance. Distinguishes who/what produced
91
+ // this entry so shared-namespace retrieval can filter by trust level
92
+ // instead of conflating a user's stated claim with an agent's own
93
+ // output or a raw tool/system observation.
94
+ name: 'provenance',
95
+ description: 'Who/what produced this value: user_claim | agent_output | system_observation | tool_result | unknown (default: unknown) — ADR-323',
96
+ type: 'string',
97
+ choices: ['user_claim', 'agent_output', 'system_observation', 'tool_result', 'unknown'],
98
+ },
89
99
  DB_PATH_OPTION
90
100
  ],
91
101
  examples: [
92
102
  { command: 'claude-flow memory store -k "api/auth" -v "JWT implementation"', description: 'Store text' },
93
103
  { command: 'claude-flow memory store -k "pattern/singleton" --vector', description: 'Store vector' },
94
- { command: 'claude-flow memory store -k "pattern" -v "new" --no-upsert', description: 'Strict insert (fail if key exists)' }
104
+ { command: 'claude-flow memory store -k "pattern" -v "new" --no-upsert', description: 'Strict insert (fail if key exists)' },
105
+ { command: 'claude-flow memory store -k "user/goal" -v "ship by Friday" --provenance user_claim', description: 'Store with typed provenance (ADR-323)' }
95
106
  ],
96
107
  action: async (ctx) => {
97
108
  const key = ctx.flags.key;
@@ -114,6 +125,10 @@ const storeCommand = {
114
125
  // regardless of the parser gap: only an explicit `--no-upsert` (which
115
126
  // arrives as `false`) turns it off.
116
127
  const upsert = ctx.flags.upsert !== false;
128
+ // ADR-323: leave undefined (not 'unknown') when omitted so storeEntry's
129
+ // own default applies uniformly across the CLI, MCP tool, and direct
130
+ // callers — one place decides the default, not three.
131
+ const provenance = ctx.flags.provenance;
117
132
  if (!key) {
118
133
  output.printError('Key is required. Use --key or -k');
119
134
  return { success: false, exitCode: 1 };
@@ -174,6 +189,7 @@ const storeCommand = {
174
189
  tags,
175
190
  ttl,
176
191
  upsert,
192
+ provenanceType: provenance,
177
193
  dbPath
178
194
  });
179
195
  if (!result.success) {
@@ -193,12 +209,13 @@ const storeCommand = {
193
209
  { property: 'TTL', val: ttl ? `${ttl}s` : 'None' },
194
210
  { property: 'Tags', val: tags.length > 0 ? tags.join(', ') : 'None' },
195
211
  { property: 'Vector', val: result.embedding ? `Yes (${result.embedding.dimensions}-dim)` : 'No' },
212
+ { property: 'Provenance', val: provenance || 'unknown' },
196
213
  { property: 'ID', val: result.id.substring(0, 20) }
197
214
  ]
198
215
  });
199
216
  output.writeln();
200
217
  output.printSuccess('Data stored successfully');
201
- return { success: true, data: { ...storeData, id: result.id, embedding: result.embedding } };
218
+ return { success: true, data: { ...storeData, id: result.id, embedding: result.embedding, provenanceType: provenance || 'unknown' } };
202
219
  }
203
220
  catch (error) {
204
221
  output.printError(`Failed to store: ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -361,6 +378,15 @@ const searchCommand = {
361
378
  choices: ['auto', 'mixed', 'episodic', 'semantic', 'procedural'],
362
379
  default: 'mixed',
363
380
  },
381
+ {
382
+ // ADR-323 — restrict results to entries with a matching provenance
383
+ // type (e.g. exclude user_claim when retrieving for fact-checking,
384
+ // per MemSyco-Bench's memory-induced sycophancy finding). Enforced
385
+ // across semantic, keyword, hybrid, and SmartRetrieval paths.
386
+ name: 'provenance-filter',
387
+ description: 'Comma-separated provenance types to restrict results to: user_claim,agent_output,system_observation,tool_result,unknown — ADR-323',
388
+ type: 'string',
389
+ },
364
390
  DB_PATH_OPTION
365
391
  ],
366
392
  examples: [
@@ -369,11 +395,18 @@ const searchCommand = {
369
395
  { command: 'claude-flow memory search -q "test" --build-hnsw', description: 'Build HNSW index and search' },
370
396
  { command: 'claude-flow memory search -q "auth patterns" --smart', description: 'SmartRetrieval with RRF + MMR' },
371
397
  { command: 'claude-flow memory search -q "when did we last touch auth" --intent auto', description: 'SCM-routed retrieval (dream-cycle #2760)' },
398
+ { command: 'claude-flow memory search -q "deploy plan" --provenance-filter agent_output,tool_result', description: 'Only trust agent/tool-sourced entries (ADR-323)' },
372
399
  ],
373
400
  action: async (ctx) => {
374
401
  const query = ctx.flags.query || ctx.args[0];
375
402
  let namespace = ctx.flags.namespace || 'all';
376
403
  const limit = ctx.flags.limit || 10;
404
+ // Matches the --build-hnsw / buildHnsw dual-check convention already used
405
+ // below: the parser doesn't consistently camelCase every hyphenated flag.
406
+ const provenanceFilterRaw = (ctx.flags['provenance-filter'] || ctx.flags.provenanceFilter);
407
+ const provenanceFilter = provenanceFilterRaw
408
+ ? provenanceFilterRaw.split(',').map(s => s.trim()).filter(Boolean)
409
+ : undefined;
377
410
  // #2790 fix — `||` discards an explicit `--threshold 0` (falsy) and
378
411
  // silently uses the fallback; that made `--threshold 0` return
379
412
  // FEWER results than `--threshold 0.01` (non-monotonic). Nullish
@@ -456,8 +489,13 @@ const searchCommand = {
456
489
  limit: 5000,
457
490
  includeContent: true,
458
491
  dbPath: dbPathSearch,
492
+ provenanceFilter,
459
493
  });
460
- if (list.success) {
494
+ if (!list.success) {
495
+ output.printError(list.error || 'Keyword search failed');
496
+ return { success: false, exitCode: 1 };
497
+ }
498
+ else {
461
499
  const q = query.toLowerCase();
462
500
  keywordResults = list.entries
463
501
  .filter((e) => {
@@ -473,6 +511,7 @@ const searchCommand = {
473
511
  score: inKey ? 1.0 : 0.7,
474
512
  namespace: e.namespace,
475
513
  preview: c.slice(0, 200),
514
+ provenanceType: e.provenanceType,
476
515
  };
477
516
  })
478
517
  .sort((a, b) => b.score - a.score)
@@ -526,6 +565,7 @@ const searchCommand = {
526
565
  limit: req.limit || limit * 3,
527
566
  threshold: req.threshold ?? threshold,
528
567
  dbPath: dbPathSearch,
568
+ provenanceFilter,
529
569
  });
530
570
  return {
531
571
  results: r.results.map(e => ({
@@ -534,6 +574,7 @@ const searchCommand = {
534
574
  content: e.content,
535
575
  score: e.score,
536
576
  namespace: e.namespace,
577
+ provenanceType: e.provenanceType,
537
578
  })),
538
579
  };
539
580
  };
@@ -548,6 +589,7 @@ const searchCommand = {
548
589
  score: r.score,
549
590
  namespace: r.namespace,
550
591
  preview: r.content,
592
+ provenanceType: r.provenanceType,
551
593
  }));
552
594
  searchTimeMs = smartResult.stats.durationMs;
553
595
  smartStats = smartResult.stats;
@@ -559,7 +601,8 @@ const searchCommand = {
559
601
  namespace,
560
602
  limit,
561
603
  threshold,
562
- dbPath: dbPathSearch
604
+ dbPath: dbPathSearch,
605
+ provenanceFilter
563
606
  });
564
607
  if (!searchResult.success) {
565
608
  output.printError(searchResult.error || 'Search failed');
@@ -569,7 +612,8 @@ const searchCommand = {
569
612
  key: r.key,
570
613
  score: r.score,
571
614
  namespace: r.namespace,
572
- preview: r.content
615
+ preview: r.content,
616
+ provenanceType: r.provenanceType,
573
617
  }));
574
618
  searchTimeMs = searchResult.searchTime;
575
619
  }
@@ -35,9 +35,12 @@
35
35
  */
36
36
  import { output } from '../output.js';
37
37
  import { spawnSync } from 'child_process';
38
- import { existsSync } from 'fs';
38
+ import { existsSync, readFileSync } from 'fs';
39
39
  import { join, dirname, resolve } from 'path';
40
40
  import { fileURLToPath } from 'url';
41
+ import { runFlywheelWorker } from '../services/harness-flywheel-runtime.js';
42
+ import { listFlywheelReceipts, promoteFlywheelCandidate, readFlywheelTransactionState, verifyFlywheelLedger, } from '../services/flywheel-transaction.js';
43
+ import { evaluatePolicyRequest } from '../services/policy-runtime.js';
41
44
  const __dirname = dirname(fileURLToPath(import.meta.url));
42
45
  // Subcommand → plugin script filename
43
46
  const SUBCOMMANDS = {
@@ -65,7 +68,96 @@ const SUBCOMMANDS = {
65
68
  // @metaharness/darwin@0.8.0 — GEPA library surface (genome ops; the
66
69
  // gepaOptimize loop itself stays library-only / behind evolve)
67
70
  gepa: 'gepa.mjs',
71
+ // ADR-153 Darwin evolution and stable benchmark-corpus operations.
72
+ evolve: 'evolve.mjs',
73
+ bench: 'bench.mjs',
74
+ // ADR-322 evaluation receipt + atomic promotion loop; handled in-process.
75
+ flywheel: '__internal__',
68
76
  };
77
+ function flywheelFlag(flags, name, fallback) {
78
+ const value = flags[name];
79
+ return (value === undefined ? fallback : value);
80
+ }
81
+ async function dispatchFlywheel(operation, positional, flags) {
82
+ const projectRoot = resolve(String(flywheelFlag(flags, 'projectRoot', process.cwd())));
83
+ let data;
84
+ if (!operation || operation === 'status') {
85
+ data = {
86
+ state: readFlywheelTransactionState(projectRoot),
87
+ ledger: verifyFlywheelLedger(projectRoot),
88
+ };
89
+ }
90
+ else if (operation === 'run') {
91
+ const privateKeyPath = flywheelFlag(flags, 'privateKey');
92
+ const publicKeyPath = flywheelFlag(flags, 'publicKey');
93
+ if (!!privateKeyPath !== !!publicKeyPath) {
94
+ data = { success: false, reason: '--private-key and --public-key must be supplied together' };
95
+ }
96
+ else {
97
+ data = await runFlywheelWorker(projectRoot, {
98
+ optInOverride: true,
99
+ sample: Number(flywheelFlag(flags, 'sample', 40)),
100
+ proposer: String(flywheelFlag(flags, 'proposer', 'auto')),
101
+ receiptPrivateKeyPem: privateKeyPath ? readFileSync(resolve(privateKeyPath), 'utf8') : undefined,
102
+ receiptPublicKeyPem: publicKeyPath ? readFileSync(resolve(publicKeyPath), 'utf8') : undefined,
103
+ maxConcurrency: Number(flywheelFlag(flags, 'maxConcurrency', 2)),
104
+ evaluationTimeoutMs: Number(flywheelFlag(flags, 'timeoutMs', 120_000)),
105
+ });
106
+ }
107
+ }
108
+ else if (operation === 'receipts') {
109
+ data = listFlywheelReceipts(projectRoot).map(({ receipt, state }) => ({
110
+ receiptId: receipt.payload.receiptId,
111
+ candidateId: receipt.payload.candidateId,
112
+ baselineRef: receipt.payload.baselineRef,
113
+ decision: receipt.payload.decision,
114
+ signed: !!receipt.signature,
115
+ issuedAt: receipt.payload.issuedAt,
116
+ state,
117
+ }));
118
+ }
119
+ else if (operation === 'history') {
120
+ const state = readFlywheelTransactionState(projectRoot);
121
+ data = { ledgerHead: state.ledgerHead, commits: state.commits };
122
+ }
123
+ else if (operation === 'promote') {
124
+ const receiptId = positional[0];
125
+ const publicKeyPath = flywheelFlag(flags, 'publicKey');
126
+ if (!receiptId || !publicKeyPath) {
127
+ data = { success: false, reason: 'promote requires <receipt-id> and --public-key <PEM path>' };
128
+ }
129
+ else {
130
+ const policy = await evaluatePolicyRequest({
131
+ identity: { id: process.env.CLAUDE_FLOW_PRINCIPAL_ID ?? 'metaharness-local', type: 'agent', roles: ['optimizer'] },
132
+ action: {
133
+ type: 'metaharness.candidate.promote',
134
+ resource: receiptId,
135
+ environment: 'production',
136
+ destructive: true,
137
+ },
138
+ context: {
139
+ approvalIds: String(flywheelFlag(flags, 'approvalId', '')).split(',').filter(Boolean),
140
+ },
141
+ }, projectRoot);
142
+ if (policy.enforcedOutcome !== 'allowed') {
143
+ data = { success: false, reason: `policy-${policy.enforcedOutcome}:${policy.reason}`, policyReceiptId: policy.receiptId };
144
+ }
145
+ else {
146
+ const publicKey = readFileSync(resolve(publicKeyPath), 'utf8');
147
+ data = await promoteFlywheelCandidate(projectRoot, receiptId, {
148
+ confirm: flywheelFlag(flags, 'confirm', false) === true,
149
+ trustedPublicKeys: new Set([publicKey]),
150
+ });
151
+ }
152
+ }
153
+ }
154
+ else {
155
+ data = { success: false, reason: `unknown flywheel operation: ${operation}` };
156
+ }
157
+ output.writeln(JSON.stringify(data, null, 2));
158
+ const success = !(data && typeof data === 'object' && data.success === false);
159
+ return { success, exitCode: success ? 0 : 1, data };
160
+ }
69
161
  /**
70
162
  * Walk up from the current dirname to find the ruflo repo root that
71
163
  * contains plugins/ruflo-metaharness/. Handles three install layouts:
@@ -127,7 +219,7 @@ export const metaharnessCommand = {
127
219
  // iter 73 — list reflects all dispatchable subcommands (was
128
220
  // stale at the iter-3 list of 5). Keep this synced with the
129
221
  // SUBCOMMANDS map above.
130
- description: 'One of: score | genome | mcp-scan | threat-model | oia-audit | audit-list | audit-trend | similarity | drift-from-history | mint | redblue | learn | gepa',
222
+ description: 'One of: score | genome | mcp-scan | threat-model | oia-audit | audit-list | audit-trend | similarity | drift-from-history | mint | redblue | learn | gepa | evolve | bench | flywheel',
131
223
  type: 'string',
132
224
  },
133
225
  ],
@@ -190,6 +282,9 @@ export const metaharnessCommand = {
190
282
  output.writeln(' drift-from-history iter 53 — diff current state against most recent audit (1-command drift)');
191
283
  output.writeln(' mint scaffold a custom harness (dry-run by default)');
192
284
  output.writeln(' redblue adversarial red/blue LLM testing (init|run|patch|attack|report)');
285
+ output.writeln(' evolve Darwin candidate evolution');
286
+ output.writeln(' bench create or verify a stable benchmark suite');
287
+ output.writeln(' flywheel receipt loop: run | status | receipts | history | promote');
193
288
  output.writeln('');
194
289
  output.writeln('Each subcommand accepts --format json|table and --help.');
195
290
  output.writeln('');
@@ -201,6 +296,9 @@ export const metaharnessCommand = {
201
296
  output.writeln(`Valid: ${Object.keys(SUBCOMMANDS).join(', ')}`);
202
297
  return { success: false, exitCode: 2, data: { subcommand } };
203
298
  }
299
+ if (subcommand === 'flywheel') {
300
+ return dispatchFlywheel(positionalRest[0], positionalRest.slice(1), ctxFlags);
301
+ }
204
302
  const scriptDir = locatePluginScripts(SUBCOMMANDS[subcommand]);
205
303
  if (!scriptDir) {
206
304
  output.writeln(output.warning('metaharness: plugins/ruflo-metaharness/scripts/ not found. Install ruflo with `npm i ruflo` or run from the ruflo repo.'));
@@ -0,0 +1,4 @@
1
+ import type { Command } from '../types.js';
2
+ export declare const policyCommand: Command;
3
+ export default policyCommand;
4
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1,107 @@
1
+ import { output } from '../output.js';
2
+ import { autoMigratePolicyStateIfNeeded, evaluatePolicyRequest, loadPolicyState, revokePolicyApproval, setPolicyBudget, setPolicyMode, upsertPolicyRule, verifyPolicyLedger, } from '../services/policy-runtime.js';
3
+ function argJson(value, label) {
4
+ if (!value)
5
+ throw new Error(`${label} requires a JSON argument`);
6
+ try {
7
+ return JSON.parse(value);
8
+ }
9
+ catch {
10
+ throw new Error(`${label} must be valid JSON`);
11
+ }
12
+ }
13
+ function print(data) {
14
+ output.writeln(JSON.stringify(data, null, 2));
15
+ return { success: true, exitCode: 0, data };
16
+ }
17
+ function requireInteractiveAdministrator() {
18
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
19
+ throw new Error('policy administration requires an interactive local terminal');
20
+ }
21
+ }
22
+ export const policyCommand = {
23
+ name: 'policy',
24
+ description: 'Agentic policy engine — evaluate actions, manage rules/approvals, and verify the decision ledger (ADR-324)',
25
+ options: [
26
+ { name: 'mode', type: 'string', description: 'Policy mode: legacy | observe | enforce' },
27
+ { name: 'project-root', type: 'string', description: 'Project root containing .claude-flow/policy' },
28
+ ],
29
+ async action(context) {
30
+ const args = context.args ?? [];
31
+ const flags = context.flags ?? {};
32
+ const root = String(flags.projectRoot ?? process.cwd());
33
+ const operation = args[0] ?? 'status';
34
+ try {
35
+ if (operation === 'init' || operation === 'migrate') {
36
+ if (flags.mode || args[1])
37
+ requireInteractiveAdministrator();
38
+ const migration = await autoMigratePolicyStateIfNeeded(root);
39
+ const mode = String(flags.mode ?? args[1] ?? '');
40
+ if (mode) {
41
+ if (!['legacy', 'observe', 'enforce'].includes(mode))
42
+ throw new Error('mode must be legacy, observe, or enforce');
43
+ await setPolicyMode(mode, root);
44
+ }
45
+ return print({ ...migration, state: loadPolicyState(root) });
46
+ }
47
+ if (operation === 'status') {
48
+ const state = loadPolicyState(root);
49
+ return print({
50
+ version: state.version,
51
+ mode: state.mode,
52
+ migratedFrom: state.migratedFrom,
53
+ rules: state.rules.length,
54
+ budgets: state.budgets.length,
55
+ approvals: state.approvals.length,
56
+ receipts: state.receipts.length,
57
+ ledger: await verifyPolicyLedger(root),
58
+ });
59
+ }
60
+ if (operation === 'evaluate') {
61
+ return print(await evaluatePolicyRequest(argJson(args[1], 'evaluate'), root));
62
+ }
63
+ if (operation === 'rule' && args[1] === 'add') {
64
+ requireInteractiveAdministrator();
65
+ const rule = argJson(args[2], 'rule add');
66
+ await upsertPolicyRule(rule, root);
67
+ return print({ success: true, ruleId: rule.id });
68
+ }
69
+ if (operation === 'budget' && args[1] === 'set') {
70
+ requireInteractiveAdministrator();
71
+ const budget = argJson(args[2], 'budget set');
72
+ await setPolicyBudget(budget, root);
73
+ return print({ success: true, budgetId: budget.id });
74
+ }
75
+ if (operation === 'approve') {
76
+ throw new Error('approval issuance requires an authenticated human identity adapter; '
77
+ + 'the local TTY is not an identity credential');
78
+ }
79
+ if (operation === 'revoke') {
80
+ requireInteractiveAdministrator();
81
+ if (!args[1])
82
+ throw new Error('revoke requires an approval id');
83
+ return print({ success: await revokePolicyApproval(args[1], root), approvalId: args[1] });
84
+ }
85
+ if (operation === 'audit') {
86
+ const state = loadPolicyState(root);
87
+ return print({ receipts: state.receipts });
88
+ }
89
+ if (operation === 'verify')
90
+ return print(await verifyPolicyLedger(root));
91
+ throw new Error(`unknown policy operation: ${operation}`);
92
+ }
93
+ catch (error) {
94
+ const message = error instanceof Error ? error.message : String(error);
95
+ output.printError(message);
96
+ return { success: false, exitCode: 1, data: { error: message } };
97
+ }
98
+ },
99
+ examples: [
100
+ { command: 'ruflo policy status', description: 'Show policy mode and ledger health' },
101
+ { command: 'ruflo policy init --mode observe', description: 'Migrate an existing install without blocking legacy actions' },
102
+ { command: 'ruflo policy budget set \'{"id":"daily-model","action":"model.call","maxCostUsd":10,"periodMs":86400000}\'', description: 'Set an atomic policy budget ceiling' },
103
+ { command: 'ruflo policy evaluate \'{"identity":{"id":"agent-1","type":"agent"},"action":{"type":"deploy","environment":"production"}}\'', description: 'Evaluate an action and write a decision receipt' },
104
+ ],
105
+ };
106
+ export default policyCommand;
107
+ //# sourceMappingURL=policy.js.map
package/dist/src/index.js CHANGED
@@ -120,6 +120,24 @@ export class CLI {
120
120
  // stamp read + string compare (sub-ms), and the copy runs at most once per
121
121
  // version bump. Best-effort + silent — never blocks or fails a command.
122
122
  if (commandPath[0] !== 'init' && commandPath[0] !== 'update') {
123
+ // ADR-324: existing installations acquire a versioned policy state on
124
+ // first use. Migration is additive and starts in legacy mode, so older
125
+ // AgentDB, MCP, hooks, and swarm workflows keep their exact behavior.
126
+ try {
127
+ const { autoMigratePolicyStateIfNeeded } = await import('./services/policy-runtime.js');
128
+ const migration = await autoMigratePolicyStateIfNeeded(process.cwd());
129
+ if (migration.migrated && this.output.isVerbose()) {
130
+ this.output.printDebug(`Initialized agentic policy compatibility state (${migration.mode})`);
131
+ }
132
+ }
133
+ catch (error) {
134
+ // Legacy compatibility requires startup to remain available if the
135
+ // policy state cannot be initialized. Enforce-mode actions still
136
+ // fail at their authoritative dispatcher when state is unreadable.
137
+ if (this.output.isVerbose()) {
138
+ this.output.printDebug(`Policy compatibility migration skipped: ${error instanceof Error ? error.message : String(error)}`);
139
+ }
140
+ }
123
141
  try {
124
142
  const { autoRefreshHelpersIfStale } = await import('./init/helper-refresh.js');
125
143
  // alsoRefreshGlobal:true — refresh ~/.claude/helpers too, not just
@@ -21,6 +21,8 @@ import { analyzeTools } from './mcp-tools/analyze-tools.js';
21
21
  import { progressTools } from './mcp-tools/progress-tools.js';
22
22
  import { embeddingsTools } from './mcp-tools/embeddings-tools.js';
23
23
  import { claimsTools } from './mcp-tools/claims-tools.js';
24
+ import { policyTools } from './mcp-tools/policy-tools.js';
25
+ import { authorizeMcpTool, classifyMcpTool } from './services/policy-runtime.js';
24
26
  import { securityTools } from './mcp-tools/security-tools.js';
25
27
  import { transferTools } from './mcp-tools/transfer-tools.js';
26
28
  // V2 Compatibility tools
@@ -47,7 +49,7 @@ import { wasmAgentTools } from './mcp-tools/wasm-agent-tools.js';
47
49
  // the local WASM-sandboxed `wasm_agent_*` (rvagent) tools. Lives in the
48
50
  // `ruflo-agent` plugin.
49
51
  import { managedAgentTools } from './mcp-tools/managed-agent-tools.js';
50
- import { guidanceTools } from './mcp-tools/guidance-tools.js';
52
+ import { configureGuidanceToolProvider, guidanceTools, } from './mcp-tools/guidance-tools.js';
51
53
  import { autopilotTools } from './mcp-tools/autopilot-tools.js';
52
54
  // ADR-150 — MetaHarness MCP tools (score / genome / mcp-scan / threat-model / oia-audit)
53
55
  import { metaharnessTools } from './mcp-tools/metaharness-tools.js';
@@ -127,6 +129,10 @@ registerTools([
127
129
  ...progressTools,
128
130
  ...embeddingsTools,
129
131
  ...claimsTools,
132
+ // Remote MCP exposes evaluation/status only. Administrative mutation is a
133
+ // local CLI bootstrap/recovery operation until the server has authenticated
134
+ // human identity propagation.
135
+ ...policyTools.filter((tool) => ['policy_evaluate', 'policy_status'].includes(tool.name)),
130
136
  ...securityTools,
131
137
  ...transferTools,
132
138
  // V2 Compatibility tools
@@ -167,6 +173,15 @@ registerTools([
167
173
  // ADR-164 Phase 4 §5.1.8 — http_fetch (1 tool, secure-by-default HTTP probe)
168
174
  ...httpFetchTools,
169
175
  ]);
176
+ // The capability brain consumes the completed live registry. This is injected
177
+ // after registration to keep guidance honest and avoid a registry import cycle.
178
+ configureGuidanceToolProvider(() => Array.from(TOOL_REGISTRY.values()).map((tool) => ({
179
+ name: tool.name,
180
+ description: tool.description,
181
+ category: tool.category,
182
+ tags: tool.tags,
183
+ version: tool.version,
184
+ })));
170
185
  /**
171
186
  * MCP Client Error
172
187
  */
@@ -211,6 +226,15 @@ export async function callMCPTool(toolName, input = {}, context) {
211
226
  throw new MCPClientError(`MCP tool not found: ${toolName}`, toolName);
212
227
  }
213
228
  try {
229
+ // ADR-324: one policy chokepoint for every local CLI/MCP invocation.
230
+ // Policy administration is not exempt: authorization calls the engine
231
+ // directly, so there is no recursive MCP dispatch. In enforce mode an
232
+ // administrator must explicitly allow policy.* actions or use the local
233
+ // CLI bootstrap path.
234
+ const decision = await authorizeMcpTool(toolName, input, context, classifyMcpTool(toolName));
235
+ if (decision.enforcedOutcome !== 'allowed') {
236
+ throw new Error(`policy-${decision.enforcedOutcome}:${decision.reason}; receipt=${decision.receiptId}`);
237
+ }
214
238
  // Call the tool handler
215
239
  const result = await tool.handler(input, context);
216
240
  // ADR-146 P2: scan every tool result for indirect-injection before it