@claude-flow/cli 3.63.9 → 3.65.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 +2 -2
- package/catalog-manifest.json +3 -3
- 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 +28 -2
- 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 +45 -0
- 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.d.ts +1 -0
- package/dist/src/mcp-tools/hooks-tools.js +38 -0
- 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 +228 -0
- package/dist/src/memory/verified-outcomes.js +372 -0
- 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 +36 -36
- 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 +222 -222
- 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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signed plugin manifests for the ruflo marketplace (ADR-509 §2.1).
|
|
3
|
+
*
|
|
4
|
+
* At release, scripts/sign-plugins.mjs writes `<plugin>/.claude-plugin/signature.json`
|
|
5
|
+
* for every plugin listed in `.claude-plugin/marketplace.json`: a sorted list of
|
|
6
|
+
* every file's path and SHA-256, the plugin's version, and an Ed25519 signature
|
|
7
|
+
* over the canonical JSON of both. The key pair is the existing helpers signing
|
|
8
|
+
* key (ADR-174): the public half is RUFLO_HELPERS_PUBKEY, already pinned here.
|
|
9
|
+
*
|
|
10
|
+
* This module is the verifier. The canonical bytes MUST match the signer
|
|
11
|
+
* byte-for-byte; a round-trip test (signer script → this verifier) pins that.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash, verify as edVerify } from 'node:crypto';
|
|
14
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, readlinkSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { RUFLO_HELPERS_PUBKEY } from '../../init/helper-signing.js';
|
|
17
|
+
export const SIGNATURE_RELATIVE = '.claude-plugin/signature.json';
|
|
18
|
+
/** Never part of a plugin's signed content: VCS metadata and installed dependencies. */
|
|
19
|
+
const SKIP_DIRS = new Set(['.git', 'node_modules']);
|
|
20
|
+
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
21
|
+
/** Canonical bytes: fixed key order, files sorted by path. MUST match scripts/sign-plugins.mjs. */
|
|
22
|
+
export function canonicalPluginManifestBytes(m) {
|
|
23
|
+
const files = [...m.files]
|
|
24
|
+
.map((f) => ({ path: f.path, sha256: f.sha256 }))
|
|
25
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
26
|
+
return Buffer.from(JSON.stringify({ plugin: m.plugin, version: m.version, files }), 'utf-8');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Every file under a plugin root, as posix paths, excluding the signature
|
|
30
|
+
* itself. A symlink is an entry of its own (digest of `link:<target>`), never
|
|
31
|
+
* followed: following it would let a link swap in content the digest never saw.
|
|
32
|
+
*/
|
|
33
|
+
export function digestPluginDir(root) {
|
|
34
|
+
const out = [];
|
|
35
|
+
const walk = (rel) => {
|
|
36
|
+
const abs = rel ? join(root, rel) : root;
|
|
37
|
+
for (const name of readdirSync(abs).sort()) {
|
|
38
|
+
const childRel = rel ? `${rel}/${name}` : name;
|
|
39
|
+
if (childRel === SIGNATURE_RELATIVE)
|
|
40
|
+
continue;
|
|
41
|
+
const st = lstatSync(join(abs, name));
|
|
42
|
+
if (st.isSymbolicLink())
|
|
43
|
+
out.push({ path: childRel, sha256: sha256(`link:${readlinkSync(join(abs, name))}`) });
|
|
44
|
+
else if (st.isDirectory()) {
|
|
45
|
+
if (!SKIP_DIRS.has(name))
|
|
46
|
+
walk(childRel);
|
|
47
|
+
}
|
|
48
|
+
else if (st.isFile())
|
|
49
|
+
out.push({ path: childRel, sha256: sha256(readFileSync(join(abs, name))) });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
walk('');
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
function parseSigned(text) {
|
|
56
|
+
try {
|
|
57
|
+
const s = JSON.parse(text);
|
|
58
|
+
if (!s || s.algorithm !== 'ed25519' || typeof s.signature !== 'string' || !s.manifest)
|
|
59
|
+
return null;
|
|
60
|
+
const m = s.manifest;
|
|
61
|
+
if (typeof m.plugin !== 'string' || typeof m.version !== 'string' || !Array.isArray(m.files))
|
|
62
|
+
return null;
|
|
63
|
+
if (!m.files.every((f) => f && typeof f.path === 'string' && typeof f.sha256 === 'string'))
|
|
64
|
+
return null;
|
|
65
|
+
return s;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Signature check only: true when `signed.signature` verifies over its manifest. */
|
|
72
|
+
export function signatureValid(signed, pubkeyPem = RUFLO_HELPERS_PUBKEY) {
|
|
73
|
+
try {
|
|
74
|
+
return edVerify(null, canonicalPluginManifestBytes(signed.manifest), pubkeyPem, Buffer.from(signed.signature, 'base64'));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Check a plugin directory against its signature.json. `expectName` (the
|
|
82
|
+
* marketplace name) must match the signed plugin name: a valid signature
|
|
83
|
+
* copied from another plugin is a bad signature here, not a pass.
|
|
84
|
+
*/
|
|
85
|
+
export function verifyPluginDir(dir, expectName, pubkeyPem = RUFLO_HELPERS_PUBKEY) {
|
|
86
|
+
const base = { plugin: expectName, dir, result: 'unsigned', changed: [], missing: [], extra: [] };
|
|
87
|
+
const sigPath = join(dir, SIGNATURE_RELATIVE);
|
|
88
|
+
if (!existsSync(sigPath))
|
|
89
|
+
return base;
|
|
90
|
+
const signed = parseSigned(readFileSync(sigPath, 'utf-8'));
|
|
91
|
+
if (!signed || signed.manifest.plugin !== expectName || !signatureValid(signed, pubkeyPem)) {
|
|
92
|
+
return { ...base, result: 'bad-signature', version: signed?.manifest.version };
|
|
93
|
+
}
|
|
94
|
+
const signedFiles = new Map(signed.manifest.files.map((f) => [f.path, f.sha256]));
|
|
95
|
+
const onDisk = new Map(digestPluginDir(dir).map((f) => [f.path, f.sha256]));
|
|
96
|
+
const changed = [];
|
|
97
|
+
const missing = [];
|
|
98
|
+
const extra = [];
|
|
99
|
+
for (const [path, digest] of signedFiles) {
|
|
100
|
+
const now = onDisk.get(path);
|
|
101
|
+
if (now === undefined)
|
|
102
|
+
missing.push(path);
|
|
103
|
+
else if (now !== digest)
|
|
104
|
+
changed.push(path);
|
|
105
|
+
}
|
|
106
|
+
for (const path of onDisk.keys())
|
|
107
|
+
if (!signedFiles.has(path))
|
|
108
|
+
extra.push(path);
|
|
109
|
+
const clean = changed.length === 0 && missing.length === 0 && extra.length === 0;
|
|
110
|
+
return { ...base, result: clean ? 'verified' : 'modified', changed, missing, extra, version: signed.manifest.version };
|
|
111
|
+
}
|
|
112
|
+
/** The plugins a marketplace clone lists, each with its directory (its `source`, else plugins/<name>). */
|
|
113
|
+
export function listMarketplacePlugins(clone) {
|
|
114
|
+
const catalog = JSON.parse(readFileSync(join(clone, '.claude-plugin', 'marketplace.json'), 'utf-8'));
|
|
115
|
+
const listed = Array.isArray(catalog.plugins) ? catalog.plugins : [];
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const p of listed) {
|
|
118
|
+
if (!p || typeof p.name !== 'string' || !/^[A-Za-z0-9._-]{1,100}$/.test(p.name))
|
|
119
|
+
continue;
|
|
120
|
+
const source = typeof p.source === 'string' ? p.source : `./plugins/${p.name}`;
|
|
121
|
+
// A source outside the clone (absolute, `..`, or a remote object) is not this clone's to verify.
|
|
122
|
+
if (source.startsWith('/') || source.split(/[\\/]/).includes('..'))
|
|
123
|
+
continue;
|
|
124
|
+
out.push({ name: p.name, dir: join(clone, source) });
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
/** One line per verification, with the differing files named (capped). */
|
|
129
|
+
export function describeVerification(v, cap = 5) {
|
|
130
|
+
if (v.result !== 'modified')
|
|
131
|
+
return `${v.plugin}: ${v.result}`;
|
|
132
|
+
const parts = [
|
|
133
|
+
...v.changed.map((p) => `changed ${p}`),
|
|
134
|
+
...v.missing.map((p) => `missing ${p}`),
|
|
135
|
+
...v.extra.map((p) => `extra ${p}`),
|
|
136
|
+
];
|
|
137
|
+
const shown = parts.slice(0, cap).join(', ');
|
|
138
|
+
return `${v.plugin}: modified (${shown}${parts.length > cap ? `, … ${parts.length - cap} more` : ''})`;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=signature.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type PluginVerification, type VerifyResult } from './signature.js';
|
|
2
|
+
export declare const VERIFY_STATUS_RELATIVE: string;
|
|
3
|
+
export interface VerifyStatusEntry {
|
|
4
|
+
writtenAt: string;
|
|
5
|
+
clone: string;
|
|
6
|
+
/**
|
|
7
|
+
* Plugin name → the WORST result across its clone copy and every install-cache
|
|
8
|
+
* copy: Claude Code loads an installed plugin from the cache, so a clean clone
|
|
9
|
+
* next to a tampered cache must not read as verified.
|
|
10
|
+
*/
|
|
11
|
+
plugins: Record<string, VerifyResult>;
|
|
12
|
+
/** Plugin directory (normalized, see normalizeTrustPath) → that copy's own result: the mod looks up the dir it is loading. */
|
|
13
|
+
paths?: Record<string, VerifyResult>;
|
|
14
|
+
}
|
|
15
|
+
export interface VerifyStatusFile {
|
|
16
|
+
version: 1;
|
|
17
|
+
byConfigDir: Record<string, VerifyStatusEntry>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The one spelling of a path both sides key on (the CLI writing, the mod
|
|
21
|
+
* reading): forward slashes, no repeated or trailing slash, and a Windows
|
|
22
|
+
* drive path lower-cased (Windows paths are case-insensitive, and HOME /
|
|
23
|
+
* USERPROFILE / os.homedir() disagree on separators). Kept byte-identical to
|
|
24
|
+
* plugins/ruflo-mods/hooks/trust.ts normalizeTrustPath.
|
|
25
|
+
*/
|
|
26
|
+
export declare function normalizeTrustPath(p: string): string;
|
|
27
|
+
/** The worse of two results (bad-signature > modified > unsigned > verified). */
|
|
28
|
+
export declare const worseResult: (a: VerifyResult | undefined, b: VerifyResult) => VerifyResult;
|
|
29
|
+
export declare const verifyStatusPath: (home?: string) => string;
|
|
30
|
+
export declare function readVerifyStatus(home?: string): VerifyStatusFile;
|
|
31
|
+
/**
|
|
32
|
+
* Record the clone's results and the install-cache copies' (`cache`): per plugin
|
|
33
|
+
* the worst of all its copies, and per copy its own result keyed by directory.
|
|
34
|
+
*/
|
|
35
|
+
export declare function writeVerifyStatus(configDir: string, clone: string, results: readonly PluginVerification[], home?: string, cache?: readonly PluginVerification[]): string;
|
|
36
|
+
/** Verify every plugin a clone lists (or only `names`). A plugin not in the clone is skipped. */
|
|
37
|
+
export declare function verifyClone(clone: string, names?: readonly string[]): PluginVerification[];
|
|
38
|
+
/** Install-cache copies of ruflo plugins (installed_plugins.json), each verified as its own row. */
|
|
39
|
+
export declare function verifyInstallCache(configDir: string, marketplace: string, names?: readonly string[]): PluginVerification[];
|
|
40
|
+
/** The results that §2.1 makes enforceable under refuse-risky. */
|
|
41
|
+
export declare const isUntrusted: (r: VerifyResult) => r is "modified" | "bad-signature";
|
|
42
|
+
//# sourceMappingURL=verify-status.d.ts.map
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the ruflo marketplace verification lands (ADR-509 §2.1): `ruflo mods
|
|
3
|
+
* install`, `ruflo mods doctor` and `ruflo plugins verify` check the clone
|
|
4
|
+
* Claude Code loads from (and the install cache) and write the result to a
|
|
5
|
+
* status file that the ruflo-mods `plugin.register` gate reads under
|
|
6
|
+
* `modTrust: refuse-risky`.
|
|
7
|
+
*
|
|
8
|
+
* The file lives under the user's home (~/.claude-flow/mods/plugin-verify.json),
|
|
9
|
+
* not in a project: the clone is per Claude Code config dir, and a repository
|
|
10
|
+
* must not be able to ship a file that says its own tampered clone verified.
|
|
11
|
+
* It is keyed by config dir, so two config dirs never overwrite each other.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { listMarketplacePlugins, verifyPluginDir } from './signature.js';
|
|
17
|
+
export const VERIFY_STATUS_RELATIVE = join('.claude-flow', 'mods', 'plugin-verify.json');
|
|
18
|
+
/**
|
|
19
|
+
* The one spelling of a path both sides key on (the CLI writing, the mod
|
|
20
|
+
* reading): forward slashes, no repeated or trailing slash, and a Windows
|
|
21
|
+
* drive path lower-cased (Windows paths are case-insensitive, and HOME /
|
|
22
|
+
* USERPROFILE / os.homedir() disagree on separators). Kept byte-identical to
|
|
23
|
+
* plugins/ruflo-mods/hooks/trust.ts normalizeTrustPath.
|
|
24
|
+
*/
|
|
25
|
+
export function normalizeTrustPath(p) {
|
|
26
|
+
let out = p.replace(/\\/g, '/').replace(/\/{2,}/g, '/');
|
|
27
|
+
if (out.length > 1)
|
|
28
|
+
out = out.replace(/\/+$/, '');
|
|
29
|
+
return /^[A-Za-z]:(\/|$)/.test(out) ? out.toLowerCase() : out;
|
|
30
|
+
}
|
|
31
|
+
const RANK = { verified: 0, unsigned: 1, modified: 2, 'bad-signature': 3 };
|
|
32
|
+
/** The worse of two results (bad-signature > modified > unsigned > verified). */
|
|
33
|
+
export const worseResult = (a, b) => (a === undefined || RANK[b] > RANK[a] ? b : a);
|
|
34
|
+
export const verifyStatusPath = (home = homedir()) => join(home, VERIFY_STATUS_RELATIVE);
|
|
35
|
+
export function readVerifyStatus(home = homedir()) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(readFileSync(verifyStatusPath(home), 'utf-8'));
|
|
38
|
+
if (parsed && parsed.version === 1 && parsed.byConfigDir && typeof parsed.byConfigDir === 'object')
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// absent or unreadable: nothing verified yet
|
|
43
|
+
}
|
|
44
|
+
return { version: 1, byConfigDir: {} };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Record the clone's results and the install-cache copies' (`cache`): per plugin
|
|
48
|
+
* the worst of all its copies, and per copy its own result keyed by directory.
|
|
49
|
+
*/
|
|
50
|
+
export function writeVerifyStatus(configDir, clone, results, home = homedir(), cache = []) {
|
|
51
|
+
const file = readVerifyStatus(home);
|
|
52
|
+
const plugins = {};
|
|
53
|
+
const paths = {};
|
|
54
|
+
for (const r of [...results, ...cache]) {
|
|
55
|
+
plugins[r.plugin] = worseResult(plugins[r.plugin], r.result);
|
|
56
|
+
const key = normalizeTrustPath(r.dir);
|
|
57
|
+
paths[key] = worseResult(paths[key], r.result);
|
|
58
|
+
}
|
|
59
|
+
const key = normalizeTrustPath(configDir);
|
|
60
|
+
// An entry an older writer keyed with another spelling of the same dir is replaced, not left beside it.
|
|
61
|
+
for (const k of Object.keys(file.byConfigDir))
|
|
62
|
+
if (k !== key && normalizeTrustPath(k) === key)
|
|
63
|
+
delete file.byConfigDir[k];
|
|
64
|
+
file.byConfigDir[key] = { writtenAt: new Date().toISOString(), clone, plugins, paths };
|
|
65
|
+
const path = verifyStatusPath(home);
|
|
66
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
67
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
68
|
+
writeFileSync(tmp, JSON.stringify(file, null, 2) + '\n', 'utf-8');
|
|
69
|
+
renameSync(tmp, path);
|
|
70
|
+
return path;
|
|
71
|
+
}
|
|
72
|
+
/** Verify every plugin a clone lists (or only `names`). A plugin not in the clone is skipped. */
|
|
73
|
+
export function verifyClone(clone, names) {
|
|
74
|
+
const want = names && names.length ? new Set(names) : null;
|
|
75
|
+
return listMarketplacePlugins(clone)
|
|
76
|
+
.filter((p) => !want || want.has(p.name))
|
|
77
|
+
.filter((p) => existsSync(join(p.dir, '.claude-plugin', 'plugin.json')))
|
|
78
|
+
.map((p) => verifyPluginDir(p.dir, p.name));
|
|
79
|
+
}
|
|
80
|
+
/** Install-cache copies of ruflo plugins (installed_plugins.json), each verified as its own row. */
|
|
81
|
+
export function verifyInstallCache(configDir, marketplace, names) {
|
|
82
|
+
let installed;
|
|
83
|
+
try {
|
|
84
|
+
installed = JSON.parse(readFileSync(join(configDir, 'plugins', 'installed_plugins.json'), 'utf-8'));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
const plugins = installed?.plugins;
|
|
90
|
+
if (!plugins || typeof plugins !== 'object')
|
|
91
|
+
return [];
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const [id, entries] of Object.entries(plugins)) {
|
|
94
|
+
const at = id.lastIndexOf('@');
|
|
95
|
+
if (at <= 0 || id.slice(at + 1) !== marketplace)
|
|
96
|
+
continue;
|
|
97
|
+
const name = id.slice(0, at);
|
|
98
|
+
if (names && names.length && !names.includes(name))
|
|
99
|
+
continue;
|
|
100
|
+
for (const e of Array.isArray(entries) ? entries : []) {
|
|
101
|
+
const path = e?.installPath;
|
|
102
|
+
if (typeof path === 'string' && existsSync(join(path, '.claude-plugin', 'plugin.json')))
|
|
103
|
+
out.push(verifyPluginDir(path, name));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/** The results that §2.1 makes enforceable under refuse-risky. */
|
|
109
|
+
export const isUntrusted = (r) => r === 'modified' || r === 'bad-signature';
|
|
110
|
+
//# sourceMappingURL=verify-status.js.map
|
|
@@ -135,6 +135,8 @@ export interface HeadlessExecutionResult {
|
|
|
135
135
|
error?: string;
|
|
136
136
|
/** Execution ID for tracking */
|
|
137
137
|
executionId: string;
|
|
138
|
+
/** ADR-510 — plugins in the worker profile that could not be found; the worker ran without them. */
|
|
139
|
+
missingPlugins?: string[];
|
|
138
140
|
/**
|
|
139
141
|
* #2661 — true when the launch was skipped because the same job
|
|
140
142
|
* (repositoryId + HEAD + worker + config) succeeded within the freshness
|
|
@@ -25,6 +25,7 @@ import { join } from 'path';
|
|
|
25
25
|
import { getGlobalAiBudget, isQuotaErrorText } from './global-ai-budget.js';
|
|
26
26
|
import { resolveGitWorkspaceIdentity } from './git-workspace-identity.js';
|
|
27
27
|
import { getAiJobDedupRegistry, computeAiJobKey, hashWorkerConfig } from './ai-job-dedup.js';
|
|
28
|
+
import { withHeadlessPlugins } from '../mods/headless-plugins.js';
|
|
28
29
|
// ============================================
|
|
29
30
|
// Constants
|
|
30
31
|
// ============================================
|
|
@@ -777,6 +778,7 @@ export class HeadlessWorkerExecutor extends EventEmitter {
|
|
|
777
778
|
timestamp: new Date(),
|
|
778
779
|
executionId,
|
|
779
780
|
error: result.error,
|
|
781
|
+
...(result.missingPlugins ? { missingPlugins: result.missingPlugins } : {}),
|
|
780
782
|
};
|
|
781
783
|
// Log result
|
|
782
784
|
this.logExecution(executionId, 'result', JSON.stringify(executionResult, null, 2));
|
|
@@ -1011,7 +1013,10 @@ Analyze the above codebase context and provide your response following the forma
|
|
|
1011
1013
|
* Execute Claude Code in headless mode
|
|
1012
1014
|
*/
|
|
1013
1015
|
executeClaudeCode(prompt, options) {
|
|
1014
|
-
return new Promise((
|
|
1016
|
+
return new Promise((resolveRaw) => {
|
|
1017
|
+
// ADR-510: a plugin the worker profile wants but cannot find is recorded on the result.
|
|
1018
|
+
let missingPlugins = [];
|
|
1019
|
+
const resolve = (r) => resolveRaw(missingPlugins.length > 0 ? { ...r, missingPlugins } : r);
|
|
1015
1020
|
const env = {
|
|
1016
1021
|
...process.env,
|
|
1017
1022
|
CLAUDE_CODE_HEADLESS: 'true',
|
|
@@ -1057,9 +1062,14 @@ Analyze the above codebase context and provide your response following the forma
|
|
|
1057
1062
|
// and ALWAYS falls back to the raw text on any mismatch — a schema
|
|
1058
1063
|
// surprise from an older/newer `claude` CLI must degrade to exactly
|
|
1059
1064
|
// today's behavior (no usage captured), never break analysis output.
|
|
1060
|
-
|
|
1065
|
+
// ADR-510: `claude -p` on a fresh config loads no marketplace plugins, so
|
|
1066
|
+
// pass the worker profile (default ruflo-mods) as --plugin-dir and turn
|
|
1067
|
+
// function hooks on unless the caller set CLAUDE_CODE_ENABLE_FUNCTION_HOOKS.
|
|
1068
|
+
const staged = withHeadlessPlugins(['--print', '--output-format', 'json'], env, { projectRoot: this.projectRoot });
|
|
1069
|
+
missingPlugins = staged.missing;
|
|
1070
|
+
const child = spawn('claude', staged.argv, {
|
|
1061
1071
|
cwd: this.projectRoot,
|
|
1062
|
-
env,
|
|
1072
|
+
env: staged.env,
|
|
1063
1073
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1064
1074
|
windowsHide: true, // Prevent phantom console windows on Windows
|
|
1065
1075
|
detached: process.platform !== 'win32',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.65.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",
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
"build": "tsc",
|
|
87
87
|
"check:metaharness-pins": "node ../../../scripts/check-metaharness-pins.mjs",
|
|
88
88
|
"test": "vitest run",
|
|
89
|
+
"test:real-binary": "vitest run --config vitest.real-binary.config.ts",
|
|
89
90
|
"test:plugin-store": "npx tsx src/plugins/tests/standalone-test.ts",
|
|
90
91
|
"test:pattern-store": "npx tsx src/transfer/store/tests/standalone-test.ts",
|
|
91
92
|
"postinstall": "node ./scripts/postinstall.cjs",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruflo-metaharness",
|
|
3
3
|
"description": "MetaHarness integration for ruflo — surfaces score/genome/mint/mcp-scan/threat-model via skills; pairs with @metaharness/router (ADR-148/149) for cost-optimal model routing; honors ADR-150's architectural constraint that MetaHarness remains an optional augmentation, never a required runtime dependency. As a mod (ADR-445): confirms flywheel promotion, keeps secrets out of inputs, /metaharness-mod, and a status file the console shows",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "ruvnet",
|
|
7
7
|
"url": "https://github.com/ruvnet"
|
|
@@ -43,6 +43,13 @@
|
|
|
43
43
|
"on",
|
|
44
44
|
"off"
|
|
45
45
|
]
|
|
46
|
+
},
|
|
47
|
+
"openrouterApiKey": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"title": "OpenRouter API key",
|
|
50
|
+
"description": "Key for the real model judge in redblue runs (--mock-judge needs none). Kept in the credential store, never in settings; OPENROUTER_API_KEY still works when this is empty.",
|
|
51
|
+
"required": false,
|
|
52
|
+
"sensitive": true
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
55
|
}
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Newest first. One `## <version> — <date>` heading per version, then `feat:`, `fix:`, `breaking:` and `chore:` bullets (ADR-478). Built from git history; this is the whole history.
|
|
4
4
|
|
|
5
|
+
## 0.4.0 — 2026-10-29
|
|
6
|
+
- chore: operational skills are user-invoked only (`disable-model-invocation: true`) and long skill/command descriptions are cut to one sentence, so they cost less prompt on every turn (ADR-507, #4588). Every skill is still available as `/<skill>`.
|
|
7
|
+
|
|
8
|
+
## 0.3.0 — 2026-10-29
|
|
9
|
+
- feat: `openrouterApiKey` is a `userConfig` option with `sensitive: true` (ADR-506, #4587), read through `readSecret` before the `OPENROUTER_API_KEY` environment fallback.
|
|
10
|
+
|
|
5
11
|
## 0.2.6 — 2026-10-12
|
|
6
12
|
- fix: the shared text screen now catches `.env` and e-mailed credentials, no longer flags a model's token talk (`--max-tokens`, `/oauth/token`, "token usage"), and no longer flags values that only name where a secret lives (`./service-account.json`, `--set-secrets=NAME=NAME:latest`); every rule stays linear (#4094, thanks @proffesor-for-testing)
|
|
7
13
|
|
|
@@ -29,6 +29,19 @@ MetaHarness integration plugin for ruflo. Surfaces the upstream `metaharness` /
|
|
|
29
29
|
| `harness-learn` | `/harness-learn --host <h> --model <m> --slice <manifest> [--repo <checkout>] [--run]` | metaharness@0.3.0 / upstream ADR-235 — GEPA learning run; $0 dry-run default, `--run` to spend; needs a metaharness repo checkout |
|
|
30
30
|
| `harness-gepa` | `/harness-gepa --op genome\|validate\|render\|analyze [--path <genome.json>]` | darwin@0.8.0 GEPA library surface — genome load/validate/render + transcript failure analysis; `gepaOptimize` stays library-only |
|
|
31
31
|
|
|
32
|
+
## User-invoked skills (ADR-507)
|
|
33
|
+
|
|
34
|
+
These skills carry `disable-model-invocation: true`: Claude does not pick them on its own, and they cost nothing in the per-turn skill listing. Type them to run them. They run benchmarks, spend-gated learning or evolution runs, scheduled audits, or a one-shot scaffold.
|
|
35
|
+
|
|
36
|
+
| Skill | Run it with |
|
|
37
|
+
|---|---|
|
|
38
|
+
| harness-bench | `/ruflo-metaharness:harness-bench` |
|
|
39
|
+
| harness-evolve | `/ruflo-metaharness:harness-evolve` |
|
|
40
|
+
| harness-learn | `/ruflo-metaharness:harness-learn` |
|
|
41
|
+
| harness-mint | `/ruflo-metaharness:harness-mint` |
|
|
42
|
+
| harness-oia-audit | `/ruflo-metaharness:harness-oia-audit` |
|
|
43
|
+
| harness-security-bench | `/ruflo-metaharness:harness-security-bench` |
|
|
44
|
+
|
|
32
45
|
## Phase-0 baseline (ruflo itself, 2026-06-16)
|
|
33
46
|
|
|
34
47
|
```json
|
|
@@ -79,6 +92,6 @@ This plugin ships a function-hook mod (ADR-445, `hooks/register.ts`). It makes n
|
|
|
79
92
|
- **`/metaharness-mod`**: `status`, `recent`, `tools` (which of this plugin's tools are connected) and `scan <text>` (would the guard refuse it). Answered locally.
|
|
80
93
|
- **Status file**: `.claude-flow/metaharness-mod/status.json` (`{version: 1, updatedMs, guard, calls, total, blocked, recent}`, counters only, never tool input), written at session start and after every call to this plugin's tools.
|
|
81
94
|
|
|
82
|
-
Options (`userConfig`): `guard` (on).
|
|
95
|
+
Options (`userConfig`): `guard` (on); `openrouterApiKey` (sensitive, empty by default): the key for redblue's real model judge, kept in the credential store. A classic SessionStart hook exports it as `OPENROUTER_API_KEY` to the session's Bash commands when that variable is unset, so a redblue run started from a skill uses it; it never goes in argv. The `metaharness_redblue` MCP tool runs inside ruflo-core's MCP server, which does not see it: export `OPENROUTER_API_KEY` before starting Claude Code for that path (ADR-506).
|
|
83
96
|
|
|
84
97
|
Test it: `claude plugin validate plugins/ruflo-metaharness`, `claude plugin test plugins/ruflo-metaharness`, `bash plugins/ruflo-metaharness/scripts/smoke.sh`.
|
|
@@ -36,6 +36,8 @@ needs its own ADR.
|
|
|
36
36
|
| `harness-oia-audit` | Composite weekly audit worker (iter 7) | Cron-scheduled; bundles oia+threat+mcp into one timestamped record in `metaharness-audit` namespace |
|
|
37
37
|
| `harness-mint` | Scaffold a custom harness | User wants to fork; ALWAYS dry-run first, never write to project root |
|
|
38
38
|
|
|
39
|
+
`harness-oia-audit` and `harness-mint` are user-invoked (ADR-507), as are `harness-bench`, `harness-evolve`, `harness-learn` and `harness-security-bench`: the Skill tool refuses them. Tell the user which `/ruflo-metaharness:<skill>` to run rather than running it yourself.
|
|
40
|
+
|
|
39
41
|
## Tools (subprocess-only, no library imports)
|
|
40
42
|
|
|
41
43
|
- All skills shell out to the pinned `metaharness` / `harness` binaries (`metaharness@~0.4.1`, local install or one-time versioned cache — never `@latest`) via the `_harness.mjs` shared helper.
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "MetaHarness as a mod (ADR-445): a tighten-only guard on the metaharness tools (promotion needs an explicit confirm, no secrets in inputs), a status file the console reads, and /metaharness-mod.",
|
|
3
|
-
"
|
|
2
|
+
"description": "MetaHarness as a mod (ADR-445): a tighten-only guard on the metaharness tools (promotion needs an explicit confirm, no secrets in inputs), a status file the console reads, and /metaharness-mod. The classic SessionStart hook hands the sensitive option openrouterApiKey to the Bash tool as OPENROUTER_API_KEY through CLAUDE_ENV_FILE, only when that variable is not already set, so a redblue run started from a skill can use a key kept in the credential store (scripts/_secret-env.cjs, ADR-506).",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node -e \"require(require('path').join(process.env.CLAUDE_PLUGIN_ROOT,'scripts','_secret-env.cjs')).bridge({openrouterApiKey:'OPENROUTER_API_KEY'})\""
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"modules": [
|
|
16
|
+
"./register.ts"
|
|
17
|
+
]
|
|
4
18
|
}
|
|
@@ -54,7 +54,9 @@
|
|
|
54
54
|
// (no real creds, no live targets, no shell, no eval, no arbitrary network).
|
|
55
55
|
// This wrapper does NOT relax those — it only forwards argv with shell:false.
|
|
56
56
|
// `--mock-judge` is the $0 CI path; the real model judge gates on
|
|
57
|
-
// $OPENROUTER_API_KEY
|
|
57
|
+
// $OPENROUTER_API_KEY. The wrapper never puts a key in argv. When that
|
|
58
|
+
// variable is unset but the plugin option `openrouterApiKey` (sensitive,
|
|
59
|
+
// ADR-506) is, the child gets it as OPENROUTER_API_KEY in its environment.
|
|
58
60
|
|
|
59
61
|
import { spawnSync } from 'node:child_process';
|
|
60
62
|
import { realpathSync } from 'node:fs';
|
|
@@ -67,6 +69,7 @@ import {
|
|
|
67
69
|
makeDegradedEmitter,
|
|
68
70
|
resolvePackageBin,
|
|
69
71
|
} from './_invoke.mjs';
|
|
72
|
+
import { readSecret } from './_secret.mjs';
|
|
70
73
|
|
|
71
74
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
72
75
|
|
|
@@ -78,6 +81,13 @@ const REDBLUE_PIN = `${REDBLUE_PKG}@${REDBLUE_PIN_VERSION}`;
|
|
|
78
81
|
|
|
79
82
|
const REASON_PREFIX = 'metaharness-redblue';
|
|
80
83
|
|
|
84
|
+
/** The child environment, with OPENROUTER_API_KEY filled from the plugin option when the variable is unset (ADR-506). */
|
|
85
|
+
export function judgeEnv(env) {
|
|
86
|
+
if (env.OPENROUTER_API_KEY) return env;
|
|
87
|
+
const key = readSecret('openrouterApiKey', [], env);
|
|
88
|
+
return key ? { ...env, OPENROUTER_API_KEY: key } : env;
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
// Cache dir is versioned by the pin so bumping REDBLUE_PIN_VERSION
|
|
82
92
|
// invalidates stale installs (handled inside ensureCachedInstall). The
|
|
83
93
|
// resolved layout matches the pre-consolidation helper so existing caches
|
|
@@ -133,7 +143,7 @@ export function runRedblue(args, opts = {}) {
|
|
|
133
143
|
encoding: 'utf-8',
|
|
134
144
|
timeout: timeoutMs,
|
|
135
145
|
cwd: opts.cwd,
|
|
136
|
-
env: { ...process.env, ...(opts.env || {}) },
|
|
146
|
+
env: judgeEnv({ ...process.env, ...(opts.env || {}) }),
|
|
137
147
|
shell: false,
|
|
138
148
|
});
|
|
139
149
|
const durationMs = Date.now() - start;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Secret bridge (ADR-506): hand a sensitive `userConfig` option to the scripts a skill or command runs
|
|
2
|
+
// through the Bash tool. Copied verbatim into each plugin that needs it (a test keeps the copies identical).
|
|
3
|
+
//
|
|
4
|
+
// Claude Code gives a plugin's option only to that plugin's hooks, as CLAUDE_PLUGIN_OPTION_<KEY>; the Bash
|
|
5
|
+
// tool never sees it. A SessionStart hook, though, gets CLAUDE_ENV_FILE, and the lines it appends there are
|
|
6
|
+
// loaded into every later Bash tool command of the session. So the plugin's SessionStart hook calls
|
|
7
|
+
// bridge({ option: 'VARIABLE' }) and the script reads VARIABLE as it always did.
|
|
8
|
+
//
|
|
9
|
+
// Exposure: the value then sits in the session's environment file (under ~/.claude, owner-only) and in the
|
|
10
|
+
// environment of the session's Bash commands, the same as exporting it in a shell profile. What the option
|
|
11
|
+
// adds is masked entry and credential-store storage instead of a plain-text profile or .env file.
|
|
12
|
+
//
|
|
13
|
+
// Rules: a variable the user already exported wins and is never overwritten; the value is never printed,
|
|
14
|
+
// logged or put in argv; a value with a newline or NUL is refused; any failure is silent and never blocks
|
|
15
|
+
// the session.
|
|
16
|
+
'use strict'
|
|
17
|
+
const fs = require('fs')
|
|
18
|
+
|
|
19
|
+
const optionEnvName = (name) => `CLAUDE_PLUGIN_OPTION_${String(name).replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()}`
|
|
20
|
+
const VARIABLE = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
21
|
+
const quote = (value) => `'${value.replace(/'/g, `'\\''`)}'`
|
|
22
|
+
|
|
23
|
+
/** The env-file lines for the options that are set and whose variable is not. Never contains a value that was not asked for. */
|
|
24
|
+
function bridgeLines(map, env = process.env) {
|
|
25
|
+
const lines = []
|
|
26
|
+
for (const [option, variable] of Object.entries(map || {})) {
|
|
27
|
+
if (!VARIABLE.test(variable)) continue
|
|
28
|
+
if (typeof env[variable] === 'string' && env[variable].trim() !== '') continue
|
|
29
|
+
const value = env[optionEnvName(option)]
|
|
30
|
+
if (typeof value !== 'string' || value.trim() === '' || /[\n\r\0]/.test(value)) continue
|
|
31
|
+
lines.push(`export ${variable}=${quote(value)}`)
|
|
32
|
+
}
|
|
33
|
+
return lines
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Append the lines to CLAUDE_ENV_FILE (owner-only). Returns the variable NAMES written, never values. */
|
|
37
|
+
function bridge(map, env = process.env) {
|
|
38
|
+
try {
|
|
39
|
+
const file = env.CLAUDE_ENV_FILE
|
|
40
|
+
if (!file) return []
|
|
41
|
+
const lines = bridgeLines(map, env)
|
|
42
|
+
if (lines.length === 0) return []
|
|
43
|
+
fs.appendFileSync(file, lines.join('\n') + '\n', { mode: 0o600 })
|
|
44
|
+
return lines.map((line) => line.slice('export '.length, line.indexOf('=')))
|
|
45
|
+
} catch {
|
|
46
|
+
return []
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { bridge, bridgeLines, optionEnvName }
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// readSecret: one way to read a plugin credential (ADR-506). Copied verbatim into each plugin that
|
|
2
|
+
// needs it, since a plugin may not import from another plugin's folder; keep the copies identical
|
|
3
|
+
// (a test compares them).
|
|
4
|
+
//
|
|
5
|
+
// Order: CLAUDE_PLUGIN_OPTION_<NAME>, which Claude Code sets for a plugin's hooks when the
|
|
6
|
+
// `userConfig` option (declared with "sensitive": true, kept in the credential store) is filled in;
|
|
7
|
+
// then the environment variables the plugin always read, for standalone CLI use and backward
|
|
8
|
+
// compatibility. The value is never logged, echoed, or placed in an error message.
|
|
9
|
+
|
|
10
|
+
/** The environment variable Claude Code sets for a `userConfig` key (non-identifier characters to `_`, upper-cased). */
|
|
11
|
+
export const optionEnvName = (name) => `CLAUDE_PLUGIN_OPTION_${String(name).replace(/[^A-Za-z0-9_]/g, '_').toUpperCase()}`
|
|
12
|
+
|
|
13
|
+
/** The secret, or undefined when neither the option nor any fallback variable holds a non-empty value. Read at call time. */
|
|
14
|
+
export function readSecret(name, envFallbacks = [], env = process.env) {
|
|
15
|
+
for (const variable of [optionEnvName(name), ...envFallbacks]) {
|
|
16
|
+
const value = env[variable]
|
|
17
|
+
if (typeof value === 'string' && value.trim() !== '') return value
|
|
18
|
+
}
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Where the secret was found (a variable NAME, never the value), or undefined. */
|
|
23
|
+
export function secretSource(name, envFallbacks = [], env = process.env) {
|
|
24
|
+
for (const variable of [optionEnvName(name), ...envFallbacks]) {
|
|
25
|
+
const value = env[variable]
|
|
26
|
+
if (typeof value === 'string' && value.trim() !== '') return variable
|
|
27
|
+
}
|
|
28
|
+
return undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Error text for a missing secret: names the option and the variables, never a value. */
|
|
32
|
+
export function secretHint(name, envFallbacks = []) {
|
|
33
|
+
const vars = envFallbacks.length ? ` or export ${envFallbacks.join(' / ')} in your shell` : ''
|
|
34
|
+
return `no ${name} configured: set the plugin option "${name}" with /plugin (it is stored in the credential store)${vars}. Never paste a key into a prompt or a file.`
|
|
35
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Structural smoke test for ruflo-metaharness v0.
|
|
2
|
+
# Structural smoke test for ruflo-metaharness v0.4.0 (ADR-150 Phase 1).
|
|
3
3
|
set -u
|
|
4
4
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
5
|
PASS=0
|
|
@@ -32,10 +32,10 @@ else
|
|
|
32
32
|
bad "extraction-regex-rot: EXPECTED_TOOLS=$EXPECTED_TOOLS EXPECTED_SUBS=$EXPECTED_SUBS"
|
|
33
33
|
fi
|
|
34
34
|
|
|
35
|
-
step "1. plugin.json declares 0.
|
|
35
|
+
step "1. plugin.json declares 0.4.0 with adr-150 keywords"
|
|
36
36
|
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
|
37
|
-
if [[ "$v" != "0.
|
|
38
|
-
bad "expected 0.
|
|
37
|
+
if [[ "$v" != "0.4.0" ]]; then
|
|
38
|
+
bad "expected 0.4.0, got '$v'"
|
|
39
39
|
else
|
|
40
40
|
miss=""
|
|
41
41
|
for k in ruflo metaharness harness scorecard genome mcp-scan threat-model router adr-150 adr-148 adr-149 optional-dependency graceful-degradation subprocess phase-1-mvp; do
|
|
@@ -483,7 +483,7 @@ step "17z74a. plugin helper pins accept the CLI-declared ranges — no tool-time
|
|
|
483
483
|
miss=""
|
|
484
484
|
# 17z74 compares package.json files with each other; the helpers' own
|
|
485
485
|
# *_PIN_VERSION constants (what findLocalPackageDir accepts) were never
|
|
486
|
-
# compared with them and drifted (~0.
|
|
486
|
+
# compared with them and drifted (~0.4.0 / ~0.8.0 vs declared ^0.4.1 /
|
|
487
487
|
# ~0.10.2), so every metaharness_* call npm-installed an older release next
|
|
488
488
|
# to the one ruflo ships. Hermetic runtime gate: stub packages at the declared
|
|
489
489
|
# versions, published layout, npm/npx trap.
|
|
@@ -1287,7 +1287,7 @@ miss=""
|
|
|
1287
1287
|
DISP="$ROOT/../../v3/@claude-flow/cli/src/commands/metaharness.ts"
|
|
1288
1288
|
WRAPPER="$ROOT/../../v3/@claude-flow/cli/src/mcp-tools/metaharness-tools.ts"
|
|
1289
1289
|
# All 13 dispatchable subcommands present (iter-73 had 10; @metaharness/redblue
|
|
1290
|
-
# added redblue.mjs; metaharness@0.
|
|
1290
|
+
# added redblue.mjs; metaharness@0.4.0/darwin@0.8.0 added learn.mjs + gepa.mjs).
|
|
1291
1291
|
for sub in score genome mcp-scan threat-model oia-audit audit-list audit-trend similarity drift-from-history mint redblue learn gepa; do
|
|
1292
1292
|
grep -q "${sub}" "$DISP" 2>/dev/null || miss="$miss subcommand-${sub}-not-listed"
|
|
1293
1293
|
done
|
|
@@ -2199,7 +2199,7 @@ grep -q "result has 'degraded'" "$F" || miss="$miss no-degraded-assertion"
|
|
|
2199
2199
|
grep -q "result has 'exitCode'" "$F" || miss="$miss no-exitcode-assertion"
|
|
2200
2200
|
# All tool names enumerated (similarity iter 36, drift_from_history iter 54,
|
|
2201
2201
|
# ADR-153 added bench/evolve/security_bench, @metaharness/redblue added redblue,
|
|
2202
|
-
# metaharness@0.
|
|
2202
|
+
# metaharness@0.4.0/darwin@0.8.0 added learn + gepa, ADR-322 added flywheel)
|
|
2203
2203
|
for tool in metaharness_score metaharness_genome metaharness_mcp_scan metaharness_threat_model metaharness_oia_audit metaharness_audit_list metaharness_audit_trend metaharness_similarity metaharness_drift_from_history metaharness_bench metaharness_evolve metaharness_security_bench metaharness_redblue metaharness_learn metaharness_gepa metaharness_flywheel; do
|
|
2204
2204
|
grep -q "${tool}" "$F" || miss="$miss missing-${tool}"
|
|
2205
2205
|
done
|