@claude-flow/cli 3.65.1 → 3.66.1

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 (59) hide show
  1. package/.claude/helpers/helpers.manifest.json +5 -4
  2. package/.claude/helpers/hook-handler.cjs +66 -0
  3. package/.claude/helpers/learning-loop.cjs +594 -0
  4. package/catalog-manifest.json +2 -2
  5. package/dist/src/commands/hooks.js +4 -1
  6. package/dist/src/init/executor.js +1 -1
  7. package/dist/src/init/helper-refresh.js +4 -0
  8. package/dist/src/mcp-tools/memory-tools.js +14 -0
  9. package/dist/src/memory/memory-bridge.js +21 -1
  10. package/dist/src/memory/sona-optimizer.d.ts +7 -0
  11. package/dist/src/memory/sona-optimizer.js +36 -6
  12. package/dist/src/memory/verified-lessons.d.ts +80 -0
  13. package/dist/src/memory/verified-lessons.js +113 -0
  14. package/dist/src/memory/verified-outcomes.d.ts +9 -47
  15. package/dist/src/memory/verified-outcomes.js +11 -46
  16. package/dist/src/mods/command-registry/schema.d.ts +6 -6
  17. package/dist/src/update/rate-limiter.d.ts +6 -0
  18. package/dist/src/update/rate-limiter.js +12 -1
  19. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  20. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/policy-engine.d.ts.map +1 -1
  21. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/policy-engine.js +31 -0
  22. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/policy-engine.js.map +1 -1
  23. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  24. package/node_modules/@claude-flow/plugin-agent-federation/package.json +1 -1
  25. package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
  26. package/package.json +4 -4
  27. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/agent-card.d.ts +0 -130
  28. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/consume.d.ts +0 -34
  29. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/planner-descriptor.d.ts +0 -40
  30. package/node_modules/@claude-flow/plugin-agent-federation/dist/a2a/well-known.d.ts +0 -46
  31. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/claim-checker.d.ts +0 -27
  32. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-breaker-service.d.ts +0 -192
  33. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/federation-coordinator.d.ts +0 -211
  34. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/inbound-dispatcher.d.ts +0 -113
  35. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/policy-engine.d.ts +0 -41
  36. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/spend-reporter.d.ts +0 -150
  37. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/trust-evaluator.d.ts +0 -74
  38. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.d.ts +0 -13
  39. package/node_modules/@claude-flow/plugin-agent-federation/dist/cli-commands.d.ts +0 -7
  40. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/entities/federation-envelope.d.ts +0 -45
  41. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/entities/federation-node.d.ts +0 -104
  42. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/entities/federation-session.d.ts +0 -55
  43. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/entities/trust-level.d.ts +0 -17
  44. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/audit-service.d.ts +0 -69
  45. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/discovery-service.d.ts +0 -76
  46. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/handshake-service.d.ts +0 -62
  47. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/pii-pipeline-service.d.ts +0 -65
  48. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/routing-service.d.ts +0 -54
  49. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/wg-firewall-service.d.ts +0 -71
  50. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/wg-mesh-service.d.ts +0 -156
  51. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/services/wg-witness-service.d.ts +0 -98
  52. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/value-objects/federation-budget.d.ts +0 -89
  53. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/value-objects/federation-node-state.d.ts +0 -92
  54. package/node_modules/@claude-flow/plugin-agent-federation/dist/domain/value-objects/wg-config.d.ts +0 -70
  55. package/node_modules/@claude-flow/plugin-agent-federation/dist/index.d.ts +0 -24
  56. package/node_modules/@claude-flow/plugin-agent-federation/dist/mcp-tools.d.ts +0 -10
  57. package/node_modules/@claude-flow/plugin-agent-federation/dist/plugin.d.ts +0 -19
  58. package/node_modules/@claude-flow/plugin-agent-federation/dist/transport/midstream-aware-loader.d.ts +0 -72
  59. package/node_modules/@claude-flow/plugin-agent-federation/dist/version.d.ts +0 -3
@@ -878,6 +878,19 @@ export const memoryTools = [
878
878
  offset,
879
879
  dbPath,
880
880
  });
