@consensus-tools/consensus-tools 0.1.0 → 0.1.2
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/README.md +17 -14
- package/bin/consensus-tools.js +34 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +11 -6
- package/src/cli.ts +214 -109
- package/src/cliConfig.ts +97 -0
- package/src/initWizard.ts +236 -0
- package/src/standalone.ts +409 -0
- package/src/testing/consensusTestRunner.ts +251 -0
- /package/{LICENSE → LICENSE.txt} +0 -0
package/README.md
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
# consensus.tools
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
<picture>
|
|
5
|
-
<img src="https://raw.githubusercontent.com/consensus-tools/consensus-tools/main/assets/consensus-tools" alt="consensus-tools" width="500">
|
|
6
|
-
</picture>
|
|
7
|
-
</p>
|
|
3
|
+

|
|
8
4
|
|
|
9
5
|
**High-confidence decisions for agentic systems.**
|
|
10
6
|
Local-first. Incentive-aligned. Verifiable.
|
|
@@ -89,7 +85,7 @@ const job = await openclaw.consensus.jobs.post({
|
|
|
89
85
|
policy: "HIGHEST_CONFIDENCE_SINGLE",
|
|
90
86
|
reward: 8,
|
|
91
87
|
stake: 4,
|
|
92
|
-
|
|
88
|
+
expiresSeconds: 180
|
|
93
89
|
});
|
|
94
90
|
```
|
|
95
91
|
|
|
@@ -131,6 +127,7 @@ When you’re ready, point the same CLI at a hosted board:
|
|
|
131
127
|
export CONSENSUS_MODE=remote
|
|
132
128
|
export CONSENSUS_URL=https://api.consensus.tools
|
|
133
129
|
export CONSENSUS_BOARD_ID=board_abc123
|
|
130
|
+
export CONSENSUS_API_KEY_ENV=CONSENSUS_API_KEY
|
|
134
131
|
export CONSENSUS_API_KEY=...
|
|
135
132
|
```
|
|
136
133
|
|
|
@@ -143,12 +140,18 @@ Same guarantees.
|
|
|
143
140
|
## CLI
|
|
144
141
|
|
|
145
142
|
```sh
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
consensus
|
|
149
|
-
consensus
|
|
150
|
-
consensus
|
|
151
|
-
consensus
|
|
143
|
+
# Standalone CLI
|
|
144
|
+
npm i -g @consensus-tools/consensus-tools
|
|
145
|
+
consensus-tools init
|
|
146
|
+
consensus-tools board use local|remote
|
|
147
|
+
consensus-tools jobs post
|
|
148
|
+
consensus-tools submissions create <jobId>
|
|
149
|
+
consensus-tools votes cast <jobId>
|
|
150
|
+
consensus-tools resolve <jobId>
|
|
151
|
+
|
|
152
|
+
# OpenClaw plugin CLI
|
|
153
|
+
openclaw consensus init
|
|
154
|
+
openclaw consensus jobs list
|
|
152
155
|
```
|
|
153
156
|
|
|
154
157
|
The CLI generates .sh API templates so everything is scriptable and inspectable.
|
|
@@ -356,10 +359,10 @@ Build systems that deserve trust.
|
|
|
356
359
|
This plugin is packaged to work with `openclaw plugins install`:
|
|
357
360
|
|
|
358
361
|
```
|
|
359
|
-
openclaw plugins install @
|
|
362
|
+
openclaw plugins install @consensus-tools/consensus-tools
|
|
360
363
|
```
|
|
361
364
|
|
|
362
|
-
The package includes `openclaw.extensions` pointing at `./index.ts`, so OpenClaw will load it as a plugin. The interaction skill is kept separately under `extensions/consensus-
|
|
365
|
+
The package includes `openclaw.extensions` pointing at `./index.ts`, so OpenClaw will load it as a plugin. The interaction skill is kept separately under `extensions/consensus-interact/`.
|
|
363
366
|
|
|
364
367
|
## Configure
|
|
365
368
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
function nodeSupportsImportFlag() {
|
|
10
|
+
const [major, minor] = process.versions.node.split('.').map((n) => Number(n));
|
|
11
|
+
if (!Number.isFinite(major) || !Number.isFinite(minor)) return true;
|
|
12
|
+
// --import was added in Node 20.6.0 and 18.19.0.
|
|
13
|
+
if (major > 20) return true;
|
|
14
|
+
if (major === 20) return minor >= 6;
|
|
15
|
+
if (major === 19) return true;
|
|
16
|
+
if (major === 18) return minor >= 19;
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const tsxLoaderPath = require.resolve('tsx');
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = path.dirname(__filename);
|
|
23
|
+
const entry = path.join(__dirname, '..', 'src', 'standalone.ts');
|
|
24
|
+
|
|
25
|
+
const loaderSpecifier = pathToFileURL(tsxLoaderPath).href;
|
|
26
|
+
const nodeArgs = nodeSupportsImportFlag() ? ['--import', loaderSpecifier] : ['--loader', loaderSpecifier];
|
|
27
|
+
|
|
28
|
+
const result = spawnSync(process.execPath, [...nodeArgs, entry, ...process.argv.slice(2)], {
|
|
29
|
+
stdio: 'inherit',
|
|
30
|
+
env: process.env
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
process.exit(result.status ?? 1);
|
|
34
|
+
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@consensus-tools/consensus-tools",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
|
+
"bin": {
|
|
7
|
+
"consensus-tools": "bin/consensus-tools.js"
|
|
8
|
+
},
|
|
6
9
|
"openclaw": {
|
|
7
|
-
"extensions": [
|
|
10
|
+
"extensions": [
|
|
11
|
+
"./index.ts"
|
|
12
|
+
]
|
|
8
13
|
},
|
|
9
14
|
"scripts": {
|
|
10
|
-
"test": "
|
|
15
|
+
"test": "node --test --import tsx"
|
|
11
16
|
},
|
|
12
17
|
"files": [
|
|
18
|
+
"bin",
|
|
13
19
|
"index.ts",
|
|
14
20
|
"src",
|
|
15
21
|
"openclaw.plugin.json",
|
|
16
22
|
"README.md"
|
|
17
23
|
],
|
|
18
24
|
"dependencies": {
|
|
19
|
-
"ajv": "^8.12.0"
|
|
20
|
-
},
|
|
21
|
-
"devDependencies": {
|
|
25
|
+
"ajv": "^8.12.0",
|
|
22
26
|
"openai": "^4.79.0",
|
|
23
27
|
"tsx": "^4.19.2"
|
|
24
28
|
},
|
|
29
|
+
"devDependencies": {},
|
|
25
30
|
"optionalDependencies": {
|
|
26
31
|
"better-sqlite3": "^9.4.3"
|
|
27
32
|
}
|
package/src/cli.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import os from 'node:os';
|
|
4
3
|
import type { ConsensusToolsConfig, Job } from './types';
|
|
5
4
|
import { renderTable } from './util/table';
|
|
6
|
-
import { runConsensusPolicyTests } from '
|
|
5
|
+
import { runConsensusPolicyTests } from './testing/consensusTestRunner';
|
|
6
|
+
import { runInitWizard } from './initWizard';
|
|
7
|
+
import {
|
|
8
|
+
defaultConsensusCliConfig,
|
|
9
|
+
getConfigValue,
|
|
10
|
+
loadCliConfig,
|
|
11
|
+
parseValue,
|
|
12
|
+
saveCliConfig,
|
|
13
|
+
setConfigValue,
|
|
14
|
+
type ConsensusCliConfig
|
|
15
|
+
} from './cliConfig';
|
|
7
16
|
|
|
8
17
|
export interface ConsensusToolsBackendCli {
|
|
9
18
|
postJob(agentId: string, input: any): Promise<Job>;
|
|
@@ -17,15 +26,67 @@ export interface ConsensusToolsBackendCli {
|
|
|
17
26
|
resolveJob(agentId: string, jobId: string, input: any): Promise<any>;
|
|
18
27
|
}
|
|
19
28
|
|
|
29
|
+
export async function initRepo(opts: {
|
|
30
|
+
rootDir?: string;
|
|
31
|
+
force?: boolean;
|
|
32
|
+
wizard?: boolean;
|
|
33
|
+
templatesOnly?: boolean;
|
|
34
|
+
}): Promise<void> {
|
|
35
|
+
const rootDir = opts.rootDir || process.cwd();
|
|
36
|
+
const force = Boolean(opts.force);
|
|
37
|
+
const templatesOnly = Boolean(opts.templatesOnly);
|
|
38
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
39
|
+
const wizard = typeof opts.wizard === 'boolean' ? opts.wizard : interactive;
|
|
40
|
+
|
|
41
|
+
if (templatesOnly || !wizard) {
|
|
42
|
+
await writeInitTemplates(rootDir, force, defaultConsensusCliConfig);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!interactive) {
|
|
47
|
+
throw new Error('init wizard requires a TTY. Re-run with --templates-only.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = await runInitWizard(rootDir);
|
|
51
|
+
await writeInitTemplates(rootDir, force, result.config);
|
|
52
|
+
if (result.env) {
|
|
53
|
+
await writeEnvFile(rootDir, force, result.env);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
20
57
|
export function registerCli(program: any, backend: ConsensusToolsBackendCli, config: ConsensusToolsConfig, agentId: string) {
|
|
21
58
|
const consensus = program.command('consensus').description('Consensus tools');
|
|
59
|
+
registerConsensusSubcommands(consensus, backend, config, agentId);
|
|
60
|
+
}
|
|
22
61
|
|
|
62
|
+
export function registerStandaloneCli(
|
|
63
|
+
program: any,
|
|
64
|
+
backend: ConsensusToolsBackendCli,
|
|
65
|
+
config: ConsensusToolsConfig,
|
|
66
|
+
agentId: string
|
|
67
|
+
) {
|
|
68
|
+
registerConsensusSubcommands(program, backend, config, agentId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function registerConsensusSubcommands(
|
|
72
|
+
consensus: any,
|
|
73
|
+
backend: ConsensusToolsBackendCli,
|
|
74
|
+
_config: ConsensusToolsConfig,
|
|
75
|
+
agentId: string
|
|
76
|
+
) {
|
|
23
77
|
consensus
|
|
24
78
|
.command('init')
|
|
25
|
-
.description('
|
|
79
|
+
.description('Initialize consensus-tools in this repo (.consensus/)')
|
|
26
80
|
.option('--force', 'Overwrite existing files')
|
|
81
|
+
.option('--wizard', 'Run an interactive wizard (default when TTY)')
|
|
82
|
+
.option('--templates-only', 'Only generate templates; skip prompts')
|
|
27
83
|
.action(async (opts: any) => {
|
|
28
|
-
await
|
|
84
|
+
await initRepo({
|
|
85
|
+
rootDir: process.cwd(),
|
|
86
|
+
force: Boolean(opts.force),
|
|
87
|
+
wizard: typeof opts.wizard === 'boolean' ? opts.wizard : undefined,
|
|
88
|
+
templatesOnly: Boolean(opts.templatesOnly)
|
|
89
|
+
});
|
|
29
90
|
console.log('Created .consensus templates.');
|
|
30
91
|
});
|
|
31
92
|
|
|
@@ -34,7 +95,7 @@ export function registerCli(program: any, backend: ConsensusToolsBackendCli, con
|
|
|
34
95
|
.command('get <key>')
|
|
35
96
|
.description('Get a config value')
|
|
36
97
|
.action(async (key: string) => {
|
|
37
|
-
const cfg = await
|
|
98
|
+
const cfg = await loadCliConfig();
|
|
38
99
|
const value = getConfigValue(cfg, key);
|
|
39
100
|
output(value ?? null, true);
|
|
40
101
|
});
|
|
@@ -43,10 +104,10 @@ export function registerCli(program: any, backend: ConsensusToolsBackendCli, con
|
|
|
43
104
|
.command('set <key> <value>')
|
|
44
105
|
.description('Set a config value')
|
|
45
106
|
.action(async (key: string, value: string) => {
|
|
46
|
-
const cfg = await
|
|
107
|
+
const cfg = await loadCliConfig();
|
|
47
108
|
const parsed = parseValue(value);
|
|
48
109
|
setConfigValue(cfg, key, parsed);
|
|
49
|
-
await
|
|
110
|
+
await saveCliConfig(cfg);
|
|
50
111
|
output({ ok: true }, true);
|
|
51
112
|
});
|
|
52
113
|
|
|
@@ -55,7 +116,7 @@ export function registerCli(program: any, backend: ConsensusToolsBackendCli, con
|
|
|
55
116
|
.command('use <type> [url]')
|
|
56
117
|
.description('Select local or remote board')
|
|
57
118
|
.action(async (type: string, url?: string) => {
|
|
58
|
-
const cfg = await
|
|
119
|
+
const cfg = await loadCliConfig();
|
|
59
120
|
if (type !== 'local' && type !== 'remote') {
|
|
60
121
|
throw new Error('board type must be local or remote');
|
|
61
122
|
}
|
|
@@ -63,7 +124,7 @@ export function registerCli(program: any, backend: ConsensusToolsBackendCli, con
|
|
|
63
124
|
if (type === 'remote' && url) {
|
|
64
125
|
cfg.boards.remote.url = url;
|
|
65
126
|
}
|
|
66
|
-
await
|
|
127
|
+
await saveCliConfig(cfg);
|
|
67
128
|
output({ activeBoard: cfg.activeBoard, url: cfg.boards.remote.url }, true);
|
|
68
129
|
});
|
|
69
130
|
|
|
@@ -78,7 +139,7 @@ export function registerCli(program: any, backend: ConsensusToolsBackendCli, con
|
|
|
78
139
|
.option('--policy <policy>', 'Policy key')
|
|
79
140
|
.option('--reward <n>', 'Reward amount', parseFloat)
|
|
80
141
|
.option('--stake <n>', 'Stake amount', parseFloat)
|
|
81
|
-
.option('--
|
|
142
|
+
.option('--expires <seconds>', 'Expires seconds', parseInt)
|
|
82
143
|
.option('--json', 'JSON output')
|
|
83
144
|
.action(async (opts: any) => {
|
|
84
145
|
const input = opts.input ?? (await readStdinIfAny());
|
|
@@ -93,7 +154,7 @@ export function registerCli(program: any, backend: ConsensusToolsBackendCli, con
|
|
|
93
154
|
stakeAmount: opts.stake,
|
|
94
155
|
reward: opts.reward,
|
|
95
156
|
stakeRequired: opts.stake,
|
|
96
|
-
expiresSeconds: opts.
|
|
157
|
+
expiresSeconds: opts.expires
|
|
97
158
|
});
|
|
98
159
|
output(job, opts.json);
|
|
99
160
|
});
|
|
@@ -228,7 +289,7 @@ export function registerCli(program: any, backend: ConsensusToolsBackendCli, con
|
|
|
228
289
|
.command('run')
|
|
229
290
|
.description('Run consensus policy tests with generation script')
|
|
230
291
|
.option('--agents <n>', 'Number of agent personalities', parseInt)
|
|
231
|
-
.option('--script <path>', 'Path to generation script', '
|
|
292
|
+
.option('--script <path>', 'Path to generation script', '.consensus/generation.ts')
|
|
232
293
|
.option('--openai-key <key>', 'OpenAI API key (or set OPENAI_API_KEY)')
|
|
233
294
|
.option('--model <name>', 'Model name', 'gpt-5.2')
|
|
234
295
|
.action(async (opts: any) => {
|
|
@@ -270,89 +331,14 @@ async function readStdinIfAny(): Promise<string | undefined> {
|
|
|
270
331
|
return text || undefined;
|
|
271
332
|
}
|
|
272
333
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
};
|
|
279
|
-
defaults: { policy: string; reward: number; stake: number; leaseSeconds: number };
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
const defaultConsensusConfig: ConsensusConfig = {
|
|
283
|
-
activeBoard: 'local',
|
|
284
|
-
boards: {
|
|
285
|
-
local: {
|
|
286
|
-
type: 'local',
|
|
287
|
-
root: '~/.openclaw/workplace/consensus-board',
|
|
288
|
-
jobsPath: 'jobs',
|
|
289
|
-
ledgerPath: 'ledger.json'
|
|
290
|
-
},
|
|
291
|
-
remote: {
|
|
292
|
-
type: 'remote',
|
|
293
|
-
url: 'https://api.consensus.tools',
|
|
294
|
-
boardId: 'board_replace_me',
|
|
295
|
-
auth: { type: 'apiKey', apiKeyEnv: 'CONSENSUS_API_KEY' }
|
|
296
|
-
}
|
|
297
|
-
},
|
|
298
|
-
defaults: {
|
|
299
|
-
policy: 'HIGHEST_CONFIDENCE_SINGLE',
|
|
300
|
-
reward: 8,
|
|
301
|
-
stake: 4,
|
|
302
|
-
leaseSeconds: 180
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
function configPath(): string {
|
|
307
|
-
const envPath = process.env.CONSENSUS_CONFIG;
|
|
308
|
-
if (envPath) return expandHome(envPath);
|
|
309
|
-
return path.join(os.homedir(), '.consensus', 'config.json');
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function expandHome(input: string): string {
|
|
313
|
-
if (!input.startsWith('~')) return input;
|
|
314
|
-
return path.join(os.homedir(), input.slice(1));
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async function loadConfigFile(): Promise<ConsensusConfig> {
|
|
318
|
-
const filePath = configPath();
|
|
319
|
-
try {
|
|
320
|
-
const raw = await fs.readFile(filePath, 'utf8');
|
|
321
|
-
return JSON.parse(raw) as ConsensusConfig;
|
|
322
|
-
} catch {
|
|
323
|
-
return JSON.parse(JSON.stringify(defaultConsensusConfig)) as ConsensusConfig;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async function saveConfigFile(config: ConsensusConfig): Promise<void> {
|
|
328
|
-
const filePath = configPath();
|
|
329
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
330
|
-
await fs.writeFile(filePath, JSON.stringify(config, null, 2), 'utf8');
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function getConfigValue(config: any, key: string): any {
|
|
334
|
-
return key.split('.').reduce((acc, part) => (acc ? acc[part] : undefined), config);
|
|
334
|
+
async function writeEnvFile(rootDir: string, force: boolean, env: Record<string, string>): Promise<void> {
|
|
335
|
+
const filePath = path.join(rootDir, '.consensus', '.env');
|
|
336
|
+
const lines = Object.entries(env).map(([k, v]) => `export ${k}=${v}`);
|
|
337
|
+
const content = [...lines, ''].join('\n');
|
|
338
|
+
await writeFile(filePath, content, force);
|
|
335
339
|
}
|
|
336
340
|
|
|
337
|
-
function
|
|
338
|
-
const parts = key.split('.');
|
|
339
|
-
let cur = config as any;
|
|
340
|
-
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
341
|
-
if (!cur[parts[i]]) cur[parts[i]] = {};
|
|
342
|
-
cur = cur[parts[i]];
|
|
343
|
-
}
|
|
344
|
-
cur[parts[parts.length - 1]] = value;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function parseValue(input: string): any {
|
|
348
|
-
try {
|
|
349
|
-
return JSON.parse(input);
|
|
350
|
-
} catch {
|
|
351
|
-
return input;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
async function writeInitTemplates(rootDir: string, force: boolean): Promise<void> {
|
|
341
|
+
async function writeInitTemplates(rootDir: string, force: boolean, config: ConsensusCliConfig): Promise<void> {
|
|
356
342
|
const baseDir = path.join(rootDir, '.consensus');
|
|
357
343
|
const apiDir = path.join(baseDir, 'api');
|
|
358
344
|
|
|
@@ -360,8 +346,10 @@ async function writeInitTemplates(rootDir: string, force: boolean): Promise<void
|
|
|
360
346
|
|
|
361
347
|
const files: Array<{ path: string; content: string; executable?: boolean }> = [
|
|
362
348
|
{ path: path.join(baseDir, 'README.md'), content: consensusReadme() },
|
|
363
|
-
{ path: path.join(baseDir, 'env.example'), content: envExample() },
|
|
364
|
-
{ path: path.join(baseDir, '
|
|
349
|
+
{ path: path.join(baseDir, 'env.example'), content: envExample(config) },
|
|
350
|
+
{ path: path.join(baseDir, '.gitignore'), content: ['.env', ''].join('\n') },
|
|
351
|
+
{ path: path.join(baseDir, 'config.json'), content: JSON.stringify(config, null, 2) },
|
|
352
|
+
{ path: path.join(baseDir, 'generation.ts'), content: generationScriptTemplate() },
|
|
365
353
|
{ path: path.join(apiDir, 'common.sh'), content: commonSh(), executable: true },
|
|
366
354
|
{ path: path.join(apiDir, 'jobs_post.sh'), content: jobsPostSh(), executable: true },
|
|
367
355
|
{ path: path.join(apiDir, 'jobs_get.sh'), content: jobsGetSh(), executable: true },
|
|
@@ -397,7 +385,7 @@ function consensusReadme(): string {
|
|
|
397
385
|
return [
|
|
398
386
|
'# consensus.tools shell templates',
|
|
399
387
|
'',
|
|
400
|
-
'This folder is generated by `consensus init
|
|
388
|
+
'This folder is generated by `consensus-tools init` (or `openclaw consensus init`).',
|
|
401
389
|
'',
|
|
402
390
|
'## Quick start',
|
|
403
391
|
'',
|
|
@@ -426,6 +414,13 @@ function consensusReadme(): string {
|
|
|
426
414
|
'bash .consensus/api/jobs_post.sh "Title" "Desc" "Input"',
|
|
427
415
|
'```',
|
|
428
416
|
'',
|
|
417
|
+
'Try the CLI:',
|
|
418
|
+
'',
|
|
419
|
+
'```bash',
|
|
420
|
+
'consensus-tools jobs list',
|
|
421
|
+
'consensus-tools jobs post --title "Hello" --desc "World" --input "Test"',
|
|
422
|
+
'```',
|
|
423
|
+
'',
|
|
429
424
|
'Notes',
|
|
430
425
|
'',
|
|
431
426
|
'Local mode writes to CONSENSUS_ROOT (defaults in env.example).',
|
|
@@ -437,24 +432,129 @@ function consensusReadme(): string {
|
|
|
437
432
|
].join('\n');
|
|
438
433
|
}
|
|
439
434
|
|
|
440
|
-
function envExample(): string {
|
|
435
|
+
function envExample(config: ConsensusCliConfig): string {
|
|
436
|
+
const apiKeyEnv = config.boards.remote.auth.apiKeyEnv || 'CONSENSUS_API_KEY';
|
|
441
437
|
return [
|
|
442
438
|
'# Mode: "local" or "remote"',
|
|
443
|
-
|
|
439
|
+
`export CONSENSUS_MODE=${config.activeBoard === 'remote' ? 'remote' : 'local'}`,
|
|
440
|
+
'',
|
|
441
|
+
'# Agent id (used by the CLI; optional)',
|
|
442
|
+
'export CONSENSUS_AGENT_ID="cli@your-machine"',
|
|
444
443
|
'',
|
|
445
444
|
'# Local board root (JSON filesystem board)',
|
|
446
|
-
|
|
445
|
+
`export CONSENSUS_ROOT="${config.boards.local.root}"`,
|
|
447
446
|
'',
|
|
448
447
|
'# Remote board settings',
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
448
|
+
`export CONSENSUS_URL="${config.boards.remote.url}"`,
|
|
449
|
+
`export CONSENSUS_BOARD_ID="${config.boards.remote.boardId}"`,
|
|
450
|
+
`export CONSENSUS_API_KEY_ENV="${apiKeyEnv}"`,
|
|
451
|
+
`export ${apiKeyEnv}="replace_me"`,
|
|
452
452
|
'',
|
|
453
453
|
'# Defaults (used by jobs_post.sh if not provided)',
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
454
|
+
`export CONSENSUS_DEFAULT_POLICY="${config.defaults.policy}"`,
|
|
455
|
+
`export CONSENSUS_DEFAULT_REWARD="${config.defaults.reward}"`,
|
|
456
|
+
`export CONSENSUS_DEFAULT_STAKE="${config.defaults.stake}"`,
|
|
457
|
+
`export CONSENSUS_DEFAULT_LEASE_SECONDS="${config.defaults.leaseSeconds}"`,
|
|
458
|
+
''
|
|
459
|
+
].join('\n');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function generationScriptTemplate(): string {
|
|
463
|
+
return [
|
|
464
|
+
'// Generated by consensus-tools init.',
|
|
465
|
+
'// Customize this file, then run:',
|
|
466
|
+
'// consensus-tools tests run --agents 6 --script .consensus/generation.ts',
|
|
467
|
+
'',
|
|
468
|
+
'/**',
|
|
469
|
+
' * This script is intentionally deterministic by default (mockResponse).',
|
|
470
|
+
' * If you provide OPENAI_API_KEY (or --openai-key), the runner will call OpenAI.',
|
|
471
|
+
' */',
|
|
472
|
+
'const script = {',
|
|
473
|
+
" name: 'rx_negation_demo',",
|
|
474
|
+
' task: {',
|
|
475
|
+
" title: 'Negation test: prescription advancement',",
|
|
476
|
+
" desc: 'Tell us why this insulin amount is NOT enough based on patient file, insurance reqs, and doctor notes.',",
|
|
477
|
+
' input: [',
|
|
478
|
+
" 'We want to negation test prescription advancement for diabetic patients.',",
|
|
479
|
+
" 'Explain why the insulin amount is not enough based on:',",
|
|
480
|
+
" '- patient file',",
|
|
481
|
+
" '- insurance info/requirements',",
|
|
482
|
+
" '- doctor notes',",
|
|
483
|
+
" '',",
|
|
484
|
+
" 'Patient file: (paste here)',",
|
|
485
|
+
" '',",
|
|
486
|
+
" 'Insurance info/requirements: (paste here)',",
|
|
487
|
+
" '',",
|
|
488
|
+
" 'Doctor notes: (paste here)'",
|
|
489
|
+
' ].join(\"\\n\")',
|
|
490
|
+
' },',
|
|
491
|
+
" expectedAnswer: 'INSUFFICIENT',",
|
|
492
|
+
' personas: [],',
|
|
493
|
+
' getPersonas(count) {',
|
|
494
|
+
' const n = Math.max(3, Number(count || 3));',
|
|
495
|
+
' const third = Math.ceil(n / 3);',
|
|
496
|
+
'',
|
|
497
|
+
' const mk = (role, i, systemPrompt, personaRole) => ({',
|
|
498
|
+
" id: `${role}_${i + 1}`,",
|
|
499
|
+
" name: `${role.toUpperCase()} Agent ${i + 1}`,",
|
|
500
|
+
' systemPrompt,',
|
|
501
|
+
' role: personaRole',
|
|
502
|
+
' });',
|
|
503
|
+
'',
|
|
504
|
+
' const doctors = Array.from({ length: third }, (_, i) =>',
|
|
505
|
+
' mk(',
|
|
506
|
+
" 'doctor',",
|
|
507
|
+
' i,',
|
|
508
|
+
" 'You are a practicing clinician. Be precise. Use only the provided case context. Focus on medical necessity.',",
|
|
509
|
+
" 'accurate'",
|
|
510
|
+
' )',
|
|
511
|
+
' );',
|
|
512
|
+
' const support = Array.from({ length: third }, (_, i) =>',
|
|
513
|
+
' mk(',
|
|
514
|
+
" 'support',",
|
|
515
|
+
' i,',
|
|
516
|
+
" 'You are customer support at a pharmacy benefits manager. Focus on process, eligibility, required docs, and next steps.',",
|
|
517
|
+
" 'accurate'",
|
|
518
|
+
' )',
|
|
519
|
+
' );',
|
|
520
|
+
' const insurance = Array.from({ length: n - 2 * third }, (_, i) =>',
|
|
521
|
+
' mk(',
|
|
522
|
+
" 'insurance',",
|
|
523
|
+
' i,',
|
|
524
|
+
" 'You are an insurance reviewer. Apply coverage criteria and utilization management rules. Be skeptical and cite requirements.',",
|
|
525
|
+
// Make the last insurance persona contrarian to ensure policies see disagreement.
|
|
526
|
+
" i === (n - 2 * third) - 1 ? 'contrarian' : 'accurate'",
|
|
527
|
+
' )',
|
|
528
|
+
' );',
|
|
529
|
+
'',
|
|
530
|
+
' return [...doctors, ...support, ...insurance].slice(0, n);',
|
|
531
|
+
' },',
|
|
532
|
+
' buildPrompt(persona, task, expectedAnswer) {',
|
|
533
|
+
' return {',
|
|
534
|
+
' system: persona.systemPrompt,',
|
|
535
|
+
' user: [',
|
|
536
|
+
" 'Return JSON: {\"answer\": string, \"confidence\": number, \"evidence\": string[]}',",
|
|
537
|
+
" '',",
|
|
538
|
+
" `TASK: ${task.title}`,",
|
|
539
|
+
" task.desc ? `DESC: ${task.desc}` : '',",
|
|
540
|
+
" '',",
|
|
541
|
+
" `INPUT:\\n${task.input}`,",
|
|
542
|
+
" '',",
|
|
543
|
+
" `EXPECTED (for negation testing): ${expectedAnswer}`",
|
|
544
|
+
' ].filter(Boolean).join(\"\\n\")',
|
|
545
|
+
' };',
|
|
546
|
+
' },',
|
|
547
|
+
' mockResponse(persona, task, expectedAnswer) {',
|
|
548
|
+
' const answer = persona.role === \"contrarian\" ? \"SUFFICIENT\" : expectedAnswer;',
|
|
549
|
+
' return JSON.stringify({',
|
|
550
|
+
' answer,',
|
|
551
|
+
' confidence: persona.role === \"contrarian\" ? 0.2 : 0.9,',
|
|
552
|
+
' evidence: [persona.name, task.title]',
|
|
553
|
+
' });',
|
|
554
|
+
' }',
|
|
555
|
+
'};',
|
|
556
|
+
'',
|
|
557
|
+
'export default script;',
|
|
458
558
|
''
|
|
459
559
|
].join('\n');
|
|
460
560
|
}
|
|
@@ -509,9 +609,14 @@ function commonSh(): string {
|
|
|
509
609
|
' echo "${CONSENSUS_URL%/}/v1/boards/${CONSENSUS_BOARD_ID}"',
|
|
510
610
|
'}',
|
|
511
611
|
'',
|
|
612
|
+
'api_key_env() {',
|
|
613
|
+
' echo "${CONSENSUS_API_KEY_ENV:-CONSENSUS_API_KEY}"',
|
|
614
|
+
'}',
|
|
615
|
+
'',
|
|
512
616
|
'remote_auth_header() {',
|
|
513
|
-
'
|
|
514
|
-
'
|
|
617
|
+
' local name; name="$(api_key_env)"',
|
|
618
|
+
' require_env "$name"',
|
|
619
|
+
' echo "Authorization: Bearer ${!name}"',
|
|
515
620
|
'}',
|
|
516
621
|
'',
|
|
517
622
|
'curl_json() {',
|