@claude-flow/cli 3.65.1 → 3.66.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/.claude/helpers/helpers.manifest.json +5 -4
- package/.claude/helpers/hook-handler.cjs +66 -0
- package/.claude/helpers/learning-loop.cjs +594 -0
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/hooks.js +4 -1
- package/dist/src/init/executor.js +1 -1
- package/dist/src/init/helper-refresh.js +4 -0
- package/dist/src/memory/sona-optimizer.d.ts +7 -0
- package/dist/src/memory/sona-optimizer.js +36 -6
- package/dist/src/memory/verified-lessons.d.ts +80 -0
- package/dist/src/memory/verified-lessons.js +113 -0
- package/dist/src/memory/verified-outcomes.d.ts +9 -47
- package/dist/src/memory/verified-outcomes.js +11 -46
- package/dist/src/update/rate-limiter.d.ts +6 -0
- package/dist/src/update/rate-limiter.js +12 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
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
|
|
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():
|
|
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,
|
|
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
|
-
|
|
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)
|
|
@@ -10,6 +10,12 @@ export interface RateLimitState {
|
|
|
10
10
|
}
|
|
11
11
|
export declare function loadState(): RateLimitState;
|
|
12
12
|
export declare function saveState(state: RateLimitState): void;
|
|
13
|
+
/**
|
|
14
|
+
* Whether this is a CI run. CI systems set `CI` to `true`, `1` or other values, so any value
|
|
15
|
+
* except empty, `0` or `false` counts (GitHub Actions sets CI=true). Matching only `true` let CI=1 runs check for updates and
|
|
16
|
+
* install one (`npm install @claude-flow/cli@<latest>`) into the working directory mid-run.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isCI(env?: NodeJS.ProcessEnv): boolean;
|
|
13
19
|
export declare function shouldCheckForUpdates(intervalHours?: number): {
|
|
14
20
|
allowed: boolean;
|
|
15
21
|
reason?: string;
|
|
@@ -45,9 +45,20 @@ export function saveState(state) {
|
|
|
45
45
|
ensureDir();
|
|
46
46
|
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Whether this is a CI run. CI systems set `CI` to `true`, `1` or other values, so any value
|
|
50
|
+
* except empty, `0` or `false` counts (GitHub Actions sets CI=true). Matching only `true` let CI=1 runs check for updates and
|
|
51
|
+
* install one (`npm install @claude-flow/cli@<latest>`) into the working directory mid-run.
|
|
52
|
+
*/
|
|
53
|
+
export function isCI(env = process.env) {
|
|
54
|
+
return ['CI', 'CONTINUOUS_INTEGRATION'].some((name) => {
|
|
55
|
+
const value = env[name];
|
|
56
|
+
return value !== undefined && value.trim() !== '' && !/^(0|false)$/i.test(value.trim());
|
|
57
|
+
});
|
|
58
|
+
}
|
|
48
59
|
export function shouldCheckForUpdates(intervalHours = DEFAULT_INTERVAL_HOURS) {
|
|
49
60
|
// Skip in CI environments
|
|
50
|
-
if (
|
|
61
|
+
if (isCI()) {
|
|
51
62
|
return { allowed: false, reason: 'CI environment detected' };
|
|
52
63
|
}
|
|
53
64
|
// Skip if explicitly disabled
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.66.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",
|