@claude-flow/cli 3.65.0 → 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/mcp-tools/http-fetch-pinned.d.ts +44 -0
- package/dist/src/mcp-tools/http-fetch-pinned.js +148 -0
- package/dist/src/mcp-tools/http-fetch-tools.d.ts +11 -2
- package/dist/src/mcp-tools/http-fetch-tools.js +19 -70
- 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
|
@@ -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",
|