claude-flow 3.63.8 → 3.64.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.
@@ -57,10 +57,10 @@
57
57
  "source": "git-subdir",
58
58
  "url": "https://github.com/proffesor-for-testing/agentic-qe.git",
59
59
  "path": "plugins/agentic-qe-fleet",
60
- "ref": "v3.15.16",
61
- "sha": "4edfa469d8f957b45bc0ea25424eb831b134f129"
60
+ "ref": "v3.15.17",
61
+ "sha": "b177b2315521376b83f2eed40e4994fd7581d067"
62
62
  },
63
- "description": "Agentic QE (external: maintained in proffesor-for-testing/agentic-qe, pinned to v3.15.16): a quality-engineering fleet with 11 QE agents, 9 skills, 9 commands, the agentic-qe MCP server pinned to the plugin version, and the aqe-mod guard that refuses destructive operations on AQE learning data"
63
+ "description": "Agentic QE (external: maintained in proffesor-for-testing/agentic-qe, pinned to v3.15.17): a quality-engineering fleet with 11 QE agents, 9 skills, 9 commands, the agentic-qe MCP server pinned to the plugin version, and the aqe-mod guard that refuses destructive operations on AQE learning data"
64
64
  },
65
65
  {
66
66
  "name": "ruflo-docs",
package/README.md CHANGED
@@ -244,7 +244,7 @@ This adds slash commands and agent definitions. `ruflo-core` (installed above) d
244
244
  | [**ruflo-browser**](plugins/ruflo-browser/README.md) | Automate browser testing with Playwright |
245
245
  | [**ruflo-jujutsu**](plugins/ruflo-jujutsu/README.md) | Analyze git diffs, score risk, suggest reviewers |
246
246
  | [**ruflo-docs**](plugins/ruflo-docs/README.md) | Generate and maintain documentation automatically |
247
- | [**agentic-qe-fleet**](https://github.com/proffesor-for-testing/agentic-qe/tree/v3.15.16/plugins/agentic-qe-fleet) | External, maintained by [Agentic QE](https://github.com/proffesor-for-testing/agentic-qe): 11 QE agents, test generation, coverage, chaos and TDD skills, the `agentic-qe` MCP server, and a guard for AQE learning data (`/plugin install agentic-qe-fleet@ruflo`). Install it from one marketplace only, `agentic-qe` or `ruflo`: enabled from both, its mod and commands load twice |
247
+ | [**agentic-qe-fleet**](https://github.com/proffesor-for-testing/agentic-qe/tree/v3.15.17/plugins/agentic-qe-fleet) | External, maintained by [Agentic QE](https://github.com/proffesor-for-testing/agentic-qe): 11 QE agents, test generation, coverage, chaos and TDD skills, the `agentic-qe` MCP server, and a guard for AQE learning data (`/plugin install agentic-qe-fleet@ruflo`). Install it from one marketplace only, `agentic-qe` or `ruflo`: enabled from both, its mod and commands load twice |
248
248
 
249
249
  #### Security & Compliance
250
250
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-flow",
3
- "version": "3.63.8",
3
+ "version": "3.64.0",
4
4
  "workspaces": [
5
5
  "v3/@claude-flow/codex",
6
6
  "v3/@claude-flow/mcp",
@@ -244,7 +244,7 @@ This adds slash commands and agent definitions. `ruflo-core` (installed above) d
244
244
  | [**ruflo-browser**](plugins/ruflo-browser/README.md) | Automate browser testing with Playwright |
245
245
  | [**ruflo-jujutsu**](plugins/ruflo-jujutsu/README.md) | Analyze git diffs, score risk, suggest reviewers |
246
246
  | [**ruflo-docs**](plugins/ruflo-docs/README.md) | Generate and maintain documentation automatically |
247
- | [**agentic-qe-fleet**](https://github.com/proffesor-for-testing/agentic-qe/tree/v3.15.16/plugins/agentic-qe-fleet) | External, maintained by [Agentic QE](https://github.com/proffesor-for-testing/agentic-qe): 11 QE agents, test generation, coverage, chaos and TDD skills, the `agentic-qe` MCP server, and a guard for AQE learning data (`/plugin install agentic-qe-fleet@ruflo`). Install it from one marketplace only, `agentic-qe` or `ruflo`: enabled from both, its mod and commands load twice |
247
+ | [**agentic-qe-fleet**](https://github.com/proffesor-for-testing/agentic-qe/tree/v3.15.17/plugins/agentic-qe-fleet) | External, maintained by [Agentic QE](https://github.com/proffesor-for-testing/agentic-qe): 11 QE agents, test generation, coverage, chaos and TDD skills, the `agentic-qe` MCP server, and a guard for AQE learning data (`/plugin install agentic-qe-fleet@ruflo`). Install it from one marketplace only, `agentic-qe` or `ruflo`: enabled from both, its mod and commands load twice |
248
248
 
249
249
  #### Security & Compliance
250
250
 
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 7,
4
- "generatedAt": "2026-10-28T19:25:21.649Z",
5
- "gitSha": "1ec84301",
4
+ "generatedAt": "2026-10-29T20:56:47.419Z",
5
+ "gitSha": "00c0f743",
6
6
  "catalog": {
7
7
  "agents": 177,
8
- "tools": 444,
8
+ "tools": 445,
9
9
  "skills": 34
10
10
  },
11
11
  "benchmark": null
@@ -2322,6 +2322,11 @@ async function checkMods() {
2322
2322
  ? { name: 'ruflo mods (ADR-404)', status: 'pass', message: findings.find((f) => f.name === 'last mod start')?.message ?? 'enabled' }
2323
2323
  : { name: 'ruflo mods (ADR-404)', status: 'warn', message: warnings.map((f) => `${f.name}: ${f.message}`).join('; '), fix: 'ruflo mods doctor' };
2324
2324
  }
2325
+ // #4581 / ADR-502: ruflo-mods learning loop — is it recording, and do its verified outcomes reach SONA? Warn-only.
2326
+ async function checkLearningLoop() {
2327
+ const { learningLoopHealth } = await import('../memory/verified-outcomes.js');
2328
+ return { name: 'Learning loop (#4581)', ...learningLoopHealth(process.cwd()) };
2329
+ }
2325
2330
  // Opt-in @ruvector/typesafe task router (optional peer). `--component typesafe` only.
2326
2331
  async function checkTypesafeRouter() {
2327
2332
  const name = '@ruvector/typesafe router';
@@ -2653,6 +2658,7 @@ export const doctorCommand = {
2653
2658
  'auth': checkAuth, // ADR-306
2654
2659
  'typesafe': checkTypesafeRouter, // opt-in @ruvector/typesafe task router
2655
2660
  'mods': checkMods, // ADR-404 — ruflo as a Claude Code mod
2661
+ 'learning-loop': checkLearningLoop, // #4581 — verified-outcome learning loop
2656
2662
  };
2657
2663
  if (component !== undefined && !Object.hasOwn(componentMap, component)) {
2658
2664
  const message = `Unknown doctor component: ${JSON.stringify(component)}. Valid components: ${Object.keys(componentMap).join(', ')}`;
@@ -2765,6 +2771,7 @@ export const doctorCommand = {
2765
2771
  checkProxySponsoredConsent, // ADR-313 — Meta LLM Proxy sponsored-downtime health
2766
2772
  checkAuth, // ADR-306 — Cognitum identity (warn-only; never fails bare `ruflo doctor`)
2767
2773
  checkMods, // ADR-404 — Claude Code mod path (warn-only)
2774
+ checkLearningLoop, // #4581 — verified outcomes reach SONA (warn-only)
2768
2775
  ];
2769
2776
  let checksToRun = allChecks;
2770
2777
  if (component !== undefined) {
@@ -2800,6 +2807,7 @@ export const doctorCommand = {
2800
2807
  [checkProxyBinary, 'proxy-binary'], [checkProxyProcess, 'proxy-process'],
2801
2808
  [checkProxyBindAddress, 'proxy-bind'], [checkAuth, 'auth'],
2802
2809
  [checkTypesafeRouter, 'typesafe'], [checkMods, 'mods'],
2810
+ [checkLearningLoop, 'learning-loop'],
2803
2811
  ]);
2804
2812
  nativeAgentDbChecks.forEach((check, index) => {
2805
2813
  // The path distinguishes simultaneous authority files; normalize separators.
@@ -4649,6 +4649,43 @@ const refreshAdvisorCommand = {
4649
4649
  }
4650
4650
  }
4651
4651
  };
4652
+ // #4581 / ADR-502: fold the ruflo-mods learning loop's verified outcomes into SONA.
4653
+ const verifiedIngestCommand = {
4654
+ name: 'verified-ingest',
4655
+ description: 'Train SONA from the learning loop\'s verified turns (tests/build exit status); unverified turns never train',
4656
+ options: [
4657
+ { name: 'dry-run', description: 'Count only: train nothing and write nothing', type: 'boolean', default: false },
4658
+ { name: 'force', description: 'Re-read the file even if unchanged since the last ingest', type: 'boolean', default: false },
4659
+ ],
4660
+ examples: [
4661
+ { command: 'ruflo hooks verified-ingest', description: 'Ingest new verified outcomes and refresh learned-lessons.json' },
4662
+ { command: 'ruflo hooks verified-ingest --dry-run', description: 'Show what would be trained' },
4663
+ ],
4664
+ action: async (ctx) => {
4665
+ try {
4666
+ const result = await callMCPTool('hooks_intelligence_verified-ingest', { dryRun: ctx.flags['dry-run'] === true || ctx.flags.dryRun === true, force: ctx.flags.force === true });
4667
+ if (ctx.flags.format === 'json') {
4668
+ output.printJson(result);
4669
+ return { success: true, data: result };
4670
+ }
4671
+ if (result.skipped === 'no-file') {
4672
+ output.printInfo('No recorded turns yet (.claude-flow/data/verified-trajectories.jsonl). Turn on the ruflo-mods learningLoop option.');
4673
+ return { success: true, data: result };
4674
+ }
4675
+ if (!result.sonaAvailable)
4676
+ output.printWarning('SONA unavailable: verified outcomes wait for the next ingest');
4677
+ const i = result.ingested;
4678
+ output.printSuccess(`Ingested ${i.verifiedSuccess} verified success, ${i.verifiedFailure} verified failure; ${i.unknown} unverified (not trained), ${i.rejected} malformed${result.skipped === 'unchanged' ? ' (file unchanged)' : ''}`);
4679
+ output.writeln(`Lessons at or above the 0.6 gate: ${result.lessons}; cautions: ${result.cautions}; verified patterns below the gate: ${result.belowGate}`);
4680
+ output.writeln(output.dim(`All time: ${result.totals.verifiedSuccess} success / ${result.totals.verifiedFailure} failure trained, ${result.totals.unknown} unverified`));
4681
+ return { success: true, data: result };
4682
+ }
4683
+ catch (error) {
4684
+ output.printError(`verified-ingest failed: ${error instanceof Error ? error.message : String(error)}`);
4685
+ return { success: false, exitCode: 1 };
4686
+ }
4687
+ },
4688
+ };
4652
4689
  // Main hooks command
4653
4690
  export const hooksCommand = {
4654
4691
  name: 'hooks',
@@ -4696,6 +4733,8 @@ export const hooksCommand = {
4696
4733
  refreshFunnelCommand,
4697
4734
  // Advisor co-pilot tip background refresh — ADR-316
4698
4735
  refreshAdvisorCommand,
4736
+ // Verified-outcome learning loop — #4581
4737
+ verifiedIngestCommand,
4699
4738
  ],
4700
4739
  options: [],
4701
4740
  examples: [
@@ -207,7 +207,7 @@ const runCommand = {
207
207
  ],
208
208
  action: async (ctx) => {
209
209
  const cwd = ctx.cwd || process.cwd();
210
- const dryRun = ctx.flags['dry-run'] === true;
210
+ const dryRun = ctx.flags.dryRun === true || ctx.flags['dry-run'] === true;
211
211
  const skipBackup = ctx.flags.backup === false;
212
212
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
213
213
  const v3Dir = path.join(cwd, '.claude-flow');
@@ -747,7 +747,7 @@ const fixCommand = {
747
747
  output.printSuccess('No removed-agent gaps to fix — every agent is present or covered by an installed plugin.');
748
748
  return { success: true, data: { results: [] } };
749
749
  }
750
- const dryRun = Boolean(ctx.flags['dry-run']);
750
+ const dryRun = ctx.flags.dryRun === true || ctx.flags['dry-run'] === true;
751
751
  const results = await restoreRemovedAgents(cwd, gaps, { dryRun });
752
752
  if (ctx.flags.format === 'json') {
753
753
  output.printJson({ dryRun, results });
@@ -5,5 +5,11 @@
5
5
  * Falls back to local state management when CLI tools are unavailable.
6
6
  */
7
7
  import { type MCPTool } from './types.js';
8
+ /**
9
+ * Drop userinfo from a URL-form remote (`https://user:token@host/o/r`) so a
10
+ * credential embedded in the origin URL never reaches tool output. scp-form
11
+ * remotes (`git@host:o/r`) carry no secret and are returned unchanged.
12
+ */
13
+ export declare function redactRemoteUrl(url: string): string;
8
14
  export declare const githubTools: MCPTool[];
9
15
  //# sourceMappingURL=github-tools.d.ts.map
@@ -114,6 +114,14 @@ function sanitizeLabels(value) {
114
114
  }
115
115
  return out;
116
116
  }
117
+ /**
118
+ * Drop userinfo from a URL-form remote (`https://user:token@host/o/r`) so a
119
+ * credential embedded in the origin URL never reaches tool output. scp-form
120
+ * remotes (`git@host:o/r`) carry no secret and are returned unchanged.
121
+ */
122
+ export function redactRemoteUrl(url) {
123
+ return url.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/]*@/i, '$1');
124
+ }
117
125
  /** Check if gh CLI is available */
118
126
  function hasGhCli() {
119
127
  return run('gh --version') !== null;
@@ -161,7 +169,7 @@ export const githubTools = [
161
169
  let owner = input.owner || '';
162
170
  let repo = input.repo || '';
163
171
  if (remoteUrl && (!owner || !repo)) {
164
- const m = remoteUrl.match(/[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/);
172
+ const m = remoteUrl.match(/[:/]([^/]+)\/([^/]+?)(?:\.git)?$/);
165
173
  if (m) {
166
174
  owner = owner || m[1];
167
175
  repo = repo || m[2];
@@ -200,7 +208,7 @@ export const githubTools = [
200
208
  repository: repoKey,
201
209
  branch: repoInfo.branch,
202
210
  metrics: repoInfo.metrics,
203
- remoteUrl: remoteUrl || null,
211
+ remoteUrl: remoteUrl ? redactRemoteUrl(remoteUrl) : null,
204
212
  lastAnalyzed: repoInfo.lastAnalyzed,
205
213
  };
206
214
  }
@@ -105,6 +105,7 @@ export declare const hooksTaskCompleted: MCPTool;
105
105
  * names its store and a `consistency` block flags relationships that drift.
106
106
  */
107
107
  export declare const hooksIntelligenceUnifiedStats: MCPTool;
108
+ export declare const hooksVerifiedIngest: MCPTool;
108
109
  export declare const hooksTools: MCPTool[];
109
110
  export default hooksTools;
110
111
  //# sourceMappingURL=hooks-tools.d.ts.map
@@ -1181,6 +1181,9 @@ export const hooksRoute = {
1181
1181
  // Q-learning and keyword suggestions retain their existing routing paths.
1182
1182
  try {
1183
1183
  const sonaStart = performance.now();
1184
+ // #4581: fold in any verified outcomes the ruflo-mods learning loop
1185
+ // recorded since the last call (one stat when nothing changed).
1186
+ await ingestVerifiedQuietly();
1184
1187
  const sona = await getSONAOptimizer();
1185
1188
  const suggestion = await sona?.getRoutingSuggestion(task);
1186
1189
  const { isSupportedSonaAgent } = await import('../memory/sona-optimizer.js');
@@ -2639,9 +2642,11 @@ export const hooksSessionEnd = {
2639
2642
  }
2640
2643
  }
2641
2644
  }
2645
+ const verifiedOutcomes = await ingestVerifiedQuietly();
2642
2646
  return {
2643
2647
  sessionId,
2644
2648
  duration,
2649
+ verifiedOutcomes,
2645
2650
  statePath: saveState ? `.claude/sessions/${sessionId}.json` : undefined,
2646
2651
  daemon: { stopped: daemonStopped },
2647
2652
  sessionPersistence: sessionPersistence || { controller: 'none', persisted: false },
@@ -4879,6 +4884,12 @@ export const hooksWorkerDetect = {
4879
4884
  progress: 0,
4880
4885
  phase: 'initializing',
4881
4886
  startedAt: new Date(),
4887
+ // hooks_worker-detect has no executor. Unlike its sibling
4888
+ // hooks_worker-dispatch, it used to silently fabricate a
4889
+ // completed worker after 1.5s. Mark the record and response so
4890
+ // callers cannot treat it as real analysis (#4549 automation scan).
4891
+ synthetic: true,
4892
+ note: 'No executor ran: hooks_worker-detect only records a synthetic worker. Use hooks_worker-dispatch with the daemon for real work.',
4882
4893
  });
4883
4894
  dispatched.push(workerId);
4884
4895
  // Mark worker completion after processing
@@ -4886,13 +4897,15 @@ export const hooksWorkerDetect = {
4886
4897
  const w = activeWorkers.get(workerId);
4887
4898
  if (w) {
4888
4899
  w.progress = 100;
4889
- w.phase = 'completed';
4900
+ w.phase = 'synthetic-completed';
4890
4901
  w.status = 'completed';
4891
4902
  w.completedAt = new Date();
4892
4903
  }
4893
4904
  }, 1500);
4894
4905
  }
4895
4906
  result.autoDispatched = true;
4907
+ result.autoDispatchStatus = 'synthetic-completed';
4908
+ result.note = 'No executor ran: these worker records are synthetic placeholders. Use hooks_worker-dispatch with the daemon for real work.';
4896
4909
  result.workerIds = dispatched;
4897
4910
  }
4898
4911
  }
@@ -5442,6 +5455,37 @@ export const hooksIntelligenceUnifiedStats = {
5442
5455
  return intel.getUnifiedLearningStats();
5443
5456
  },
5444
5457
  };
5458
+ /**
5459
+ * #4581 / ADR-502: ingest the ruflo-mods learning loop's verified outcomes.
5460
+ * Never throws; null when the module or SONA is unavailable.
5461
+ */
5462
+ async function ingestVerifiedQuietly(force = false) {
5463
+ try {
5464
+ const { ingestVerifiedOutcomes } = await import('../memory/verified-outcomes.js');
5465
+ const sona = await getSONAOptimizer();
5466
+ return await ingestVerifiedOutcomes({ force, sona: sona });
5467
+ }
5468
+ catch {
5469
+ return null;
5470
+ }
5471
+ }
5472
+ export const hooksVerifiedIngest = {
5473
+ name: 'hooks_intelligence_verified-ingest',
5474
+ description: 'Ingest the ruflo-mods learning loop\'s recorded turns (.claude-flow/data/verified-trajectories.jsonl, #4581): turns a test, build or typecheck process verified train SONA; unverified turns are counted and never train. Writes .claude-flow/data/learned-lessons.json (patterns at or above the 0.6 routing gate, plus repeated verified failures as cautions). hooks_route and hooks_session-end already ingest automatically; call this to see the counts or to force a re-read. Use when native Bash hooks (via Claude Code\'s settings.json) are wrong because the outcome must reach Ruflo-side state — SONA pattern persistence and routing — and must be gated on a verifier\'s exit status rather than recorded unconditionally.',
5475
+ inputSchema: {
5476
+ type: 'object',
5477
+ properties: {
5478
+ dryRun: { type: 'boolean', description: 'Count only: train nothing and write nothing' },
5479
+ force: { type: 'boolean', description: 'Re-read the file even if it has not changed since the last ingest' },
5480
+ },
5481
+ },
5482
+ handler: async (params) => {
5483
+ const { ingestVerifiedOutcomes } = await import('../memory/verified-outcomes.js');
5484
+ const sona = await getSONAOptimizer();
5485
+ const result = await ingestVerifiedOutcomes({ dryRun: params.dryRun === true, force: params.force === true || params.dryRun === true, sona: sona });
5486
+ return { success: true, ...result };
5487
+ },
5488
+ };
5445
5489
  // Export all hooks tools
5446
5490
  export const hooksTools = [
5447
5491
  hooksIntelligenceUnifiedStats,
@@ -5471,6 +5515,7 @@ export const hooksTools = [
5471
5515
  hooksTrajectoryStart,
5472
5516
  hooksTrajectoryStep,
5473
5517
  hooksTrajectoryEnd,
5518
+ hooksVerifiedIngest,
5474
5519
  hooksPatternStore,
5475
5520
  hooksPatternSearch,
5476
5521
  hooksIntelligenceStats,
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Verified-outcome learning loop, CLI half (#4581, ADR-502).
3
+ *
4
+ * The ruflo-mods `learningLoop` option records one trajectory per main-loop
5
+ * turn in `.claude-flow/data/verified-trajectories.jsonl`. A mod may not run
6
+ * processes or call MCP tools, so it can only observe: a turn is
7
+ * `verified-success` or `verified-failure` when a recognised test, build or
8
+ * typecheck command ran after the turn's last edit and its process exit
9
+ * status said so; anything else is `unknown`.
10
+ *
11
+ * This module is the only place those records reach SONA:
12
+ * - `verified-*` records train SONA (`processTrajectoryOutcome`);
13
+ * - `unknown` records are counted and never train;
14
+ * - each record is ingested once (an id ledger, never a rewrite of the
15
+ * mod's file, which the mod appends to concurrently);
16
+ * - task text is PII-redacted before it reaches SONA's keyword store.
17
+ *
18
+ * It then writes `.claude-flow/data/learned-lessons.json`: patterns that
19
+ * verified outcomes produced, those at or above the SONA routing gate as
20
+ * lessons, and repeated verified failures as cautions. The mod reads that
21
+ * file and adds at most three cited lessons to a prompt.
22
+ */
23
+ export declare const VERIFIED_PATH: string;
24
+ export declare const LEDGER_PATH: string;
25
+ export declare const LESSONS_PATH: string;
26
+ /** The SONA routing gate (sona-optimizer.ts `SONA_ROUTING_GATE`); a lesson below it never influences routing. */
27
+ export declare const LESSON_GATE = 0.6;
28
+ /** Ingested ids remembered; the mod keeps at most 500 lines, so this always covers the whole file. */
29
+ export declare const MAX_LEDGER_IDS = 2000;
30
+ /** A file past this many bytes is not read (the mod caps its own at far less). */
31
+ export declare const MAX_FILE_BYTES: number;
32
+ export declare const MAX_LESSONS = 20;
33
+ /** Verified failures (and more failures than successes) before a pattern becomes a caution. */
34
+ export declare const CAUTION_MIN_FAILURES = 2;
35
+ export type Outcome = 'verified-success' | 'verified-failure' | 'unknown';
36
+ export type VerifierKind = 'test' | 'build' | 'typecheck' | 'lint';
37
+ export interface VerifiedRecord {
38
+ readonly id: string;
39
+ readonly task: string;
40
+ readonly agent: string;
41
+ readonly outcome: Outcome;
42
+ readonly verifier: {
43
+ readonly kind: VerifierKind;
44
+ readonly command: string;
45
+ readonly passed: boolean;
46
+ } | null;
47
+ readonly steps: ReadonlyArray<{
48
+ readonly tool: string;
49
+ readonly ok: boolean;
50
+ }>;
51
+ readonly edits: number;
52
+ readonly startedAt: number;
53
+ readonly endedAt: number;
54
+ }
55
+ export interface IngestLedger {
56
+ version: 1;
57
+ ids: string[];
58
+ /** mtime of the jsonl at the last ingest, so a caller can skip an unchanged file cheaply. */
59
+ lastMtimeMs: number;
60
+ totals: {
61
+ verifiedSuccess: number;
62
+ verifiedFailure: number;
63
+ unknown: number;
64
+ rejected: number;
65
+ };
66
+ /** Pattern keys verified outcomes trained, with the last verifier command that decided them (the lesson's evidence). */
67
+ patterns: Record<string, {
68
+ evidence: string;
69
+ at: number;
70
+ }>;
71
+ }
72
+ export interface Lesson {
73
+ readonly id: string;
74
+ readonly kind: 'lesson' | 'caution';
75
+ readonly agent: string;
76
+ readonly keywords: string[];
77
+ readonly confidence: number;
78
+ readonly successes: number;
79
+ readonly failures: number;
80
+ readonly evidence: string;
81
+ }
82
+ export interface LessonsFile {
83
+ version: 1;
84
+ gate: number;
85
+ generatedAt: number;
86
+ lessons: Lesson[];
87
+ /** Verified patterns still below the gate, for the cold-start progress line. */
88
+ belowGate: number;
89
+ }
90
+ export interface IngestResult {
91
+ ingested: {
92
+ verifiedSuccess: number;
93
+ verifiedFailure: number;
94
+ unknown: number;
95
+ rejected: number;
96
+ };
97
+ trained: number;
98
+ skipped: 'no-file' | 'unchanged' | 'too-large' | null;
99
+ lessons: number;
100
+ cautions: number;
101
+ belowGate: number;
102
+ totals: IngestLedger['totals'];
103
+ sonaAvailable: boolean;
104
+ }
105
+ /** The SONA surface ingest needs; injectable so tests never touch `.swarm/`. */
106
+ export interface SonaLike {
107
+ processTrajectoryOutcome(outcome: {
108
+ trajectoryId: string;
109
+ task: string;
110
+ agent: string;
111
+ success: boolean;
112
+ steps?: Array<{
113
+ action: string;
114
+ result: string;
115
+ quality: number;
116
+ timestamp: string;
117
+ }>;
118
+ duration?: number;
119
+ feedback?: string;
120
+ }): {
121
+ learned: boolean;
122
+ patternKey: string;
123
+ confidence: number;
124
+ };
125
+ exportPatterns(): Record<string, {
126
+ keywords: string[];
127
+ agent: string;
128
+ confidence: number;
129
+ successCount: number;
130
+ failureCount: number;
131
+ }>;
132
+ }
133
+ /** One line of the mod's file, validated; anything malformed is null. Unknown fields never survive. */
134
+ export declare function parseRecord(line: string): VerifiedRecord | null;
135
+ export declare function emptyLedger(): IngestLedger;
136
+ /** A ledger file read back; a corrupt or hostile one starts over (re-ingest is bounded by the mod's 500-line cap). */
137
+ export declare function readLedger(raw: string | null): IngestLedger;
138
+ /** A lesson's citation: a short hash of the SONA pattern key, so the key's own text never reaches a prompt. */
139
+ export declare const lessonId: (patternKey: string) => string;
140
+ /** Builds the lessons file from SONA's patterns, keeping only the ones verified outcomes trained. */
141
+ export declare function buildLessons(patterns: ReturnType<SonaLike['exportPatterns']>, verified: IngestLedger['patterns'], now: number): LessonsFile;
142
+ export interface IngestOptions {
143
+ /** Project root; defaults to process.cwd(), where SONA keeps `.swarm/`. */
144
+ root?: string;
145
+ /** Ingest even when the file's mtime matches the last ingest. */
146
+ force?: boolean;
147
+ /** Count only: train nothing and write nothing. */
148
+ dryRun?: boolean;
149
+ /** SONA to train; defaults to the process's optimizer. Null means unavailable. */
150
+ sona?: SonaLike | null;
151
+ now?: number;
152
+ }
153
+ export declare function ingestVerifiedOutcomes(options?: IngestOptions): Promise<IngestResult>;
154
+ /**
155
+ * The doctor's view: whether the loop is recording and whether its records
156
+ * reach SONA. Read-only.
157
+ */
158
+ export declare function learningLoopHealth(root?: string): {
159
+ status: 'pass' | 'warn';
160
+ message: string;
161
+ fix?: string;
162
+ };
163
+ //# sourceMappingURL=verified-outcomes.d.ts.map
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Verified-outcome learning loop, CLI half (#4581, ADR-502).
3
+ *
4
+ * The ruflo-mods `learningLoop` option records one trajectory per main-loop
5
+ * turn in `.claude-flow/data/verified-trajectories.jsonl`. A mod may not run
6
+ * processes or call MCP tools, so it can only observe: a turn is
7
+ * `verified-success` or `verified-failure` when a recognised test, build or
8
+ * typecheck command ran after the turn's last edit and its process exit
9
+ * status said so; anything else is `unknown`.
10
+ *
11
+ * This module is the only place those records reach SONA:
12
+ * - `verified-*` records train SONA (`processTrajectoryOutcome`);
13
+ * - `unknown` records are counted and never train;
14
+ * - each record is ingested once (an id ledger, never a rewrite of the
15
+ * mod's file, which the mod appends to concurrently);
16
+ * - task text is PII-redacted before it reaches SONA's keyword store.
17
+ *
18
+ * It then writes `.claude-flow/data/learned-lessons.json`: patterns that
19
+ * verified outcomes produced, those at or above the SONA routing gate as
20
+ * lessons, and repeated verified failures as cautions. The mod reads that
21
+ * file and adds at most three cited lessons to a prompt.
22
+ */
23
+ import { createHash } from 'node:crypto';
24
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
25
+ import { dirname, join } from 'node:path';
26
+ export const VERIFIED_PATH = join('.claude-flow', 'data', 'verified-trajectories.jsonl');
27
+ export const LEDGER_PATH = join('.claude-flow', 'data', 'verified-ingest.json');
28
+ export const LESSONS_PATH = join('.claude-flow', 'data', 'learned-lessons.json');
29
+ /** The SONA routing gate (sona-optimizer.ts `SONA_ROUTING_GATE`); a lesson below it never influences routing. */
30
+ export const LESSON_GATE = 0.6;
31
+ /** Ingested ids remembered; the mod keeps at most 500 lines, so this always covers the whole file. */
32
+ export const MAX_LEDGER_IDS = 2000;
33
+ /** A file past this many bytes is not read (the mod caps its own at far less). */
34
+ export const MAX_FILE_BYTES = 2 * 1024 * 1024;
35
+ export const MAX_LESSONS = 20;
36
+ /** Verified failures (and more failures than successes) before a pattern becomes a caution. */
37
+ export const CAUTION_MIN_FAILURES = 2;
38
+ const OUTCOMES = ['verified-success', 'verified-failure', 'unknown'];
39
+ const KINDS = ['test', 'build', 'typecheck', 'lint'];
40
+ const ID = /^vt-[A-Za-z0-9_-]{1,80}$/;
41
+ const AGENT = /^[a-z][a-z0-9-]{0,40}$/;
42
+ const TOOL = /^[A-Za-z0-9_.:-]{1,80}$/;
43
+ /** SONA's agent roles (sona-optimizer.ts AGENT_TYPES); a route agent outside them trains as `coder`. */
44
+ const SONA_AGENTS = new Set(['coder', 'tester', 'reviewer', 'architect', 'researcher', 'optimizer', 'debugger', 'documenter', 'security-architect', 'performance-engineer']);
45
+ const num = (v, max = Number.MAX_SAFE_INTEGER) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? Math.min(Math.floor(v), max) : 0);
46
+ const text = (v, max) => (typeof v === 'string' ? v.replace(/[\u0000-\u001f\u007f]/g, ' ').trim().slice(0, max) : '');
47
+ /** One line of the mod's file, validated; anything malformed is null. Unknown fields never survive. */
48
+ export function parseRecord(line) {
49
+ let raw;
50
+ try {
51
+ raw = JSON.parse(line);
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
57
+ return null;
58
+ const r = raw;
59
+ if (r.v !== 1 || typeof r.id !== 'string' || !ID.test(r.id))
60
+ return null;
61
+ if (!OUTCOMES.includes(r.outcome))
62
+ return null;
63
+ const agent = typeof r.agent === 'string' && AGENT.test(r.agent) ? r.agent : 'coder';
64
+ const task = text(r.task, 300);
65
+ if (!task)
66
+ return null;
67
+ let verifier = null;
68
+ if (r.verifier && typeof r.verifier === 'object') {
69
+ const v = r.verifier;
70
+ if (KINDS.includes(v.kind) && typeof v.passed === 'boolean') {
71
+ verifier = { kind: v.kind, command: text(v.command, 80), passed: v.passed };
72
+ }
73
+ }
74
+ // A verified outcome must carry the verifier that decided it, and agree with it.
75
+ const outcome = r.outcome;
76
+ if (outcome !== 'unknown' && (!verifier || verifier.passed !== (outcome === 'verified-success')))
77
+ return null;
78
+ const steps = Array.isArray(r.steps)
79
+ ? r.steps
80
+ .slice(0, 64)
81
+ .filter((s) => !!s && typeof s === 'object' && typeof s.tool === 'string' && TOOL.test(s.tool))
82
+ .map(s => ({ tool: s.tool, ok: s.ok === true }))
83
+ : [];
84
+ return { id: r.id, task, agent, outcome, verifier, steps, edits: num(r.edits, 10_000), startedAt: num(r.startedAt), endedAt: num(r.endedAt) };
85
+ }
86
+ export function emptyLedger() {
87
+ return { version: 1, ids: [], lastMtimeMs: 0, totals: { verifiedSuccess: 0, verifiedFailure: 0, unknown: 0, rejected: 0 }, patterns: {} };
88
+ }
89
+ /** A ledger file read back; a corrupt or hostile one starts over (re-ingest is bounded by the mod's 500-line cap). */
90
+ export function readLedger(raw) {
91
+ if (!raw)
92
+ return emptyLedger();
93
+ try {
94
+ const j = JSON.parse(raw);
95
+ if (!j || j.version !== 1 || !Array.isArray(j.ids))
96
+ return emptyLedger();
97
+ const t = (j.totals ?? {});
98
+ const patterns = {};
99
+ for (const [k, v] of Object.entries(j.patterns && typeof j.patterns === 'object' ? j.patterns : {})) {
100
+ if (k === '__proto__' || typeof k !== 'string' || k.length > 400 || !v || typeof v !== 'object')
101
+ continue;
102
+ patterns[k] = { evidence: text(v.evidence, 80), at: num(v.at) };
103
+ }
104
+ return {
105
+ version: 1,
106
+ ids: j.ids.filter((id) => typeof id === 'string' && ID.test(id)).slice(-MAX_LEDGER_IDS),
107
+ lastMtimeMs: num(j.lastMtimeMs),
108
+ totals: { verifiedSuccess: num(t.verifiedSuccess), verifiedFailure: num(t.verifiedFailure), unknown: num(t.unknown), rejected: num(t.rejected) },
109
+ patterns,
110
+ };
111
+ }
112
+ catch {
113
+ return emptyLedger();
114
+ }
115
+ }
116
+ /** Redacts PII from task text before it reaches SONA's keyword store; never throws. */
117
+ async function redact(task) {
118
+ try {
119
+ const { detectPII, redactPII } = await import('../transfer/anonymization/index.js');
120
+ return detectPII(task).found ? redactPII(task) : task;
121
+ }
122
+ catch {
123
+ return task;
124
+ }
125
+ }
126
+ /** Keywords a lesson may show: SONA keeps any non-separator text, the prompt gets only plain words. */
127
+ const SAFE_KEYWORD = /^[a-z0-9][a-z0-9 _./-]{0,40}$/;
128
+ /** A lesson's citation: a short hash of the SONA pattern key, so the key's own text never reaches a prompt. */
129
+ export const lessonId = (patternKey) => `L-${createHash('sha256').update(patternKey).digest('hex').slice(0, 10)}`;
130
+ /** Builds the lessons file from SONA's patterns, keeping only the ones verified outcomes trained. */
131
+ export function buildLessons(patterns, verified, now) {
132
+ const lessons = [];
133
+ let belowGate = 0;
134
+ for (const [id, p] of Object.entries(patterns)) {
135
+ const v = verified[id];
136
+ if (!v || !p || !Array.isArray(p.keywords))
137
+ continue;
138
+ const keywords = p.keywords.filter(k => typeof k === 'string' && SAFE_KEYWORD.test(k)).slice(0, 10);
139
+ if (keywords.length === 0 || !AGENT.test(p.agent))
140
+ continue;
141
+ const base = { id: lessonId(id), agent: p.agent, keywords, confidence: Math.round(p.confidence * 1000) / 1000, successes: p.successCount, failures: p.failureCount, evidence: v.evidence };
142
+ if (p.failureCount >= CAUTION_MIN_FAILURES && p.failureCount > p.successCount)
143
+ lessons.push({ ...base, kind: 'caution' });
144
+ else if (p.confidence >= LESSON_GATE)
145
+ lessons.push({ ...base, kind: 'lesson' });
146
+ else
147
+ belowGate++;
148
+ }
149
+ lessons.sort((a, b) => b.confidence - a.confidence || b.successes + b.failures - (a.successes + a.failures));
150
+ return { version: 1, gate: LESSON_GATE, generatedAt: now, lessons: lessons.slice(0, MAX_LESSONS), belowGate };
151
+ }
152
+ function writeAtomic(path, body) {
153
+ mkdirSync(dirname(path), { recursive: true });
154
+ const tmp = `${path}.${process.pid}.tmp`;
155
+ writeFileSync(tmp, body);
156
+ renameSync(tmp, path);
157
+ }
158
+ /**
159
+ * Ingests every not-yet-ingested record. Verified records train SONA; unknown
160
+ * ones are counted. Safe to call often: an unchanged file is one stat.
161
+ */
162
+ /** Per-root mtime of the last completed ingest in this process, so a hot caller (hooks_route) skips the ledger read too. */
163
+ const lastIngested = new Map();
164
+ export async function ingestVerifiedOutcomes(options = {}) {
165
+ const root = options.root ?? process.cwd();
166
+ const now = options.now ?? Date.now();
167
+ const file = join(root, VERIFIED_PATH);
168
+ if (!options.force && !options.dryRun && options.sona !== null) {
169
+ try {
170
+ const memo = lastIngested.get(root);
171
+ if (memo && memo.mtimeMs === statSync(file).mtimeMs) {
172
+ return { ingested: { verifiedSuccess: 0, verifiedFailure: 0, unknown: 0, rejected: 0 }, trained: 0, skipped: 'unchanged', lessons: 0, cautions: 0, belowGate: 0, totals: { ...memo.totals }, sonaAvailable: true };
173
+ }
174
+ }
175
+ catch {
176
+ // no file: the full path below reports it
177
+ }
178
+ }
179
+ const ledgerPath = join(root, LEDGER_PATH);
180
+ const ledger = readLedger(existsSync(ledgerPath) ? readFileSync(ledgerPath, 'utf8') : null);
181
+ const ingested = { verifiedSuccess: 0, verifiedFailure: 0, unknown: 0, rejected: 0 };
182
+ const done = (skipped, sonaAvailable, lessons) => ({
183
+ ingested,
184
+ trained: ingested.verifiedSuccess + ingested.verifiedFailure,
185
+ skipped,
186
+ lessons: lessons?.lessons.filter(l => l.kind === 'lesson').length ?? 0,
187
+ cautions: lessons?.lessons.filter(l => l.kind === 'caution').length ?? 0,
188
+ belowGate: lessons?.belowGate ?? 0,
189
+ totals: ledger.totals,
190
+ sonaAvailable,
191
+ });
192
+ if (!existsSync(file))
193
+ return done('no-file', options.sona !== null);
194
+ const st = statSync(file);
195
+ if (st.size > MAX_FILE_BYTES)
196
+ return done('too-large', options.sona !== null);
197
+ if (!options.force && st.mtimeMs === ledger.lastMtimeMs)
198
+ return done('unchanged', options.sona !== null);
199
+ let sona = options.sona;
200
+ if (sona === undefined) {
201
+ try {
202
+ const { getSONAOptimizer } = await import('./sona-optimizer.js');
203
+ sona = (await getSONAOptimizer());
204
+ }
205
+ catch {
206
+ sona = null;
207
+ }
208
+ }
209
+ const seen = new Set(ledger.ids);
210
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
211
+ if (!line.trim())
212
+ continue;
213
+ const record = parseRecord(line);
214
+ if (!record) {
215
+ ingested.rejected++;
216
+ continue;
217
+ }
218
+ if (seen.has(record.id))
219
+ continue;
220
+ // Without SONA nothing verified can train yet: leave it un-ingested for the next call.
221
+ if (record.outcome !== 'unknown' && !sona)
222
+ continue;
223
+ seen.add(record.id);
224
+ ledger.ids.push(record.id);
225
+ if (record.outcome === 'unknown') {
226
+ ingested.unknown++;
227
+ continue;
228
+ }
229
+ if (options.dryRun) {
230
+ if (record.outcome === 'verified-success')
231
+ ingested.verifiedSuccess++;
232
+ else
233
+ ingested.verifiedFailure++;
234
+ continue;
235
+ }
236
+ const success = record.outcome === 'verified-success';
237
+ const result = sona.processTrajectoryOutcome({
238
+ trajectoryId: record.id,
239
+ task: await redact(record.task),
240
+ agent: SONA_AGENTS.has(record.agent) ? record.agent : 'coder',
241
+ success,
242
+ steps: record.steps.map(s => ({ action: s.tool, result: s.ok ? 'ok' : 'error', quality: s.ok ? 1 : 0, timestamp: new Date(record.endedAt || now).toISOString() })),
243
+ duration: record.endedAt > record.startedAt ? record.endedAt - record.startedAt : 0,
244
+ feedback: `verified by ${record.verifier.kind}: ${record.verifier.command}`,
245
+ });
246
+ if (result.learned && result.patternKey)
247
+ ledger.patterns[result.patternKey] = { evidence: record.verifier.command, at: now };
248
+ if (success)
249
+ ingested.verifiedSuccess++;
250
+ else
251
+ ingested.verifiedFailure++;
252
+ }
253
+ if (options.dryRun)
254
+ return done(null, !!sona);
255
+ ledger.ids = ledger.ids.slice(-MAX_LEDGER_IDS);
256
+ // Without SONA, verified records stayed un-ingested: keep the mtime stale so the next call retries them.
257
+ if (sona) {
258
+ ledger.lastMtimeMs = st.mtimeMs;
259
+ lastIngested.set(root, { mtimeMs: st.mtimeMs, totals: { ...ledger.totals } });
260
+ }
261
+ for (const k of Object.keys(ingested))
262
+ ledger.totals[k] += ingested[k];
263
+ // Keep the verified-pattern map bounded to what SONA still holds.
264
+ const patterns = sona ? sona.exportPatterns() : {};
265
+ if (sona)
266
+ for (const k of Object.keys(ledger.patterns))
267
+ if (!patterns[k])
268
+ delete ledger.patterns[k];
269
+ writeAtomic(ledgerPath, JSON.stringify(ledger));
270
+ const lessons = sona ? buildLessons(patterns, ledger.patterns, now) : undefined;
271
+ if (lessons)
272
+ writeAtomic(join(root, LESSONS_PATH), JSON.stringify(lessons, null, 2));
273
+ return done(null, !!sona, lessons);
274
+ }
275
+ /**
276
+ * The doctor's view: whether the loop is recording and whether its records
277
+ * reach SONA. Read-only.
278
+ */
279
+ export function learningLoopHealth(root = process.cwd()) {
280
+ const file = join(root, VERIFIED_PATH);
281
+ if (!existsSync(file))
282
+ return { status: 'pass', message: 'not recording (ruflo-mods learningLoop option off or no turn yet)' };
283
+ let lines = [];
284
+ try {
285
+ if (statSync(file).size <= MAX_FILE_BYTES)
286
+ lines = readFileSync(file, 'utf8').split('\n').filter(l => l.trim());
287
+ }
288
+ catch {
289
+ return { status: 'warn', message: 'verified-trajectories.jsonl unreadable' };
290
+ }
291
+ const ledgerPath = join(root, LEDGER_PATH);
292
+ const ledger = readLedger(existsSync(ledgerPath) ? readFileSync(ledgerPath, 'utf8') : null);
293
+ const seen = new Set(ledger.ids);
294
+ const records = lines.map(parseRecord).filter((r) => r !== null);
295
+ const waiting = records.filter(r => !seen.has(r.id) && r.outcome !== 'unknown').length;
296
+ const verified = records.filter(r => r.outcome !== 'unknown').length;
297
+ const summary = `${records.length} turn(s) recorded, ${verified} verified; trained so far ${ledger.totals.verifiedSuccess} success / ${ledger.totals.verifiedFailure} failure, ${ledger.totals.unknown} unknown (never trained)`;
298
+ if (waiting > 0) {
299
+ return { status: 'warn', message: `${summary}; ${waiting} verified outcome(s) not yet ingested`, fix: 'ruflo hooks verified-ingest (or any hooks_route / hooks_session-end call ingests them)' };
300
+ }
301
+ return { status: 'pass', message: summary };
302
+ }
303
+ //# sourceMappingURL=verified-outcomes.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.63.8",
3
+ "version": "3.64.0",
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",
@@ -1 +0,0 @@
1
- sha256:6141a8ea990c5063b77e090ae8f37f9c539d8aa8f58dcceb30f3a82f97e57319
@@ -1,42 +0,0 @@
1
- {
2
- "adoptedAt": 1793206001070,
3
- "championId": "sha256:6141a8ea990c5063b77e090ae8f37f9c539d8aa8f58dcceb30f3a82f97e57319",
4
- "manifest": {
5
- "schema": "ruflo.proven-config/v1",
6
- "policy": {
7
- "ref": "sha256:6141a8ea990c5063b77e090ae8f37f9c539d8aa8f58dcceb30f3a82f97e57319",
8
- "value": {
9
- "alpha": 0.3,
10
- "subjectWeight": 1,
11
- "mmrLambda": 0.5,
12
- "bodyWeight": 1.5,
13
- "typePenaltyFactor": 0.5
14
- }
15
- },
16
- "layer": "framework/node-cli",
17
- "compatibility": {
18
- "ruflo": ">=3.24.0"
19
- },
20
- "benchmark": {
21
- "corpus": "ADR-081-labelled-v1",
22
- "corpusHash": "sha256:2f700b5c363e20a3bd88ce2bc9b87bbbbaa61732c6177894c6ec37890f888982"
23
- },
24
- "receipt": {
25
- "heldOutDelta": 0.07381404928570845,
26
- "redblue": "PASS",
27
- "drift": 0,
28
- "canary": {
29
- "rollbackRate": 0,
30
- "latencyP95": 244.612458000076,
31
- "costPerTask": 0
32
- },
33
- "receiptCoverage": 1
34
- },
35
- "platform": [
36
- "linux",
37
- "macOS",
38
- "windows"
39
- ]
40
- },
41
- "previous": ""
42
- }