@claude-flow/cli 3.64.0 → 3.65.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/helpers/helpers.manifest.json +2 -2
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/cost.d.ts +18 -0
- package/dist/src/commands/cost.js +195 -0
- package/dist/src/commands/doctor-auto-approve.d.ts +9 -0
- package/dist/src/commands/doctor-auto-approve.js +32 -0
- package/dist/src/commands/doctor.js +21 -3
- package/dist/src/commands/hive-mind-worker.d.ts +2 -0
- package/dist/src/commands/hive-mind-worker.js +9 -3
- package/dist/src/commands/hive-mind.js +15 -1
- package/dist/src/commands/hooks.js +8 -2
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init.js +76 -2
- package/dist/src/commands/mods.js +13 -0
- package/dist/src/commands/plugins-paths.d.ts +13 -0
- package/dist/src/commands/plugins-paths.js +55 -0
- package/dist/src/commands/plugins-verify.d.ts +9 -0
- package/dist/src/commands/plugins-verify.js +92 -0
- package/dist/src/commands/plugins.js +13 -1
- package/dist/src/cost/attribution.d.ts +35 -0
- package/dist/src/cost/attribution.js +101 -0
- package/dist/src/cost/budget-file.d.ts +57 -0
- package/dist/src/cost/budget-file.js +113 -0
- package/dist/src/cost/litellm.d.ts +69 -0
- package/dist/src/cost/litellm.js +135 -0
- package/dist/src/init/executor.d.ts +7 -2
- package/dist/src/init/executor.js +64 -10
- package/dist/src/init/helper-commands.d.ts +16 -0
- package/dist/src/init/helper-commands.js +73 -0
- package/dist/src/init/risky-settings-gate.d.ts +39 -0
- package/dist/src/init/risky-settings-gate.js +130 -0
- package/dist/src/init/settings-risk-scanner.d.ts +35 -0
- package/dist/src/init/settings-risk-scanner.js +85 -0
- package/dist/src/init/types.d.ts +12 -0
- package/dist/src/mcp-tools/capability-brain.d.ts +1 -1
- package/dist/src/mcp-tools/capability-brain.js +1 -1
- package/dist/src/mcp-tools/hooks-tools.js +2 -1
- 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/verified-guards.d.ts +70 -0
- package/dist/src/memory/verified-guards.js +101 -0
- package/dist/src/memory/verified-outcomes.d.ts +68 -3
- package/dist/src/memory/verified-outcomes.js +88 -19
- package/dist/src/memory/verified-reverify.d.ts +63 -0
- package/dist/src/memory/verified-reverify.js +174 -0
- package/dist/src/missions/schemas.d.ts +4 -4
- package/dist/src/mods/apply.d.ts +2 -0
- package/dist/src/mods/apply.js +68 -8
- package/dist/src/mods/command-registry/schema.d.ts +36 -36
- package/dist/src/mods/compact-snapshots.d.ts +21 -0
- package/dist/src/mods/compact-snapshots.js +45 -0
- package/dist/src/mods/headless-check.d.ts +58 -0
- package/dist/src/mods/headless-check.js +137 -0
- package/dist/src/mods/headless-plugins.d.ts +44 -0
- package/dist/src/mods/headless-plugins.js +92 -0
- package/dist/src/mods/install-trust.d.ts +28 -0
- package/dist/src/mods/install-trust.js +127 -0
- package/dist/src/mods/install.d.ts +19 -0
- package/dist/src/mods/install.js +38 -1
- package/dist/src/mods/plugin-paths.d.ts +60 -0
- package/dist/src/mods/plugin-paths.js +131 -0
- package/dist/src/mods/plugin-repair.d.ts +8 -0
- package/dist/src/mods/plugin-repair.js +7 -0
- package/dist/src/plugins/marketplace-trust/scan.d.ts +23 -0
- package/dist/src/plugins/marketplace-trust/scan.js +255 -0
- package/dist/src/plugins/marketplace-trust/signature.d.ts +53 -0
- package/dist/src/plugins/marketplace-trust/signature.js +140 -0
- package/dist/src/plugins/marketplace-trust/verify-status.d.ts +42 -0
- package/dist/src/plugins/marketplace-trust/verify-status.js +110 -0
- package/dist/src/services/headless-worker-executor.d.ts +2 -0
- package/dist/src/services/headless-worker-executor.js +13 -3
- package/package.json +2 -1
- package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +8 -1
- package/plugins/ruflo-metaharness/CHANGELOG.md +6 -0
- package/plugins/ruflo-metaharness/README.md +14 -1
- package/plugins/ruflo-metaharness/agents/metaharness-architect.md +2 -0
- package/plugins/ruflo-metaharness/hooks/hooks.json +16 -2
- package/plugins/ruflo-metaharness/scripts/_redblue.mjs +12 -2
- package/plugins/ruflo-metaharness/scripts/_secret-env.cjs +50 -0
- package/plugins/ruflo-metaharness/scripts/_secret.mjs +35 -0
- package/plugins/ruflo-metaharness/scripts/smoke.sh +7 -7
- package/plugins/ruflo-metaharness/skills/harness-bench/SKILL.md +2 -1
- package/plugins/ruflo-metaharness/skills/harness-drift-from-history/SKILL.md +1 -1
- package/plugins/ruflo-metaharness/skills/harness-evolve/SKILL.md +2 -1
- package/plugins/ruflo-metaharness/skills/harness-gepa/SKILL.md +1 -1
- package/plugins/ruflo-metaharness/skills/harness-learn/SKILL.md +2 -1
- package/plugins/ruflo-metaharness/skills/harness-mint/SKILL.md +2 -1
- package/plugins/ruflo-metaharness/skills/harness-oia-audit/SKILL.md +1 -0
- package/plugins/ruflo-metaharness/skills/harness-score/SKILL.md +1 -1
- package/plugins/ruflo-metaharness/skills/harness-security-bench/SKILL.md +2 -1
- package/plugins/ruflo-metaharness/skills/harness-similarity/SKILL.md +1 -1
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
* AbortController, response truncation, default User-Agent.
|
|
12
12
|
*
|
|
13
13
|
* Architectural constraints (load-bearing):
|
|
14
|
-
* - DEFAULT-REFUSES private addresses + loopback (CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 opt-in)
|
|
14
|
+
* - DEFAULT-REFUSES private addresses + loopback (CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 opt-in):
|
|
15
|
+
* IP literals by text here, and every resolved address of a hostname at connect time, on the
|
|
16
|
+
* connection itself (http-fetch-pinned.ts), so DNS that points at, or rebinds to, a private
|
|
17
|
+
* address is refused too
|
|
15
18
|
* - DEFAULT-REFUSES Authorization / Cookie / X-Auth-* (CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1 opt-in)
|
|
16
19
|
* - hard timeout 30s default, 60s ceiling
|
|
17
20
|
* - response truncated to 256KB default, 1MB ceiling
|
|
@@ -19,6 +22,8 @@
|
|
|
19
22
|
*
|
|
20
23
|
* @module @claude-flow/cli/mcp-tools/http-fetch
|
|
21
24
|
*/
|
|
25
|
+
import { allowPrivateAddresses, isPrivateOrLoopback, pinnedRequest, PrivateAddressError } from './http-fetch-pinned.js';
|
|
26
|
+
export { isPrivateOrLoopback } from './http-fetch-pinned.js';
|
|
22
27
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
23
28
|
const MAX_TIMEOUT_MS = 60_000;
|
|
24
29
|
const DEFAULT_MAX_RESPONSE_BYTES = 256 * 1024; // 256 KB
|
|
@@ -54,75 +59,11 @@ export function validateUrl(rawUrl) {
|
|
|
54
59
|
throw new HttpFetchValidationError(`protocol ${parsed.protocol} not allowed (only http: and https:)`, 'FORBIDDEN_PROTOCOL');
|
|
55
60
|
}
|
|
56
61
|
const host = parsed.hostname.toLowerCase();
|
|
57
|
-
|
|
58
|
-
if (!allowPrivate && isPrivateOrLoopback(host)) {
|
|
62
|
+
if (!allowPrivateAddresses() && isPrivateOrLoopback(host)) {
|
|
59
63
|
throw new HttpFetchValidationError(`host ${host} is loopback/private/link-local; set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 to override`, 'PRIVATE_ADDRESS');
|
|
60
64
|
}
|
|
61
65
|
return parsed;
|
|
62
66
|
}
|
|
63
|
-
function isPrivateOrLoopback(host) {
|
|
64
|
-
if (host === 'localhost' || host === 'localhost.localdomain')
|
|
65
|
-
return true;
|
|
66
|
-
// Node's URL parser keeps brackets in `.hostname` for every IPv6 literal
|
|
67
|
-
// (e.g. "[::1]", "[fc00::1]", "[::ffff:7f00:1]"). Strip them once so every
|
|
68
|
-
// check below operates on the bare address — without this, the fc00::/7
|
|
69
|
-
// and fe80::/10 checks further down never match anything (they test
|
|
70
|
-
// `startsWith('fc'/'fe80:')` against a string starting with "["), and an
|
|
71
|
-
// IPv4-mapped-IPv6 literal (`::ffff:a.b.c.d`, which Node normalizes to a
|
|
72
|
-
// canonical `::ffff:xxxx:yyyy` hex form) sails through both the IPv4 and
|
|
73
|
-
// IPv6 branches untested — a live SSRF bypass (e.g. `::ffff:127.0.0.1` or
|
|
74
|
-
// `::ffff:169.254.169.254` reaching loopback / cloud-metadata targets).
|
|
75
|
-
const bare = host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
|
|
76
|
-
if (bare === '::1')
|
|
77
|
-
return true; // IPv6 loopback
|
|
78
|
-
// IPv4-mapped IPv6 — decode back to dotted-decimal and recurse into the
|
|
79
|
-
// IPv4 check below so it inherits every existing private/loopback/
|
|
80
|
-
// link-local/CGNAT rule instead of needing its own copy.
|
|
81
|
-
const mappedHex = bare.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
82
|
-
if (mappedHex) {
|
|
83
|
-
const hi = parseInt(mappedHex[1], 16);
|
|
84
|
-
const lo = parseInt(mappedHex[2], 16);
|
|
85
|
-
const decoded = `${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`;
|
|
86
|
-
return isPrivateOrLoopback(decoded);
|
|
87
|
-
}
|
|
88
|
-
const mappedDecimal = bare.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
89
|
-
if (mappedDecimal)
|
|
90
|
-
return isPrivateOrLoopback(mappedDecimal[1]);
|
|
91
|
-
// IPv4 numeric checks
|
|
92
|
-
const m = bare.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
93
|
-
if (m) {
|
|
94
|
-
const a = Number(m[1]);
|
|
95
|
-
const b = Number(m[2]);
|
|
96
|
-
if (a === 0)
|
|
97
|
-
return true; // 0.0.0.0/8
|
|
98
|
-
if (a === 127)
|
|
99
|
-
return true; // loopback
|
|
100
|
-
if (a === 10)
|
|
101
|
-
return true; // RFC1918
|
|
102
|
-
if (a === 172 && b >= 16 && b <= 31)
|
|
103
|
-
return true; // RFC1918
|
|
104
|
-
if (a === 192 && b === 168)
|
|
105
|
-
return true; // RFC1918
|
|
106
|
-
if (a === 169 && b === 254)
|
|
107
|
-
return true; // link-local
|
|
108
|
-
if (a === 100 && b >= 64 && b <= 127)
|
|
109
|
-
return true; // CGNAT
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
// IPv6 special forms — be conservative and accept only public-looking
|
|
113
|
-
// literals. Reject obvious private/local forms.
|
|
114
|
-
if (bare.startsWith('fc') || bare.startsWith('fd'))
|
|
115
|
-
return true; // fc00::/7 ULA
|
|
116
|
-
if (bare.startsWith('fe80:'))
|
|
117
|
-
return true; // link-local
|
|
118
|
-
// KNOWN LIMITATION (pre-existing, not introduced by the bracket-stripping /
|
|
119
|
-
// IPv4-mapped-IPv6 fix above; flagged by an independent adversarial review):
|
|
120
|
-
// `::` (IPv6 unspecified/"any", the analog of 0.0.0.0, which the IPv4 branch
|
|
121
|
-
// above does block), IPv6 multicast (ff00::/8), and NAT64 (64:ff9b::/96) are
|
|
122
|
-
// not covered. Left as a follow-up hardening item rather than scope-creeping
|
|
123
|
-
// this patch.
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
67
|
export function validateHeaders(headers) {
|
|
127
68
|
const allowAuth = process.env.CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH === '1';
|
|
128
69
|
const out = {};
|
|
@@ -156,7 +97,8 @@ function clampNumber(raw, defaultValue, max) {
|
|
|
156
97
|
* dispatcher. Returns a result object (does not throw on validation failure
|
|
157
98
|
* — it returns success: false with an errorCode).
|
|
158
99
|
*/
|
|
159
|
-
export async function httpFetchExecute(input) {
|
|
100
|
+
export async function httpFetchExecute(input, deps = {}) {
|
|
101
|
+
const send = deps.request ?? pinnedRequest;
|
|
160
102
|
const startedAt = Date.now();
|
|
161
103
|
const url = String(input.url ?? '');
|
|
162
104
|
const method = String(input.method ?? 'GET').toUpperCase();
|
|
@@ -242,12 +184,13 @@ export async function httpFetchExecute(input) {
|
|
|
242
184
|
// protocols after passing the allowlist. Every hop is validated with the
|
|
243
185
|
// same rules; sensitive headers are dropped when the origin changes.
|
|
244
186
|
while (true) {
|
|
245
|
-
|
|
187
|
+
// Never fetch(): it resolves the hostname again on its own, so what it connects to is
|
|
188
|
+
// not what was checked. `send` connects only to addresses its DNS lookup checked.
|
|
189
|
+
response = await send(currentUrl.toString(), {
|
|
246
190
|
method: currentMethod,
|
|
247
191
|
headers,
|
|
248
192
|
body: currentMethod === 'GET' || currentMethod === 'HEAD' ? undefined : currentBody,
|
|
249
193
|
signal: controller.signal,
|
|
250
|
-
redirect: 'manual',
|
|
251
194
|
});
|
|
252
195
|
if (!REDIRECT_STATUSES.has(response.status))
|
|
253
196
|
break;
|
|
@@ -378,6 +321,12 @@ export async function httpFetchExecute(input) {
|
|
|
378
321
|
}
|
|
379
322
|
catch (e) {
|
|
380
323
|
const msg = e instanceof Error ? e.message : String(e);
|
|
324
|
+
if (e instanceof PrivateAddressError) {
|
|
325
|
+
return {
|
|
326
|
+
success: false, status: 0, statusText: '', headers: {}, body: '', bodyTruncated: false, bytesRead: 0,
|
|
327
|
+
durationMs: Date.now() - startedAt, url: parsed.toString(), method, error: msg, errorCode: 'PRIVATE_ADDRESS',
|
|
328
|
+
};
|
|
329
|
+
}
|
|
381
330
|
const aborted = msg.includes('aborted') || e?.name === 'AbortError';
|
|
382
331
|
return {
|
|
383
332
|
success: false,
|
|
@@ -401,7 +350,7 @@ export async function httpFetchExecute(input) {
|
|
|
401
350
|
export const httpFetchTools = [
|
|
402
351
|
{
|
|
403
352
|
name: 'http_fetch',
|
|
404
|
-
description: 'ADR-164 §5.1.8 — HTTP probe primitive for business-pod ops benches (synthetic 200/500 endpoint checks, third-party status pages). Default-secure: blocks file://, ftp://, RFC-1918 / loopback / link-local hosts unless CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1, and rejects Authorization / Cookie / X-Auth-* headers unless CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1. Hard 30s timeout (60s ceiling), response truncated to 256 KB (1 MB ceiling), default User-Agent ruflo-http-fetch/1.0. Use when a pod or smoke contract needs a guarded HTTP probe — calling Node fetch() directly is wrong because it skips the URL allowlist and header sanitization that ADR-164 mandates for autopilot mode. Pair with the ops-pod bench in plugins/ruflo-business-pods/templates/ops.json (the §4.4 synthetic-endpoint test).',
|
|
353
|
+
description: 'ADR-164 §5.1.8 — HTTP probe primitive for business-pod ops benches (synthetic 200/500 endpoint checks, third-party status pages). Default-secure: blocks file://, ftp://, and RFC-1918 / loopback / link-local hosts, whether given as an address or as a name that resolves to one (checked on the connection itself, every redirect hop included), unless CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1, and rejects Authorization / Cookie / X-Auth-* headers unless CLAUDE_FLOW_HTTP_FETCH_ALLOW_AUTH=1. Hard 30s timeout (60s ceiling), response truncated to 256 KB (1 MB ceiling), default User-Agent ruflo-http-fetch/1.0. Use when a pod or smoke contract needs a guarded HTTP probe — calling Node fetch() directly is wrong because it skips the URL allowlist and header sanitization that ADR-164 mandates for autopilot mode. Pair with the ops-pod bench in plugins/ruflo-business-pods/templates/ops.json (the §4.4 synthetic-endpoint test).',
|
|
405
354
|
category: 'business-pods',
|
|
406
355
|
tags: ['business-pods', 'http', 'fetch', 'adr-164', 'security', 'ops-pod'],
|
|
407
356
|
inputSchema: {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gaming defences for the verified-outcome learning loop (ADR-503).
|
|
3
|
+
*
|
|
4
|
+
* Every rule here can only downgrade: a tainted record does not train, a
|
|
5
|
+
* pattern that looks gamed is held back from the prompt. Nothing here can
|
|
6
|
+
* turn an `unknown` into a verified outcome.
|
|
7
|
+
*
|
|
8
|
+
* - §2.1/§2.2 taints: the mod marks a `verified-success` whose turn edited a
|
|
9
|
+
* test file or a verifier config, or narrowed its check. Ingest never
|
|
10
|
+
* trains a tainted record unless `.claude-flow/config.json`
|
|
11
|
+
* `learningLoop.allowTaint` lists every one of its classes.
|
|
12
|
+
* - §2.3 re-verify settings (the runner lives in verified-reverify.ts).
|
|
13
|
+
* - §2.4 the anomaly gate on lessons.
|
|
14
|
+
*/
|
|
15
|
+
export type Taint = 'edited-tests' | 'edited-verifier-config' | 'narrowed-verifier';
|
|
16
|
+
export declare const TAINTS: readonly Taint[];
|
|
17
|
+
/** A record's `taint` field, validated: known classes only, each once. Anything else is dropped. */
|
|
18
|
+
export declare function parseTaint(raw: unknown): Taint[];
|
|
19
|
+
export interface LoopConfig {
|
|
20
|
+
/** Taint classes a project lets train (default none). */
|
|
21
|
+
readonly allowTaint: readonly Taint[];
|
|
22
|
+
/** Re-run a verifier at HEAD before a success trains (default off: it runs project code). */
|
|
23
|
+
readonly reverify: boolean;
|
|
24
|
+
readonly reverifyTimeoutMs: number;
|
|
25
|
+
}
|
|
26
|
+
export declare const DEFAULT_REVERIFY_TIMEOUT_MS = 300000;
|
|
27
|
+
export declare const DEFAULT_LOOP_CONFIG: LoopConfig;
|
|
28
|
+
/**
|
|
29
|
+
* `learningLoop` from `.claude-flow/config.json`. A missing, unreadable or
|
|
30
|
+
* malformed file means the defaults: no taint allowed, re-verify off. Only
|
|
31
|
+
* `reverify: true` (the boolean) turns re-verification on.
|
|
32
|
+
*/
|
|
33
|
+
export declare function readLoopConfig(root: string): LoopConfig;
|
|
34
|
+
export declare function parseLoopConfig(raw: unknown): LoopConfig;
|
|
35
|
+
/** The taint classes of a record that its project did not allow; non-empty means it never trains. */
|
|
36
|
+
export declare const blockedTaints: (taint: readonly Taint[], config: LoopConfig) => Taint[];
|
|
37
|
+
/**
|
|
38
|
+
* What ingest knows about a verified pattern, from verified records only
|
|
39
|
+
* (SONA's own counts include other feeders). Sessions are capped: the gate
|
|
40
|
+
* only asks whether there were two.
|
|
41
|
+
*/
|
|
42
|
+
export interface PatternStats {
|
|
43
|
+
successes: number;
|
|
44
|
+
failures: number;
|
|
45
|
+
/** Successes that carried an allowed taint. */
|
|
46
|
+
tainted: number;
|
|
47
|
+
/** Successes decided by a narrowed command. */
|
|
48
|
+
narrowed: number;
|
|
49
|
+
sessions: string[];
|
|
50
|
+
}
|
|
51
|
+
export declare const MAX_PATTERN_SESSIONS = 4;
|
|
52
|
+
export declare const emptyStats: () => PatternStats;
|
|
53
|
+
export declare function parseStats(raw: unknown): PatternStats;
|
|
54
|
+
/** Folds one trained record into its pattern's stats. */
|
|
55
|
+
export declare function addOutcome(stats: PatternStats, success: boolean, taint: readonly Taint[], session: string | null): void;
|
|
56
|
+
export type HoldReason = 'perfect-and-tainted' | 'only-narrowed' | 'one-session';
|
|
57
|
+
/** §2.4 case 1: records at or above which a perfect record with half its successes tainted is held. */
|
|
58
|
+
export declare const PERFECT_MIN_RECORDS = 8;
|
|
59
|
+
/**
|
|
60
|
+
* Why a would-be lesson is held back from the prompt (ADR-503 §2.4), or null.
|
|
61
|
+
* A held pattern stays in SONA; it only never becomes a prompt lesson.
|
|
62
|
+
*
|
|
63
|
+
* 1. 100% success over 8 or more records, half or more of them tainted (allowed).
|
|
64
|
+
* 2. Every success came from a narrowed command.
|
|
65
|
+
* 3. Every success came from one session: no independent repetition. A pattern
|
|
66
|
+
* with no recorded sessions (a record from before ADR-503, or one without a
|
|
67
|
+
* session) counts as one session.
|
|
68
|
+
*/
|
|
69
|
+
export declare function holdReason(stats: PatternStats, sonaSuccesses: number): HoldReason | null;
|
|
70
|
+
//# sourceMappingURL=verified-guards.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gaming defences for the verified-outcome learning loop (ADR-503).
|
|
3
|
+
*
|
|
4
|
+
* Every rule here can only downgrade: a tainted record does not train, a
|
|
5
|
+
* pattern that looks gamed is held back from the prompt. Nothing here can
|
|
6
|
+
* turn an `unknown` into a verified outcome.
|
|
7
|
+
*
|
|
8
|
+
* - §2.1/§2.2 taints: the mod marks a `verified-success` whose turn edited a
|
|
9
|
+
* test file or a verifier config, or narrowed its check. Ingest never
|
|
10
|
+
* trains a tainted record unless `.claude-flow/config.json`
|
|
11
|
+
* `learningLoop.allowTaint` lists every one of its classes.
|
|
12
|
+
* - §2.3 re-verify settings (the runner lives in verified-reverify.ts).
|
|
13
|
+
* - §2.4 the anomaly gate on lessons.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
export const TAINTS = ['edited-tests', 'edited-verifier-config', 'narrowed-verifier'];
|
|
18
|
+
/** A record's `taint` field, validated: known classes only, each once. Anything else is dropped. */
|
|
19
|
+
export function parseTaint(raw) {
|
|
20
|
+
if (!Array.isArray(raw))
|
|
21
|
+
return [];
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const t of raw.slice(0, 8))
|
|
24
|
+
if (TAINTS.includes(t) && !out.includes(t))
|
|
25
|
+
out.push(t);
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
export const DEFAULT_REVERIFY_TIMEOUT_MS = 300_000;
|
|
29
|
+
export const DEFAULT_LOOP_CONFIG = { allowTaint: [], reverify: false, reverifyTimeoutMs: DEFAULT_REVERIFY_TIMEOUT_MS };
|
|
30
|
+
/**
|
|
31
|
+
* `learningLoop` from `.claude-flow/config.json`. A missing, unreadable or
|
|
32
|
+
* malformed file means the defaults: no taint allowed, re-verify off. Only
|
|
33
|
+
* `reverify: true` (the boolean) turns re-verification on.
|
|
34
|
+
*/
|
|
35
|
+
export function readLoopConfig(root) {
|
|
36
|
+
const path = join(root, '.claude-flow', 'config.json');
|
|
37
|
+
try {
|
|
38
|
+
if (!existsSync(path))
|
|
39
|
+
return DEFAULT_LOOP_CONFIG;
|
|
40
|
+
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
41
|
+
return parseLoopConfig(raw?.learningLoop);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return DEFAULT_LOOP_CONFIG;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function parseLoopConfig(raw) {
|
|
48
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
49
|
+
return DEFAULT_LOOP_CONFIG;
|
|
50
|
+
const r = raw;
|
|
51
|
+
const sec = typeof r.reverifyTimeoutSec === 'number' && Number.isFinite(r.reverifyTimeoutSec) ? Math.min(Math.max(Math.floor(r.reverifyTimeoutSec), 10), 3600) : DEFAULT_REVERIFY_TIMEOUT_MS / 1000;
|
|
52
|
+
return { allowTaint: parseTaint(r.allowTaint), reverify: r.reverify === true, reverifyTimeoutMs: sec * 1000 };
|
|
53
|
+
}
|
|
54
|
+
/** The taint classes of a record that its project did not allow; non-empty means it never trains. */
|
|
55
|
+
export const blockedTaints = (taint, config) => taint.filter(t => !config.allowTaint.includes(t));
|
|
56
|
+
export const MAX_PATTERN_SESSIONS = 4;
|
|
57
|
+
const SESSION = /^[A-Za-z0-9_-]{1,40}$/;
|
|
58
|
+
const count = (v) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? Math.min(Math.floor(v), 1_000_000) : 0);
|
|
59
|
+
export const emptyStats = () => ({ successes: 0, failures: 0, tainted: 0, narrowed: 0, sessions: [] });
|
|
60
|
+
export function parseStats(raw) {
|
|
61
|
+
const r = (raw && typeof raw === 'object' ? raw : {});
|
|
62
|
+
const sessions = Array.isArray(r.sessions) ? [...new Set(r.sessions.filter((s) => typeof s === 'string' && SESSION.test(s)))].slice(0, MAX_PATTERN_SESSIONS) : [];
|
|
63
|
+
return { successes: count(r.successes), failures: count(r.failures), tainted: count(r.tainted), narrowed: count(r.narrowed), sessions };
|
|
64
|
+
}
|
|
65
|
+
/** Folds one trained record into its pattern's stats. */
|
|
66
|
+
export function addOutcome(stats, success, taint, session) {
|
|
67
|
+
if (!success) {
|
|
68
|
+
stats.failures++;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
stats.successes++;
|
|
72
|
+
if (taint.length > 0)
|
|
73
|
+
stats.tainted++;
|
|
74
|
+
if (taint.includes('narrowed-verifier'))
|
|
75
|
+
stats.narrowed++;
|
|
76
|
+
if (session && SESSION.test(session) && !stats.sessions.includes(session) && stats.sessions.length < MAX_PATTERN_SESSIONS)
|
|
77
|
+
stats.sessions.push(session);
|
|
78
|
+
}
|
|
79
|
+
/** §2.4 case 1: records at or above which a perfect record with half its successes tainted is held. */
|
|
80
|
+
export const PERFECT_MIN_RECORDS = 8;
|
|
81
|
+
/**
|
|
82
|
+
* Why a would-be lesson is held back from the prompt (ADR-503 §2.4), or null.
|
|
83
|
+
* A held pattern stays in SONA; it only never becomes a prompt lesson.
|
|
84
|
+
*
|
|
85
|
+
* 1. 100% success over 8 or more records, half or more of them tainted (allowed).
|
|
86
|
+
* 2. Every success came from a narrowed command.
|
|
87
|
+
* 3. Every success came from one session: no independent repetition. A pattern
|
|
88
|
+
* with no recorded sessions (a record from before ADR-503, or one without a
|
|
89
|
+
* session) counts as one session.
|
|
90
|
+
*/
|
|
91
|
+
export function holdReason(stats, sonaSuccesses) {
|
|
92
|
+
const s = stats.successes;
|
|
93
|
+
if (s >= PERFECT_MIN_RECORDS && stats.failures === 0 && stats.tainted * 2 >= s)
|
|
94
|
+
return 'perfect-and-tainted';
|
|
95
|
+
if (s > 0 && stats.narrowed === s)
|
|
96
|
+
return 'only-narrowed';
|
|
97
|
+
if ((s > 0 || sonaSuccesses > 0) && stats.sessions.length < 2)
|
|
98
|
+
return 'one-session';
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=verified-guards.js.map
|
|
@@ -19,7 +19,15 @@
|
|
|
19
19
|
* verified outcomes produced, those at or above the SONA routing gate as
|
|
20
20
|
* lessons, and repeated verified failures as cautions. The mod reads that
|
|
21
21
|
* file and adds at most three cited lessons to a prompt.
|
|
22
|
+
*
|
|
23
|
+
* ADR-503 hardens it against gamed signals (verified-guards.ts,
|
|
24
|
+
* verified-reverify.ts): a tainted success does not train unless the project
|
|
25
|
+
* allows its taint class, an opt-in re-verify can downgrade a success before
|
|
26
|
+
* it trains, and an anomaly gate holds suspicious patterns back from the
|
|
27
|
+
* prompt. Every one of these can only downgrade.
|
|
22
28
|
*/
|
|
29
|
+
import { type HoldReason, type LoopConfig, type PatternStats, type Taint } from './verified-guards.js';
|
|
30
|
+
import { type ReverifyRunner } from './verified-reverify.js';
|
|
23
31
|
export declare const VERIFIED_PATH: string;
|
|
24
32
|
export declare const LEDGER_PATH: string;
|
|
25
33
|
export declare const LESSONS_PATH: string;
|
|
@@ -49,6 +57,10 @@ export interface VerifiedRecord {
|
|
|
49
57
|
readonly ok: boolean;
|
|
50
58
|
}>;
|
|
51
59
|
readonly edits: number;
|
|
60
|
+
/** ADR-503 §2.1: what the turn did to its own check; only a `verified-success` carries any. */
|
|
61
|
+
readonly taint: readonly Taint[];
|
|
62
|
+
/** The mod process that recorded it, when the mod wrote one (ADR-503 §2.4). */
|
|
63
|
+
readonly session: string | null;
|
|
52
64
|
readonly startedAt: number;
|
|
53
65
|
readonly endedAt: number;
|
|
54
66
|
}
|
|
@@ -62,13 +74,31 @@ export interface IngestLedger {
|
|
|
62
74
|
verifiedFailure: number;
|
|
63
75
|
unknown: number;
|
|
64
76
|
rejected: number;
|
|
77
|
+
tainted: number;
|
|
65
78
|
};
|
|
66
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Pattern keys verified outcomes trained, with the last verifier command that decided them (the lesson's evidence)
|
|
81
|
+
* and what the anomaly gate needs (ADR-503 §2.4).
|
|
82
|
+
*/
|
|
67
83
|
patterns: Record<string, {
|
|
68
84
|
evidence: string;
|
|
69
85
|
at: number;
|
|
86
|
+
stats: PatternStats;
|
|
70
87
|
}>;
|
|
88
|
+
/** ADR-503 §2.3: the rotation cursor and the latest re-verifications (which commit, which check, what happened). */
|
|
89
|
+
reverify: {
|
|
90
|
+
cursor: number;
|
|
91
|
+
log: ReverifyLogEntry[];
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export interface ReverifyLogEntry {
|
|
95
|
+
readonly id: string;
|
|
96
|
+
readonly check: string | null;
|
|
97
|
+
readonly commit: string | null;
|
|
98
|
+
readonly status: 'pass' | 'fail' | 'timeout' | 'error';
|
|
99
|
+
readonly at: number;
|
|
71
100
|
}
|
|
101
|
+
export declare const MAX_REVERIFY_LOG = 50;
|
|
72
102
|
export interface Lesson {
|
|
73
103
|
readonly id: string;
|
|
74
104
|
readonly kind: 'lesson' | 'caution';
|
|
@@ -86,19 +116,38 @@ export interface LessonsFile {
|
|
|
86
116
|
lessons: Lesson[];
|
|
87
117
|
/** Verified patterns still below the gate, for the cold-start progress line. */
|
|
88
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
|
+
}>;
|
|
89
124
|
}
|
|
90
125
|
export interface IngestResult {
|
|
126
|
+
/** `tainted`: successes whose taint the project did not allow, counted and never trained (ADR-503 §2.2). */
|
|
91
127
|
ingested: {
|
|
92
128
|
verifiedSuccess: number;
|
|
93
129
|
verifiedFailure: number;
|
|
94
130
|
unknown: number;
|
|
95
131
|
rejected: number;
|
|
132
|
+
tainted: number;
|
|
96
133
|
};
|
|
97
134
|
trained: number;
|
|
98
135
|
skipped: 'no-file' | 'unchanged' | 'too-large' | null;
|
|
99
136
|
lessons: number;
|
|
100
137
|
cautions: number;
|
|
101
138
|
belowGate: number;
|
|
139
|
+
held: LessonsFile['held'];
|
|
140
|
+
/**
|
|
141
|
+
* ADR-503 §2.3: checks run, successes that passed or were downgraded to unknown, and successes left for an explicit
|
|
142
|
+
* ingest because this caller may not run project code.
|
|
143
|
+
*/
|
|
144
|
+
reverify: {
|
|
145
|
+
enabled: boolean;
|
|
146
|
+
ran: number;
|
|
147
|
+
passed: number;
|
|
148
|
+
downgraded: number;
|
|
149
|
+
deferred: number;
|
|
150
|
+
};
|
|
102
151
|
totals: IngestLedger['totals'];
|
|
103
152
|
sonaAvailable: boolean;
|
|
104
153
|
}
|
|
@@ -137,8 +186,15 @@ export declare function emptyLedger(): IngestLedger;
|
|
|
137
186
|
export declare function readLedger(raw: string | null): IngestLedger;
|
|
138
187
|
/** A lesson's citation: a short hash of the SONA pattern key, so the key's own text never reaches a prompt. */
|
|
139
188
|
export declare const lessonId: (patternKey: string) => string;
|
|
140
|
-
/**
|
|
141
|
-
|
|
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;
|
|
142
198
|
export interface IngestOptions {
|
|
143
199
|
/** Project root; defaults to process.cwd(), where SONA keeps `.swarm/`. */
|
|
144
200
|
root?: string;
|
|
@@ -149,6 +205,15 @@ export interface IngestOptions {
|
|
|
149
205
|
/** SONA to train; defaults to the process's optimizer. Null means unavailable. */
|
|
150
206
|
sona?: SonaLike | null;
|
|
151
207
|
now?: number;
|
|
208
|
+
/** `learningLoop` settings; defaults to `.claude-flow/config.json` under `root` (ADR-503). */
|
|
209
|
+
config?: LoopConfig;
|
|
210
|
+
/**
|
|
211
|
+
* Whether this caller may run project code to re-verify (ADR-503 §2.3). Only an explicit ingest passes true; a hot
|
|
212
|
+
* caller (hooks_route) leaves successes un-ingested when re-verify is on, instead of blocking for the timeout.
|
|
213
|
+
*/
|
|
214
|
+
allowReverify?: boolean;
|
|
215
|
+
/** Re-verify runner; defaults to the detached-git-worktree runner. Tests inject one. */
|
|
216
|
+
reverifier?: ReverifyRunner;
|
|
152
217
|
}
|
|
153
218
|
export declare function ingestVerifiedOutcomes(options?: IngestOptions): Promise<IngestResult>;
|
|
154
219
|
/**
|