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