881
+ // #4622: a failed query is not an empty store. Without this, a
882
+ // listing that could not read the database came back as a
883
+ // successful, empty inventory.
884
+ if (result.success === false) {
885
+ return {
886
+ success: false,
887
+ entries: [],
888
+ total: 0,
889
+ limit,
890
+ offset,
891
+ error: result.error || 'listEntries reported failure',
892
+ };
893
+ }
881
894
  const entries = result.entries.map(e => ({
882
895
  key: e.key,
883
896
  namespace: e.namespace,
@@ -898,6 +911,7 @@ export const memoryTools = [
898
911
  }
899
912
  catch (error) {
900
913
  return {
914
+ success: false,
901
915
  entries: [],
902
916
  total: 0,
903
917
  limit,
@@ -238,6 +238,17 @@ export function shouldSuppressInitLog(msg) {
238
238
  * Lazily initialize the ControllerRegistry singleton.
239
239
  * Returns null if @claude-flow/memory is not available.
240
240
  */
241
+ /**
242
+ * Why the registry's AgentDB did not open, or null when it did (#4622).
243
+ * Read from the registry's own record (@claude-flow/memory >= 3.2.0); an
244
+ * older registry has none, and nothing changes for it.
245
+ */
246
+ function agentdbInitFailure(registry, resolvedPath) {
247
+ if (typeof registry?.getAgentDBInitError !== 'function')
248
+ return null;
249
+ const reason = registry.getAgentDBInitError();
250
+ return typeof reason === 'string' && reason ? `AgentDB did not open ${resolvedPath}: ${reason}` : null;
251
+ }
241
252
  async function getRegistry(dbPath) {
242
253
  if (shouldDisableNativeBridge())
243
254
  return null;
@@ -527,6 +538,12 @@ async function getRegistry(dbPath) {
527
538
  }
528
539
  registryInstances.set(resolvedPath, registry);
529
540
  bridgeFailureReasons.delete(resolvedPath);
541
+ // #4622: the registry survives an AgentDB open failure (its other
542
+ // controllers still work), but every database operation then falls
543
+ // back to sql.js. Record why, so the fallback's own refusal names it.
544
+ const agentdbFailure = agentdbInitFailure(registry, resolvedPath);
545
+ if (agentdbFailure)
546
+ bridgeFailureReasons.set(resolvedPath, agentdbFailure);
530
547
  return registry;
531
548
  }
532
549
  catch (err) {
@@ -2188,7 +2205,10 @@ export async function isBridgeAvailable(dbPath) {
2188
2205
  if (!operationContext.getStore()?.active)
2189
2206
  return withBridgeOperation(() => isBridgeAvailable(dbPath));
2190
2207
  const registry = await getRegistry(dbPath);
2191
- return registry !== null;
2208
+ if (registry === null)
2209
+ return false;
2210
+ // #4622: a registry whose AgentDB failed to open serves no stored memory.
2211
+ return agentdbInitFailure(registry, canonicalDbPath(dbPath)) === null;
2192
2212
  }
2193
2213
  /**
2194
2214
  * Get the ControllerRegistry instance (for advanced consumers).
@@ -29,6 +29,11 @@ export interface TrajectoryOutcome {
29
29
  }>;
30
30
  feedback?: string;
31
31
  duration?: number;
32
+ /**
33
+ * ADR-511: files the task's verified fix edited (project-relative). Never keywords: they only decide which pattern a
34
+ * trajectory reinforces, so two wordings of one task that fixed the same file learn together.
35
+ */
36
+ anchors?: string[];
32
37
  }
33
38
  /**
34
39
  * Learned routing pattern
@@ -48,6 +53,8 @@ export interface LearnedPattern {
48
53
  lastUsed: number;
49
54
  /** Pattern creation time */
50
55
  createdAt: number;
56
+ /** ADR-511: files verified fixes for this pattern edited; absent in patterns saved before it. */
57
+ anchors?: string[];
51
58
  }
52
59
  /**
53
60
  * Routing suggestion result
@@ -28,6 +28,14 @@ const DECAY_RATE = 0.01; // Per day
28
28
  const MAX_PATTERNS = 1000;
29
29
  /** #4134: a trajectory reinforces an existing pattern of the same agent whose keyword set overlaps at least this much (Jaccard). */
30
30
  const NEIGHBOUR_MIN_JACCARD = 0.6;
31
+ /** ADR-511: files a pattern remembers as anchors, at most. */
32
+ const MAX_ANCHORS = 20;
33
+ /** ADR-511: words too common in task prompts to say two tasks are alike, even when they fixed the same file. */
34
+ const GENERIC_WORDS = new Set(['test', 'tests', 'fix', 'fixes', 'bug', 'bugs', 'run', 'make', 'find', 'check', 'confirm', 'please', 'wrong', 'issue', 'problem', 'error', 'code', 'file', 'files', 'project', 'cause', 'track', 'down', 'need', 'needs', 'some', 'something', 'again', 'npm', 'node']);
35
+ /** ADR-511: non-generic keywords two anchor-sharing tasks must also share to reinforce one pattern. */
36
+ const ANCHOR_MIN_SHARED = 2;
37
+ /** Anchors as stored: lower case, path characters only, at most five per trajectory. */
38
+ const anchorList = (raw) => Array.isArray(raw) ? [...new Set(raw.filter((a) => typeof a === 'string' && /^[a-z0-9_][a-z0-9_./-]{0,119}$/i.test(a) && !a.split('/').includes('..')).map(a => a.toLowerCase()))].slice(0, 5) : [];
31
39
  /** The routing gate in getRoutingSuggestion: confidence x matchRatio must reach this for a learned pattern to be used. */
32
40
  export const SONA_ROUTING_GATE = 0.6;
33
41
  // ============================================================================
@@ -215,6 +223,7 @@ export class SONAOptimizer {
215
223
  */
216
224
  processTrajectoryOutcome(outcome) {
217
225
  const { task, agent, success } = outcome;
226
+ const anchors = anchorList(outcome.anchors);
218
227
  // Extract keywords from task
219
228
  const keywords = this.extractKeywords(task);
220
229
  if (keywords.length === 0) {
@@ -232,7 +241,7 @@ export class SONAOptimizer {
232
241
  // the routing gate. Reinforce the closest existing pattern of the same agent instead.
233
242
  let reinforcedNeighbour = false;
234
243
  if (!this.patterns.has(patternKey)) {
235
- const neighbour = this.findNeighbourPattern(keywords, agent);
244
+ const neighbour = this.findNeighbourPattern(keywords, agent, anchors);
236
245
  if (neighbour) {
237
246
  patternKey = neighbour;
238
247
  reinforcedNeighbour = true;
@@ -263,6 +272,13 @@ export class SONAOptimizer {
263
272
  this.failedRoutings++;
264
273
  }
265
274
  pattern.lastUsed = Date.now();
275
+ if (anchors.length > 0) {
276
+ const kept = pattern.anchors ?? [];
277
+ for (const a of anchors)
278
+ if (!kept.includes(a) && kept.length < MAX_ANCHORS)
279
+ kept.push(a);
280
+ pattern.anchors = kept;
281
+ }
266
282
  // Store pattern
267
283
  this.patterns.set(patternKey, pattern);
268
284
  this.trajectoriesProcessed++;
@@ -566,20 +582,34 @@ export class SONAOptimizer {
566
582
  * `keywords` the most, if that overlap (Jaccard) reaches NEIGHBOUR_MIN_JACCARD.
567
583
  * Ties go to the more confident pattern. Linear in the pattern count (<= 1000).
568
584
  */
569
- findNeighbourPattern(keywords, agent) {
585
+ findNeighbourPattern(keywords, agent, anchors = []) {
570
586
  const wanted = new Set(keywords);
587
+ // ADR-511: a keyword with a `/` names a file or directory (`src/slugify`). Two tasks that each name one and share
588
+ // none are about different code, however alike the rest of their wording: they never reinforce one pattern.
589
+ const paths = keywords.filter(k => k.includes('/'));
571
590
  let bestKey = null;
572
591
  let bestJaccard = 0;
573
592
  let bestConfidence = -1;
574
593
  for (const [key, pattern] of this.patterns) {
575
594
  if (pattern.agent !== agent)
576
595
  continue;
596
+ if (paths.length > 0 && pattern.keywords.some(k => k.includes('/')) && !paths.some(k => pattern.keywords.includes(k)))
597
+ continue;
577
598
  let shared = 0;
578
- for (const k of pattern.keywords)
579
- if (wanted.has(k))
580
- shared++;
599
+ let distinctive = 0;
600
+ for (const k of pattern.keywords) {
601
+ if (!wanted.has(k))
602
+ continue;
603
+ shared++;
604
+ if (!GENERIC_WORDS.has(k))
605
+ distinctive++;
606
+ }
581
607
  const union = wanted.size + pattern.keywords.length - shared;
582
- const jaccard = union === 0 ? 0 : shared / union;
608
+ let jaccard = union === 0 ? 0 : shared / union;
609
+ // ADR-511: a verified fix to the same file, worded differently, is the same task when the wordings also share
610
+ // what makes them specific (two unrelated tasks that both touched a README are not).
611
+ if (jaccard < NEIGHBOUR_MIN_JACCARD && distinctive >= ANCHOR_MIN_SHARED && Array.isArray(pattern.anchors) && anchors.some(a => pattern.anchors.includes(a)))
612
+ jaccard = NEIGHBOUR_MIN_JACCARD;
583
613
  if (jaccard >= NEIGHBOUR_MIN_JACCARD &&
584
614
  (jaccard > bestJaccard || (jaccard === bestJaccard && pattern.confidence > bestConfidence))) {
585
615
  bestKey = key;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The lessons file the learning loop writes (#4581 ADR-502, ADR-511): SONA patterns that verified outcomes trained,
3
+ * at or above the routing gate as lessons, repeated verified failures as cautions.
4
+ *
5
+ * ADR-511 adds what a lesson can act on: the project files the passing fixes edited, counted per pattern in the
6
+ * ingest ledger and named (at most three, most often first) in the lesson. A path reaches the file only if it passes
7
+ * `lessonFile`, the same screen the mod applies when it records and again when it reads.
8
+ */
9
+ import { type HoldReason, type PatternStats } from './verified-guards.js';
10
+ /** The SONA routing gate (sona-optimizer.ts `SONA_ROUTING_GATE`); a lesson below it never influences routing. */
11
+ export declare const LESSON_GATE = 0.6;
12
+ export declare const MAX_LESSONS = 20;
13
+ /** Verified failures (and more failures than successes) before a pattern becomes a caution. */
14
+ export declare const CAUTION_MIN_FAILURES = 2;
15
+ /** ADR-511: files a record may carry, files a pattern counts, and files a lesson names. */
16
+ export declare const MAX_RECORD_FILES = 5;
17
+ export declare const MAX_PATTERN_FILES = 20;
18
+ export declare const MAX_LESSON_FILES = 3;
19
+ export interface Lesson {
20
+ readonly id: string;
21
+ readonly kind: 'lesson' | 'caution';
22
+ readonly agent: string;
23
+ readonly keywords: string[];
24
+ readonly confidence: number;
25
+ readonly successes: number;
26
+ readonly failures: number;
27
+ readonly evidence: string;
28
+ /** ADR-511: files the passing fixes edited, most often first (a lesson only). */
29
+ readonly files?: string[];
30
+ }
31
+ export interface LessonsFile {
32
+ version: 1;
33
+ gate: number;
34
+ generatedAt: number;
35
+ lessons: Lesson[];
36
+ /** Verified patterns still below the gate, for the cold-start progress line. */
37
+ belowGate: number;
38
+ /** Patterns at or above the gate that the anomaly gate kept out of the prompt (ADR-503 §2.4). */
39
+ held: Array<{
40
+ id: string;
41
+ reason: HoldReason;
42
+ }>;
43
+ }
44
+ /** What `SonaLike.exportPatterns()` returns. */
45
+ export type PatternExport = Record<string, {
46
+ keywords: string[];
47
+ agent: string;
48
+ confidence: number;
49
+ successCount: number;
50
+ failureCount: number;
51
+ }>;
52
+ /** The ledger's view of one verified pattern that buildLessons reads. */
53
+ export interface VerifiedPattern {
54
+ evidence: string;
55
+ at: number;
56
+ stats?: PatternStats;
57
+ /** ADR-511: edited file → verified successes that edited it. */
58
+ files?: Record<string, number>;
59
+ }
60
+ /**
61
+ * ADR-511: whether a path may be named in a lesson. Project-relative source files only: no absolute path, no `..`,
62
+ * no dot-file or dot-directory, no name that may hold a secret, no test file and no verifier config.
63
+ */
64
+ export declare function lessonFile(path: unknown): path is string;
65
+ /** A record's `files`, screened. */
66
+ export declare const recordFiles: (raw: unknown) => string[];
67
+ /** A ledger pattern's file counts, screened; a hostile ledger keeps nothing it should not. */
68
+ export declare function parseFileCounts(raw: unknown): Record<string, number>;
69
+ /** Counts one verified success's files; past the cap, the least-counted file makes room. */
70
+ export declare function addFiles(counts: Record<string, number>, files: readonly string[]): void;
71
+ /** The files a lesson names: most often first, ties by name, at most three. */
72
+ export declare const topFiles: (counts: Record<string, number> | undefined) => string[];
73
+ /** A lesson's citation: a short hash of the SONA pattern key, so the key's own text never reaches a prompt. */
74
+ export declare const lessonId: (patternKey: string) => string;
75
+ /**
76
+ * Builds the lessons file from SONA's patterns, keeping only the ones verified outcomes trained. A would-be lesson the
77
+ * anomaly gate holds (ADR-503 §2.4) is listed under `held` instead: it stays in SONA, never in a prompt.
78
+ */
79
+ export declare function buildLessons(patterns: PatternExport, verified: Record<string, VerifiedPattern>, now: number): LessonsFile;
80
+ //# sourceMappingURL=verified-lessons.d.ts.map
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The lessons file the learning loop writes (#4581 ADR-502, ADR-511): SONA patterns that verified outcomes trained,
3
+ * at or above the routing gate as lessons, repeated verified failures as cautions.
4
+ *
5
+ * ADR-511 adds what a lesson can act on: the project files the passing fixes edited, counted per pattern in the
6
+ * ingest ledger and named (at most three, most often first) in the lesson. A path reaches the file only if it passes
7
+ * `lessonFile`, the same screen the mod applies when it records and again when it reads.
8
+ */
9
+ import { createHash } from 'node:crypto';
10
+ import { emptyStats, holdReason } from './verified-guards.js';
11
+ /** The SONA routing gate (sona-optimizer.ts `SONA_ROUTING_GATE`); a lesson below it never influences routing. */
12
+ export const LESSON_GATE = 0.6;
13
+ export const MAX_LESSONS = 20;
14
+ /** Verified failures (and more failures than successes) before a pattern becomes a caution. */
15
+ export const CAUTION_MIN_FAILURES = 2;
16
+ /** ADR-511: files a record may carry, files a pattern counts, and files a lesson names. */
17
+ export const MAX_RECORD_FILES = 5;
18
+ export const MAX_PATTERN_FILES = 20;
19
+ export const MAX_LESSON_FILES = 3;
20
+ const AGENT = /^[a-z][a-z0-9-]{0,40}$/;
21
+ /** Keywords a lesson may show: SONA keeps any non-separator text, the prompt gets only plain words. */
22
+ const SAFE_KEYWORD = /^[a-z0-9][a-z0-9 _./-]{0,40}$/;
23
+ const FILE = /^[A-Za-z0-9_][A-Za-z0-9_./-]{0,119}$/;
24
+ /** Names that may hold a secret, whatever their contents. */
25
+ const SENSITIVE = /(?:^|[._-])(?:env|secrets?|credentials?|passwords?|tokens?|private|id_rsa|id_ed25519)(?:[._-]|$)|\.(?:pem|key|p12|pfx|crt|keystore)$|^(?:npmrc|netrc|pypirc)$/i;
26
+ /** Test files and verifier config (taint.ts in the mod): a lesson that says "edit the test" would teach gaming. */
27
+ const TEST_FILE = /(?:\.(?:test|spec)\.[A-Za-z0-9]+|^test_[^/]*\.py|_test\.(?:go|rs))$/;
28
+ const TEST_DIRS = new Set(['__tests__', 'tests', 'test']);
29
+ const VERIFIER_CONFIG = /^(?:package\.json|vitest\.config\.[^/]+|jest\.config\.[^/]+|pytest\.ini|pyproject\.toml|setup\.cfg|tox\.ini|Cargo\.toml|go\.mod|tsconfig[^/]*\.json|eslint\.config\.[^/]+|Makefile)$/;
30
+ /**
31
+ * ADR-511: whether a path may be named in a lesson. Project-relative source files only: no absolute path, no `..`,
32
+ * no dot-file or dot-directory, no name that may hold a secret, no test file and no verifier config.
33
+ */
34
+ export function lessonFile(path) {
35
+ if (typeof path !== 'string' || !FILE.test(path))
36
+ return false;
37
+ const segs = path.split('/');
38
+ if (segs.some(s => s === '' || s === '..' || s.startsWith('.')))
39
+ return false;
40
+ const base = segs[segs.length - 1];
41
+ if (SENSITIVE.test(base) || TEST_FILE.test(base) || VERIFIER_CONFIG.test(base))
42
+ return false;
43
+ return !segs.slice(0, -1).some(d => TEST_DIRS.has(d));
44
+ }
45
+ /** A record's `files`, screened. */
46
+ export const recordFiles = (raw) => Array.isArray(raw) ? [...new Set(raw.filter(lessonFile))].slice(0, MAX_RECORD_FILES) : [];
47
+ /** A ledger pattern's file counts, screened; a hostile ledger keeps nothing it should not. */
48
+ export function parseFileCounts(raw) {
49
+ const out = {};
50
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
51
+ return out;
52
+ for (const [f, n] of Object.entries(raw).slice(0, MAX_PATTERN_FILES)) {
53
+ if (lessonFile(f) && typeof n === 'number' && Number.isFinite(n) && n >= 1)
54
+ out[f] = Math.min(Math.floor(n), 1_000_000);
55
+ }
56
+ return out;
57
+ }
58
+ /** Counts one verified success's files; past the cap, the least-counted file makes room. */
59
+ export function addFiles(counts, files) {
60
+ for (const f of files) {
61
+ if (counts[f] !== undefined)
62
+ counts[f]++;
63
+ else
64
+ counts[f] = 1;
65
+ }
66
+ const keys = Object.keys(counts);
67
+ if (keys.length > MAX_PATTERN_FILES) {
68
+ for (const k of keys.sort((a, b) => counts[a] - counts[b] || a.localeCompare(b)).slice(0, keys.length - MAX_PATTERN_FILES))
69
+ delete counts[k];
70
+ }
71
+ }
72
+ /** The files a lesson names: most often first, ties by name, at most three. */
73
+ export const topFiles = (counts) => Object.entries(counts ?? {})
74
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
75
+ .slice(0, MAX_LESSON_FILES)
76
+ .map(([f]) => f);
77
+ /** A lesson's citation: a short hash of the SONA pattern key, so the key's own text never reaches a prompt. */
78
+ export const lessonId = (patternKey) => `L-${createHash('sha256').update(patternKey).digest('hex').slice(0, 10)}`;
79
+ /**
80
+ * Builds the lessons file from SONA's patterns, keeping only the ones verified outcomes trained. A would-be lesson the
81
+ * anomaly gate holds (ADR-503 §2.4) is listed under `held` instead: it stays in SONA, never in a prompt.
82
+ */
83
+ export function buildLessons(patterns, verified, now) {
84
+ const lessons = [];
85
+ const held = [];
86
+ let belowGate = 0;
87
+ for (const [id, p] of Object.entries(patterns)) {
88
+ const v = verified[id];
89
+ if (!v || !p || !Array.isArray(p.keywords))
90
+ continue;
91
+ const keywords = p.keywords.filter(k => typeof k === 'string' && SAFE_KEYWORD.test(k)).slice(0, 10);
92
+ if (keywords.length === 0 || !AGENT.test(p.agent))
93
+ continue;
94
+ 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 };
95
+ if (p.failureCount >= CAUTION_MIN_FAILURES && p.failureCount > p.successCount)
96
+ lessons.push({ ...base, kind: 'caution' });
97
+ else if (p.confidence >= LESSON_GATE) {
98
+ const reason = holdReason(v.stats ?? emptyStats(), p.successCount);
99
+ if (reason)
100
+ held.push({ id: base.id, reason });
101
+ else {
102
+ const files = topFiles(v.files);
103
+ lessons.push({ ...base, kind: 'lesson', ...(files.length ? { files } : {}) });
104
+ }
105
+ }
106
+ else
107
+ belowGate++;
108
+ }
109
+ lessons.sort((a, b) => b.confidence - a.confidence || b.successes + b.failures - (a.successes + a.failures));
110
+ held.sort((a, b) => a.id.localeCompare(b.id));
111
+ return { version: 1, gate: LESSON_GATE, generatedAt: now, lessons: lessons.slice(0, MAX_LESSONS), belowGate, held: held.slice(0, MAX_LESSONS) };
112
+ }
113
+ //# sourceMappingURL=verified-lessons.js.map
@@ -26,20 +26,17 @@
26
26
  * it trains, and an anomaly gate holds suspicious patterns back from the
27
27
  * prompt. Every one of these can only downgrade.
28
28
  */
29
- import { type HoldReason, type LoopConfig, type PatternStats, type Taint } from './verified-guards.js';
29
+ import { type LoopConfig, type PatternStats, type Taint } from './verified-guards.js';
30
30
  import { type ReverifyRunner } from './verified-reverify.js';
31
+ import { type LessonsFile, type PatternExport } from './verified-lessons.js';
32
+ export { buildLessons, CAUTION_MIN_FAILURES, LESSON_GATE, lessonFile, lessonId, MAX_LESSONS, type Lesson, type LessonsFile } from './verified-lessons.js';
31
33
  export declare const VERIFIED_PATH: string;
32
34
  export declare const LEDGER_PATH: string;
33
35
  export declare const LESSONS_PATH: string;
34
- /** The SONA routing gate (sona-optimizer.ts `SONA_ROUTING_GATE`); a lesson below it never influences routing. */
35
- export declare const LESSON_GATE = 0.6;
36
36
  /** Ingested ids remembered; the mod keeps at most 500 lines, so this always covers the whole file. */
37
37
  export declare const MAX_LEDGER_IDS = 2000;
38
38
  /** A file past this many bytes is not read (the mod caps its own at far less). */
39
39
  export declare const MAX_FILE_BYTES: number;
40
- export declare const MAX_LESSONS = 20;
41
- /** Verified failures (and more failures than successes) before a pattern becomes a caution. */
42
- export declare const CAUTION_MIN_FAILURES = 2;
43
40
  export type Outcome = 'verified-success' | 'verified-failure' | 'unknown';
44
41
  export type VerifierKind = 'test' | 'build' | 'typecheck' | 'lint';
45
42
  export interface VerifiedRecord {
@@ -61,6 +58,8 @@ export interface VerifiedRecord {
61
58
  readonly taint: readonly Taint[];
62
59
  /** The mod process that recorded it, when the mod wrote one (ADR-503 §2.4). */
63
60
  readonly session: string | null;
61
+ /** ADR-511: files a verified turn edited, screened (`lessonFile`); empty for `unknown`. */
62
+ readonly files: readonly string[];
64
63
  readonly startedAt: number;
65
64
  readonly endedAt: number;
66
65
  }
@@ -84,6 +83,7 @@ export interface IngestLedger {
84
83
  evidence: string;
85
84
  at: number;
86
85
  stats: PatternStats;
86
+ files: Record<string, number>;
87
87
  }>;
88
88
  /** ADR-503 §2.3: the rotation cursor and the latest re-verifications (which commit, which check, what happened). */
89
89
  reverify: {
@@ -99,29 +99,6 @@ export interface ReverifyLogEntry {
99
99
  readonly at: number;
100
100
  }
101
101
  export declare const MAX_REVERIFY_LOG = 50;
102
- export interface Lesson {
103
- readonly id: string;
104
- readonly kind: 'lesson' | 'caution';
105
- readonly agent: string;
106
- readonly keywords: string[];
107
- readonly confidence: number;
108
- readonly successes: number;
109
- readonly failures: number;
110
- readonly evidence: string;
111
- }
112
- export interface LessonsFile {
113
- version: 1;
114
- gate: number;
115
- generatedAt: number;
116
- lessons: Lesson[];
117
- /** Verified patterns still below the gate, for the cold-start progress line. */
118
- belowGate: number;
119
- /** Patterns at or above the gate that the anomaly gate kept out of the prompt (ADR-503 §2.4). */
120
- held: Array<{
121
- id: string;
122
- reason: HoldReason;
123
- }>;
124
- }
125
102
  export interface IngestResult {
126
103
  /** `tainted`: successes whose taint the project did not allow, counted and never trained (ADR-503 §2.2). */
127
104
  ingested: {
@@ -166,35 +143,20 @@ export interface SonaLike {
166
143
  }>;
167
144
  duration?: number;
168
145
  feedback?: string;
146
+ /** ADR-511: the edited files, which decide only which pattern the outcome reinforces. */
147
+ anchors?: string[];
169
148
  }): {
170
149
  learned: boolean;
171
150
  patternKey: string;
172
151
  confidence: number;
173
152
  };
174
- exportPatterns(): Record<string, {
175
- keywords: string[];
176
- agent: string;
177
- confidence: number;
178
- successCount: number;
179
- failureCount: number;
180
- }>;
153
+ exportPatterns(): PatternExport;
181
154
  }
182
155
  /** One line of the mod's file, validated; anything malformed is null. Unknown fields never survive. */
183
156
  export declare function parseRecord(line: string): VerifiedRecord | null;
184
157
  export declare function emptyLedger(): IngestLedger;
185
158
  /** A ledger file read back; a corrupt or hostile one starts over (re-ingest is bounded by the mod's 500-line cap). */
186
159
  export declare function readLedger(raw: string | null): IngestLedger;
187
- /** A lesson's citation: a short hash of the SONA pattern key, so the key's own text never reaches a prompt. */
188
- export declare const lessonId: (patternKey: string) => string;
189
- /**
190
- * Builds the lessons file from SONA's patterns, keeping only the ones verified outcomes trained. A would-be lesson the
191
- * anomaly gate holds (ADR-503 §2.4) is listed under `held` instead: it stays in SONA, never in a prompt.
192
- */
193
- export declare function buildLessons(patterns: ReturnType<SonaLike['exportPatterns']>, verified: Record<string, {
194
- evidence: string;
195
- at: number;
196
- stats?: PatternStats;
197
- }>, now: number): LessonsFile;
198
160
  export interface IngestOptions {
199
161
  /** Project root; defaults to process.cwd(), where SONA keeps `.swarm/`. */
200
162
  root?: string;
@@ -26,23 +26,19 @@
26
26
  * it trains, and an anomaly gate holds suspicious patterns back from the
27
27
  * prompt. Every one of these can only downgrade.
28
28
  */
29
- import { createHash } from 'node:crypto';
30
29
  import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
31
30
  import { dirname, join } from 'node:path';
32
- import { addOutcome, blockedTaints, emptyStats, holdReason, parseStats, parseTaint, readLoopConfig } from './verified-guards.js';
31
+ import { addOutcome, blockedTaints, emptyStats, parseStats, parseTaint, readLoopConfig } from './verified-guards.js';
33
32
  import { gitWorktreeRunner, ReverifySession } from './verified-reverify.js';
33
+ import { addFiles, buildLessons, parseFileCounts, recordFiles } from './verified-lessons.js';
34
+ export { buildLessons, CAUTION_MIN_FAILURES, LESSON_GATE, lessonFile, lessonId, MAX_LESSONS } from './verified-lessons.js';
34
35
  export const VERIFIED_PATH = join('.claude-flow', 'data', 'verified-trajectories.jsonl');
35
36
  export const LEDGER_PATH = join('.claude-flow', 'data', 'verified-ingest.json');
36
37
  export const LESSONS_PATH = join('.claude-flow', 'data', 'learned-lessons.json');
37
- /** The SONA routing gate (sona-optimizer.ts `SONA_ROUTING_GATE`); a lesson below it never influences routing. */
38
- export const LESSON_GATE = 0.6;
39
38
  /** Ingested ids remembered; the mod keeps at most 500 lines, so this always covers the whole file. */
40
39
  export const MAX_LEDGER_IDS = 2000;
41
40
  /** A file past this many bytes is not read (the mod caps its own at far less). */
42
41
  export const MAX_FILE_BYTES = 2 * 1024 * 1024;
43
- export const MAX_LESSONS = 20;
44
- /** Verified failures (and more failures than successes) before a pattern becomes a caution. */
45
- export const CAUTION_MIN_FAILURES = 2;
46
42
  export const MAX_REVERIFY_LOG = 50;
47
43
  const OUTCOMES = ['verified-success', 'verified-failure', 'unknown'];
48
44
  const KINDS = ['test', 'build', 'typecheck', 'lint'];
@@ -92,7 +88,8 @@ export function parseRecord(line) {
92
88
  : [];
93
89
  const taint = outcome === 'verified-success' ? parseTaint(r.taint) : [];
94
90
  const session = typeof r.session === 'string' && /^[A-Za-z0-9_-]{1,40}$/.test(r.session) ? r.session : null;
95
- return { id: r.id, task, agent, outcome, verifier, steps, edits: num(r.edits, 10_000), taint, session, startedAt: num(r.startedAt), endedAt: num(r.endedAt) };
91
+ const files = outcome === 'unknown' ? [] : recordFiles(r.files);
92
+ return { id: r.id, task, agent, outcome, verifier, steps, edits: num(r.edits, 10_000), taint, session, files, startedAt: num(r.startedAt), endedAt: num(r.endedAt) };
96
93
  }
97
94
  export function emptyLedger() {
98
95
  return { version: 1, ids: [], lastMtimeMs: 0, totals: { verifiedSuccess: 0, verifiedFailure: 0, unknown: 0, rejected: 0, tainted: 0 }, patterns: {}, reverify: { cursor: 0, log: [] } };
@@ -110,7 +107,7 @@ export function readLedger(raw) {
110
107
  for (const [k, v] of Object.entries(j.patterns && typeof j.patterns === 'object' ? j.patterns : {})) {
111
108
  if (k === '__proto__' || typeof k !== 'string' || k.length > 400 || !v || typeof v !== 'object')
112
109
  continue;
113
- patterns[k] = { evidence: text(v.evidence, 80), at: num(v.at), stats: parseStats(v.stats) };
110
+ patterns[k] = { evidence: text(v.evidence, 80), at: num(v.at), stats: parseStats(v.stats), files: parseFileCounts(v.files) };
114
111
  }
115
112
  const rv = (j.reverify && typeof j.reverify === 'object' ? j.reverify : {});
116
113
  const STATUS = ['pass', 'fail', 'timeout', 'error'];
@@ -141,42 +138,6 @@ async function redact(task) {
141
138
  return task;
142
139
  }
143
140
  }
144
- /** Keywords a lesson may show: SONA keeps any non-separator text, the prompt gets only plain words. */
145
- const SAFE_KEYWORD = /^[a-z0-9][a-z0-9 _./-]{0,40}$/;
146
- /** A lesson's citation: a short hash of the SONA pattern key, so the key's own text never reaches a prompt. */
147
- export const lessonId = (patternKey) => `L-${createHash('sha256').update(patternKey).digest('hex').slice(0, 10)}`;
148
- /**
149
- * Builds the lessons file from SONA's patterns, keeping only the ones verified outcomes trained. A would-be lesson the
150
- * anomaly gate holds (ADR-503 §2.4) is listed under `held` instead: it stays in SONA, never in a prompt.
151
- */
152
- export function buildLessons(patterns, verified, now) {
153
- const lessons = [];
154
- const held = [];
155
- let belowGate = 0;
156
- for (const [id, p] of Object.entries(patterns)) {
157
- const v = verified[id];
158
- if (!v || !p || !Array.isArray(p.keywords))
159
- continue;
160
- const keywords = p.keywords.filter(k => typeof k === 'string' && SAFE_KEYWORD.test(k)).slice(0, 10);
161
- if (keywords.length === 0 || !AGENT.test(p.agent))
162
- continue;
163
- 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 };
164
- if (p.failureCount >= CAUTION_MIN_FAILURES && p.failureCount > p.successCount)
165
- lessons.push({ ...base, kind: 'caution' });
166
- else if (p.confidence >= LESSON_GATE) {
167
- const reason = holdReason(v.stats ?? emptyStats(), p.successCount);
168
- if (reason)
169
- held.push({ id: base.id, reason });
170
- else
171
- lessons.push({ ...base, kind: 'lesson' });
172
- }
173
- else
174
- belowGate++;
175
- }
176
- lessons.sort((a, b) => b.confidence - a.confidence || b.successes + b.failures - (a.successes + a.failures));
177
- held.sort((a, b) => a.id.localeCompare(b.id));
178
- return { version: 1, gate: LESSON_GATE, generatedAt: now, lessons: lessons.slice(0, MAX_LESSONS), belowGate, held: held.slice(0, MAX_LESSONS) };
179
- }
180
141
  function writeAtomic(path, body) {
181
142
  mkdirSync(dirname(path), { recursive: true });
182
143
  const tmp = `${path}.${process.pid}.tmp`;
@@ -299,12 +260,16 @@ export async function ingestVerifiedOutcomes(options = {}) {
299
260
  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() })),
300
261
  duration: record.endedAt > record.startedAt ? record.endedAt - record.startedAt : 0,
301
262
  feedback: `verified by ${record.verifier.kind}: ${record.verifier.command}`,
263
+ // ADR-511: a failure anchors too, so a reworded failure lowers the same lesson; only a success names files.
264
+ anchors: [...record.files],
302
265
  });
303
266
  if (result.learned && result.patternKey) {
304
- const entry = ledger.patterns[result.patternKey] ?? { evidence: '', at: now, stats: emptyStats() };
267
+ const entry = ledger.patterns[result.patternKey] ?? { evidence: '', at: now, stats: emptyStats(), files: {} };
305
268
  entry.evidence = record.verifier.command;
306
269
  entry.at = now;
307
270
  addOutcome(entry.stats, success, record.taint, record.session);
271
+ if (success)
272
+ addFiles(entry.files, record.files);
308
273
  ledger.patterns[result.patternKey] = entry;
309
274
  }
310
275
  if (success)