claude-flow 3.63.9 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-flow",
3
- "version": "3.63.9",
3
+ "version": "3.64.0",
4
4
  "workspaces": [
5
5
  "v3/@claude-flow/codex",
6
6
  "v3/@claude-flow/mcp",
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 7,
4
- "generatedAt": "2026-10-29T14:18:53.918Z",
5
- "gitSha": "7f6fb334",
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: [
@@ -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 },
@@ -5450,6 +5455,37 @@ export const hooksIntelligenceUnifiedStats = {
5450
5455
  return intel.getUnifiedLearningStats();
5451
5456
  },
5452
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
+ };
5453
5489
  // Export all hooks tools
5454
5490
  export const hooksTools = [
5455
5491
  hooksIntelligenceUnifiedStats,
@@ -5479,6 +5515,7 @@ export const hooksTools = [
5479
5515
  hooksTrajectoryStart,
5480
5516
  hooksTrajectoryStep,
5481
5517
  hooksTrajectoryEnd,
5518
+ hooksVerifiedIngest,
5482
5519
  hooksPatternStore,
5483
5520
  hooksPatternSearch,
5484
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